emailengine-app 2.68.0 → 2.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/codeql/codeql-config.yml +16 -0
- package/.github/workflows/codeql.yml +102 -0
- package/.github/workflows/deploy.yml +8 -0
- package/.github/workflows/release.yaml +4 -0
- package/.github/workflows/test.yml +3 -0
- package/CHANGELOG.md +49 -0
- package/SECURITY.md +80 -0
- package/SECURITY.txt +27 -0
- package/config/default.toml +2 -0
- package/data/google-crawlers.json +13 -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 +7 -2
- package/lib/imapproxy/imap-core/lib/imap-connection.js +113 -23
- package/lib/imapproxy/imap-core/lib/imap-server.js +25 -1
- package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
- package/lib/imapproxy/imap-server.js +92 -29
- 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 +73 -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 +17 -17
- package/sbom.json +1 -1
- package/server.js +217 -19
- package/static/licenses.html +52 -182
- package/translations/messages.pot +131 -151
- package/views/dashboard.hbs +7 -26
- package/views/internals/index.hbs +15 -0
- package/views/tokens/index.hbs +9 -0
- package/workers/api.js +198 -4401
- package/workers/export.js +87 -54
- package/workers/imap.js +29 -13
- package/workers/submit.js +20 -11
- package/workers/webhooks.js +6 -20
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Boom = require('@hapi/boom');
|
|
4
|
+
const Joi = require('joi');
|
|
5
|
+
const logger = require('../logger');
|
|
6
|
+
const outbox = require('../outbox');
|
|
7
|
+
const { failAction } = require('../tools');
|
|
8
|
+
const { handleError } = require('./route-helpers');
|
|
9
|
+
const { outboxEntrySchema } = require('../schemas');
|
|
10
|
+
|
|
11
|
+
async function init(args) {
|
|
12
|
+
const { server, CORS_CONFIG } = args;
|
|
13
|
+
|
|
14
|
+
server.route({
|
|
15
|
+
method: 'GET',
|
|
16
|
+
path: '/v1/outbox',
|
|
17
|
+
|
|
18
|
+
async handler(request) {
|
|
19
|
+
try {
|
|
20
|
+
return await outbox.list(Object.assign({ logger }, request.query));
|
|
21
|
+
} catch (err) {
|
|
22
|
+
handleError(request, err);
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
options: {
|
|
27
|
+
description: 'List queued messages',
|
|
28
|
+
notes: 'Lists messages in the Outbox',
|
|
29
|
+
tags: ['api', 'Outbox'],
|
|
30
|
+
|
|
31
|
+
plugins: {},
|
|
32
|
+
|
|
33
|
+
auth: {
|
|
34
|
+
strategy: 'api-token',
|
|
35
|
+
mode: 'required'
|
|
36
|
+
},
|
|
37
|
+
cors: CORS_CONFIG,
|
|
38
|
+
|
|
39
|
+
validate: {
|
|
40
|
+
options: {
|
|
41
|
+
stripUnknown: false,
|
|
42
|
+
abortEarly: false,
|
|
43
|
+
convert: true
|
|
44
|
+
},
|
|
45
|
+
failAction,
|
|
46
|
+
|
|
47
|
+
query: Joi.object({
|
|
48
|
+
page: Joi.number()
|
|
49
|
+
.integer()
|
|
50
|
+
.min(0)
|
|
51
|
+
.max(1024 * 1024)
|
|
52
|
+
.default(0)
|
|
53
|
+
.example(0)
|
|
54
|
+
.description('Page number (zero indexed, so use 0 for first page)')
|
|
55
|
+
.label('PageNumber'),
|
|
56
|
+
pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
|
|
57
|
+
}).label('OutbixListFilter')
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
response: {
|
|
61
|
+
schema: Joi.object({
|
|
62
|
+
total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
|
|
63
|
+
page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
|
|
64
|
+
pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
|
|
65
|
+
|
|
66
|
+
messages: Joi.array().items(outboxEntrySchema).label('OutboxListEntries')
|
|
67
|
+
}).label('OutboxListResponse'),
|
|
68
|
+
failAction: 'log'
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
server.route({
|
|
74
|
+
method: 'GET',
|
|
75
|
+
path: '/v1/outbox/{queueId}',
|
|
76
|
+
|
|
77
|
+
async handler(request) {
|
|
78
|
+
try {
|
|
79
|
+
let outboxEntry = await outbox.get({ queueId: request.params.queueId, logger });
|
|
80
|
+
if (!outboxEntry) {
|
|
81
|
+
let message = 'Requested queue entry was not found';
|
|
82
|
+
let error = Boom.boomify(new Error(message), { statusCode: 404 });
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
return outboxEntry;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
handleError(request, err);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
options: {
|
|
92
|
+
description: 'Get queued message',
|
|
93
|
+
notes: 'Gets a queued message in the Outbox',
|
|
94
|
+
tags: ['api', 'Outbox'],
|
|
95
|
+
|
|
96
|
+
plugins: {},
|
|
97
|
+
|
|
98
|
+
auth: {
|
|
99
|
+
strategy: 'api-token',
|
|
100
|
+
mode: 'required'
|
|
101
|
+
},
|
|
102
|
+
cors: CORS_CONFIG,
|
|
103
|
+
|
|
104
|
+
validate: {
|
|
105
|
+
options: {
|
|
106
|
+
stripUnknown: false,
|
|
107
|
+
abortEarly: false,
|
|
108
|
+
convert: true
|
|
109
|
+
},
|
|
110
|
+
failAction,
|
|
111
|
+
|
|
112
|
+
params: Joi.object({
|
|
113
|
+
queueId: Joi.string().max(100).example('d41f0423195f271f').description('Queue identifier for scheduled email').required()
|
|
114
|
+
}).label('OutboxEntryParams')
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
response: {
|
|
118
|
+
schema: outboxEntrySchema,
|
|
119
|
+
failAction: 'log'
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
server.route({
|
|
125
|
+
method: 'DELETE',
|
|
126
|
+
path: '/v1/outbox/{queueId}',
|
|
127
|
+
|
|
128
|
+
async handler(request) {
|
|
129
|
+
try {
|
|
130
|
+
return {
|
|
131
|
+
deleted: await outbox.del({ queueId: request.params.queueId, logger })
|
|
132
|
+
};
|
|
133
|
+
} catch (err) {
|
|
134
|
+
handleError(request, err);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
options: {
|
|
138
|
+
description: 'Remove a message',
|
|
139
|
+
notes: 'Remove a message from the outbox',
|
|
140
|
+
tags: ['api', 'Outbox'],
|
|
141
|
+
|
|
142
|
+
plugins: {},
|
|
143
|
+
|
|
144
|
+
auth: {
|
|
145
|
+
strategy: 'api-token',
|
|
146
|
+
mode: 'required'
|
|
147
|
+
},
|
|
148
|
+
cors: CORS_CONFIG,
|
|
149
|
+
|
|
150
|
+
validate: {
|
|
151
|
+
options: {
|
|
152
|
+
stripUnknown: false,
|
|
153
|
+
abortEarly: false,
|
|
154
|
+
convert: true
|
|
155
|
+
},
|
|
156
|
+
failAction,
|
|
157
|
+
|
|
158
|
+
params: Joi.object({
|
|
159
|
+
queueId: Joi.string().max(100).example('d41f0423195f271f').description('Queue identifier for scheduled email').required()
|
|
160
|
+
}).label('OutboxEntryParams')
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
response: {
|
|
164
|
+
schema: Joi.object({
|
|
165
|
+
deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the message deleted')
|
|
166
|
+
}).label('DeleteOutboxEntryResponse'),
|
|
167
|
+
failAction: 'log'
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = init;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Joi = require('joi');
|
|
4
|
+
const { failAction } = require('../tools');
|
|
5
|
+
const { oauth2Apps } = require('../oauth2-apps');
|
|
6
|
+
const { pubSubErrorSchema } = require('../schemas');
|
|
7
|
+
const { handleError, flattenOAuthAppMeta } = require('./route-helpers');
|
|
8
|
+
|
|
9
|
+
async function init(args) {
|
|
10
|
+
const { server, CORS_CONFIG } = args;
|
|
11
|
+
|
|
12
|
+
server.route({
|
|
13
|
+
method: 'GET',
|
|
14
|
+
path: '/v1/pubsub/status',
|
|
15
|
+
|
|
16
|
+
async handler(request) {
|
|
17
|
+
try {
|
|
18
|
+
let response = await oauth2Apps.list(request.query.page, request.query.pageSize, { pubsub: true });
|
|
19
|
+
|
|
20
|
+
let apps = response.apps.map(app => {
|
|
21
|
+
flattenOAuthAppMeta(app);
|
|
22
|
+
return { id: app.id, name: app.name || null, lastError: app.lastError || null, pubSubError: app.pubSubError || null };
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
total: response.total,
|
|
27
|
+
page: response.page,
|
|
28
|
+
pages: response.pages,
|
|
29
|
+
apps
|
|
30
|
+
};
|
|
31
|
+
} catch (err) {
|
|
32
|
+
handleError(request, err);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
options: {
|
|
37
|
+
description: 'List Pub/Sub status',
|
|
38
|
+
notes: 'Lists Pub/Sub enabled OAuth2 applications and their subscription status',
|
|
39
|
+
tags: ['api', 'OAuth2 Applications'],
|
|
40
|
+
|
|
41
|
+
plugins: {},
|
|
42
|
+
|
|
43
|
+
auth: {
|
|
44
|
+
strategy: 'api-token',
|
|
45
|
+
mode: 'required'
|
|
46
|
+
},
|
|
47
|
+
cors: CORS_CONFIG,
|
|
48
|
+
|
|
49
|
+
validate: {
|
|
50
|
+
options: {
|
|
51
|
+
stripUnknown: false,
|
|
52
|
+
abortEarly: false,
|
|
53
|
+
convert: true
|
|
54
|
+
},
|
|
55
|
+
failAction,
|
|
56
|
+
|
|
57
|
+
query: Joi.object({
|
|
58
|
+
page: Joi.number()
|
|
59
|
+
.integer()
|
|
60
|
+
.min(0)
|
|
61
|
+
.max(1024 * 1024)
|
|
62
|
+
.default(0)
|
|
63
|
+
.example(0)
|
|
64
|
+
.description('Page number (zero indexed, so use 0 for first page)')
|
|
65
|
+
.label('PageNumber'),
|
|
66
|
+
pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
|
|
67
|
+
}).label('PubSubStatusFilter')
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
response: {
|
|
71
|
+
schema: Joi.object({
|
|
72
|
+
total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
|
|
73
|
+
page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
|
|
74
|
+
pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
|
|
75
|
+
|
|
76
|
+
apps: Joi.array()
|
|
77
|
+
.items(
|
|
78
|
+
Joi.object({
|
|
79
|
+
id: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
|
|
80
|
+
name: Joi.string().allow(null).max(256).example('My Gmail App').description('Display name for the app'),
|
|
81
|
+
lastError: Joi.object({
|
|
82
|
+
response: Joi.string().example('Enable the Cloud Pub/Sub API').description('Setup error message')
|
|
83
|
+
})
|
|
84
|
+
.allow(null)
|
|
85
|
+
.description('Setup error from ensurePubsub, if any')
|
|
86
|
+
.label('PubSubSetupError'),
|
|
87
|
+
pubSubError: pubSubErrorSchema.allow(null)
|
|
88
|
+
}).label('PubSubAppStatus')
|
|
89
|
+
)
|
|
90
|
+
.label('PubSubAppStatusList')
|
|
91
|
+
}).label('PubSubStatusResponse'),
|
|
92
|
+
failAction: 'log'
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = init;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Boom = require('@hapi/boom');
|
|
4
|
+
|
|
5
|
+
// Shared helpers for the extracted API route modules under lib/api-routes/.
|
|
6
|
+
|
|
7
|
+
// Standard API error handler. Logs the failure, passes Boom errors through unchanged, and converts
|
|
8
|
+
// plain errors into a Boom error while preserving the original statusCode and an optional
|
|
9
|
+
// machine-readable err.code. This function ALWAYS throws and never returns a value, so callers use it
|
|
10
|
+
// as the final statement inside a catch block: `catch (err) { handleError(request, err); }`.
|
|
11
|
+
function handleError(request, err) {
|
|
12
|
+
request.logger.error({ msg: 'API request failed', err });
|
|
13
|
+
if (Boom.isBoom(err)) {
|
|
14
|
+
throw err;
|
|
15
|
+
}
|
|
16
|
+
// Lower-level libraries (e.g. ImapFlow) flag "this server lacks the required capability" with a
|
|
17
|
+
// machine code but no HTTP status. Surface it as a 422 client error instead of a generic 500 - the
|
|
18
|
+
// request is well-formed, the account just cannot satisfy it (e.g. label search on non-Gmail IMAP).
|
|
19
|
+
let statusCode = err.statusCode || (err.code === 'MissingServerExtension' ? 422 : 500);
|
|
20
|
+
const error = Boom.boomify(err, { statusCode });
|
|
21
|
+
if (err.code) {
|
|
22
|
+
error.output.payload.code = err.code;
|
|
23
|
+
}
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Strips the internal `meta` field from an OAuth2 application object before returning it to the API
|
|
28
|
+
// client, surfacing any authentication or Pub/Sub error messages as `lastError`/`pubSubError`.
|
|
29
|
+
// Pure function: it mutates the passed object and closes over no module state.
|
|
30
|
+
function flattenOAuthAppMeta(app) {
|
|
31
|
+
if (!app.meta) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
let authFlag = app.meta.authFlag;
|
|
35
|
+
let pubSubFlag = app.meta.pubSubFlag;
|
|
36
|
+
delete app.meta;
|
|
37
|
+
if (authFlag && authFlag.message) {
|
|
38
|
+
app.lastError = { response: authFlag.message };
|
|
39
|
+
}
|
|
40
|
+
if (pubSubFlag && pubSubFlag.message) {
|
|
41
|
+
app.pubSubError = { message: pubSubFlag.message, description: pubSubFlag.description || null };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { handleError, flattenOAuthAppMeta };
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Joi = require('joi');
|
|
4
|
+
const { redis, documentsQueue, notifyQueue, submitQueue } = require('../db');
|
|
5
|
+
const settings = require('../settings');
|
|
6
|
+
const consts = require('../consts');
|
|
7
|
+
const { REDIS_PREFIX } = consts;
|
|
8
|
+
const { failAction } = require('../tools');
|
|
9
|
+
const { handleError } = require('./route-helpers');
|
|
10
|
+
const { settingsSchema, settingsQuerySchema } = require('../schemas');
|
|
11
|
+
|
|
12
|
+
async function init(args) {
|
|
13
|
+
const { server, notify, CORS_CONFIG } = args;
|
|
14
|
+
|
|
15
|
+
server.route({
|
|
16
|
+
method: 'GET',
|
|
17
|
+
path: '/v1/settings',
|
|
18
|
+
|
|
19
|
+
async handler(request) {
|
|
20
|
+
let values = {};
|
|
21
|
+
for (let key of Object.keys(request.query)) {
|
|
22
|
+
if (request.query[key]) {
|
|
23
|
+
if (key === 'eventTypes') {
|
|
24
|
+
values[key] = Object.keys(consts)
|
|
25
|
+
.map(key => {
|
|
26
|
+
if (/_NOTIFY?/.test(key)) {
|
|
27
|
+
return consts[key];
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
})
|
|
31
|
+
.map(key => key);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let value = await settings.get(key);
|
|
36
|
+
|
|
37
|
+
if (settings.encryptedKeys.includes(key)) {
|
|
38
|
+
// do not reveal secret values
|
|
39
|
+
// instead show boolean value true if value is set, or false if it's not
|
|
40
|
+
value = value ? true : false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
values[key] = value;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return values;
|
|
47
|
+
},
|
|
48
|
+
options: {
|
|
49
|
+
description: 'List specific settings',
|
|
50
|
+
notes: 'List setting values for specific keys',
|
|
51
|
+
tags: ['api', 'Settings'],
|
|
52
|
+
|
|
53
|
+
auth: {
|
|
54
|
+
strategy: 'api-token',
|
|
55
|
+
mode: 'required'
|
|
56
|
+
},
|
|
57
|
+
cors: CORS_CONFIG,
|
|
58
|
+
|
|
59
|
+
validate: {
|
|
60
|
+
options: {
|
|
61
|
+
stripUnknown: false,
|
|
62
|
+
abortEarly: false,
|
|
63
|
+
convert: true
|
|
64
|
+
},
|
|
65
|
+
failAction,
|
|
66
|
+
|
|
67
|
+
query: Joi.object(settingsQuerySchema).label('SettingsQuery')
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
response: {
|
|
71
|
+
schema: Joi.object(settingsSchema).label('SettingsQueryResponse'),
|
|
72
|
+
failAction: 'log'
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
server.route({
|
|
78
|
+
method: 'POST',
|
|
79
|
+
path: '/v1/settings',
|
|
80
|
+
|
|
81
|
+
async handler(request) {
|
|
82
|
+
let updated = [];
|
|
83
|
+
for (let key of Object.keys(request.payload)) {
|
|
84
|
+
switch (key) {
|
|
85
|
+
case 'serviceUrl': {
|
|
86
|
+
let url = new URL(request.payload.serviceUrl);
|
|
87
|
+
request.payload.serviceUrl = url.origin;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
case 'webhooksEnabled':
|
|
92
|
+
if (!request.payload.webhooksEnabled) {
|
|
93
|
+
// clear error message (if exists)
|
|
94
|
+
await settings.clear('webhookErrorFlag');
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await settings.set(key, request.payload[key]);
|
|
100
|
+
updated.push(key);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Broadcast to all workers (including this one); each reloads its HTTP proxy agent via
|
|
104
|
+
// the 'settings' message handler, so no inline reload is needed here.
|
|
105
|
+
notify('settings', request.payload);
|
|
106
|
+
return { updated };
|
|
107
|
+
},
|
|
108
|
+
options: {
|
|
109
|
+
description: 'Set setting values',
|
|
110
|
+
notes: 'Set setting values for specific keys',
|
|
111
|
+
tags: ['api', 'Settings'],
|
|
112
|
+
|
|
113
|
+
plugins: {},
|
|
114
|
+
|
|
115
|
+
auth: {
|
|
116
|
+
strategy: 'api-token',
|
|
117
|
+
mode: 'required'
|
|
118
|
+
},
|
|
119
|
+
cors: CORS_CONFIG,
|
|
120
|
+
|
|
121
|
+
validate: {
|
|
122
|
+
options: {
|
|
123
|
+
stripUnknown: false,
|
|
124
|
+
abortEarly: false,
|
|
125
|
+
convert: true
|
|
126
|
+
},
|
|
127
|
+
failAction,
|
|
128
|
+
|
|
129
|
+
payload: Joi.object(settingsSchema).label('Settings')
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
response: {
|
|
133
|
+
schema: Joi.object({
|
|
134
|
+
updated: Joi.array().items(Joi.string().example('notifyHeaders')).description('List of updated setting keys').label('UpdatedSettings')
|
|
135
|
+
}).label('SettingsUpdatedResponse'),
|
|
136
|
+
failAction: 'log'
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
server.route({
|
|
142
|
+
method: 'GET',
|
|
143
|
+
path: '/v1/settings/queue/{queue}',
|
|
144
|
+
|
|
145
|
+
async handler(request) {
|
|
146
|
+
try {
|
|
147
|
+
let queue = request.params.queue;
|
|
148
|
+
let values = {
|
|
149
|
+
queue
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const [resActive, resDelayed, resPaused, resWaiting, resMeta] = await redis
|
|
153
|
+
.multi()
|
|
154
|
+
.llen(`${REDIS_PREFIX}bull:${queue}:active`)
|
|
155
|
+
.zcard(`${REDIS_PREFIX}bull:${queue}:delayed`)
|
|
156
|
+
.llen(`${REDIS_PREFIX}bull:${queue}:paused`)
|
|
157
|
+
.llen(`${REDIS_PREFIX}bull:${queue}:wait`)
|
|
158
|
+
.hget(`${REDIS_PREFIX}bull:${queue}:meta`, 'paused')
|
|
159
|
+
.exec();
|
|
160
|
+
|
|
161
|
+
if (resActive[0] || resDelayed[0] || resPaused[0] || resWaiting[0]) {
|
|
162
|
+
// counting failed
|
|
163
|
+
let err = new Error('Failed to count queue lengtho');
|
|
164
|
+
err.statusCode = 500;
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
values.jobs = {
|
|
169
|
+
active: Number(resActive[1]) || 0,
|
|
170
|
+
delayed: Number(resDelayed[1]) || 0,
|
|
171
|
+
paused: Number(resPaused[1]) || 0,
|
|
172
|
+
waiting: Number(resWaiting[1]) || 0
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
values.paused = !!Number(resMeta[1]) || false;
|
|
176
|
+
|
|
177
|
+
return values;
|
|
178
|
+
} catch (err) {
|
|
179
|
+
handleError(request, err);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
options: {
|
|
183
|
+
description: 'Show queue information',
|
|
184
|
+
notes: 'Show queue status and current state',
|
|
185
|
+
tags: ['api', 'Settings'],
|
|
186
|
+
|
|
187
|
+
auth: {
|
|
188
|
+
strategy: 'api-token',
|
|
189
|
+
mode: 'required'
|
|
190
|
+
},
|
|
191
|
+
cors: CORS_CONFIG,
|
|
192
|
+
|
|
193
|
+
validate: {
|
|
194
|
+
options: {
|
|
195
|
+
stripUnknown: false,
|
|
196
|
+
abortEarly: false,
|
|
197
|
+
convert: true
|
|
198
|
+
},
|
|
199
|
+
failAction,
|
|
200
|
+
|
|
201
|
+
params: Joi.object({
|
|
202
|
+
queue: Joi.string()
|
|
203
|
+
.empty('')
|
|
204
|
+
.trim()
|
|
205
|
+
.valid('notify', 'submit', 'documents')
|
|
206
|
+
.required()
|
|
207
|
+
.example('notify')
|
|
208
|
+
.description('Queue ID')
|
|
209
|
+
.label('QueueId')
|
|
210
|
+
})
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
response: {
|
|
214
|
+
schema: Joi.object({
|
|
215
|
+
queue: Joi.string()
|
|
216
|
+
.empty('')
|
|
217
|
+
.trim()
|
|
218
|
+
.valid('notify', 'submit', 'documents')
|
|
219
|
+
.required()
|
|
220
|
+
.example('notify')
|
|
221
|
+
.description('Queue ID')
|
|
222
|
+
.label('QueueIdResponse'),
|
|
223
|
+
jobs: Joi.object({
|
|
224
|
+
active: Joi.number().integer().example(123).description('Jobs that are currently being processed'),
|
|
225
|
+
delayed: Joi.number().integer().example(123).description('Jobs that are processed in the future'),
|
|
226
|
+
paused: Joi.number().integer().example(123).description('Jobs that would be processed once queue processing is resumed'),
|
|
227
|
+
waiting: Joi.number()
|
|
228
|
+
.integer()
|
|
229
|
+
.example(123)
|
|
230
|
+
.description('Jobs that should be processed, but are waiting until there are any free handlers')
|
|
231
|
+
}).label('QueueJobs'),
|
|
232
|
+
paused: Joi.boolean().example(false).description('Is the queue paused or not')
|
|
233
|
+
}).label('SettingsQueueResponse'),
|
|
234
|
+
failAction: 'log'
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
server.route({
|
|
240
|
+
method: 'PUT',
|
|
241
|
+
path: '/v1/settings/queue/{queue}',
|
|
242
|
+
|
|
243
|
+
async handler(request) {
|
|
244
|
+
try {
|
|
245
|
+
let queue = request.params.queue;
|
|
246
|
+
|
|
247
|
+
let queueObj = {
|
|
248
|
+
documents: documentsQueue,
|
|
249
|
+
notify: notifyQueue,
|
|
250
|
+
submit: submitQueue
|
|
251
|
+
}[queue];
|
|
252
|
+
|
|
253
|
+
let values = {
|
|
254
|
+
queue
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
for (let key of Object.keys(request.payload)) {
|
|
258
|
+
switch (key) {
|
|
259
|
+
case 'paused':
|
|
260
|
+
if (request.payload[key]) {
|
|
261
|
+
await queueObj.pause();
|
|
262
|
+
} else {
|
|
263
|
+
await queueObj.resume();
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
values.paused = await queueObj.isPaused();
|
|
270
|
+
|
|
271
|
+
return values;
|
|
272
|
+
} catch (err) {
|
|
273
|
+
handleError(request, err);
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
options: {
|
|
277
|
+
description: 'Set queue settings',
|
|
278
|
+
notes: 'Set queue settings',
|
|
279
|
+
tags: ['api', 'Settings'],
|
|
280
|
+
|
|
281
|
+
plugins: {},
|
|
282
|
+
|
|
283
|
+
auth: {
|
|
284
|
+
strategy: 'api-token',
|
|
285
|
+
mode: 'required'
|
|
286
|
+
},
|
|
287
|
+
cors: CORS_CONFIG,
|
|
288
|
+
|
|
289
|
+
validate: {
|
|
290
|
+
options: {
|
|
291
|
+
stripUnknown: false,
|
|
292
|
+
abortEarly: false,
|
|
293
|
+
convert: true
|
|
294
|
+
},
|
|
295
|
+
failAction,
|
|
296
|
+
|
|
297
|
+
params: Joi.object({
|
|
298
|
+
queue: Joi.string()
|
|
299
|
+
.empty('')
|
|
300
|
+
.trim()
|
|
301
|
+
.valid('notify', 'submit', 'documents')
|
|
302
|
+
.required()
|
|
303
|
+
.example('notify')
|
|
304
|
+
.description('Queue ID')
|
|
305
|
+
.label('QueueIdParam')
|
|
306
|
+
}),
|
|
307
|
+
|
|
308
|
+
payload: Joi.object({
|
|
309
|
+
paused: Joi.boolean().empty('').example(false).description('Set queue state to paused')
|
|
310
|
+
}).label('SettingsPutQueuePayload')
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
response: {
|
|
314
|
+
schema: Joi.object({
|
|
315
|
+
queue: Joi.string()
|
|
316
|
+
.empty('')
|
|
317
|
+
.trim()
|
|
318
|
+
.valid('notify', 'submit', 'documents')
|
|
319
|
+
.required()
|
|
320
|
+
.example('notify')
|
|
321
|
+
.description('Queue ID')
|
|
322
|
+
.label('QueueIdPutResponse'),
|
|
323
|
+
paused: Joi.boolean().example(false).description('Is the queue paused or not')
|
|
324
|
+
}).label('SettingsPutQueueResponse'),
|
|
325
|
+
failAction: 'log'
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
module.exports = init;
|