emailengine-app 2.68.1 → 2.70.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/deploy.yml +8 -3
- package/.github/workflows/release.yaml +6 -0
- package/CHANGELOG.md +59 -0
- package/Gruntfile.js +3 -1
- package/config/default.toml +2 -0
- package/data/google-crawlers.json +7 -1
- package/getswagger.sh +40 -4
- package/gettext-extract.js +163 -0
- package/lib/account.js +135 -72
- package/lib/api-routes/account-routes.js +684 -106
- package/lib/api-routes/blocklist-routes.js +344 -0
- package/lib/api-routes/chat-routes.js +32 -14
- package/lib/api-routes/delivery-test-routes.js +346 -0
- package/lib/api-routes/export-routes.js +28 -14
- package/lib/api-routes/gateway-routes.js +427 -0
- package/lib/api-routes/license-routes.js +156 -0
- package/lib/api-routes/mailbox-routes.js +344 -0
- package/lib/api-routes/message-routes.js +221 -187
- package/lib/api-routes/oauth2-app-routes.js +697 -0
- package/lib/api-routes/outbox-routes.js +185 -0
- package/lib/api-routes/pubsub-routes.js +102 -0
- package/lib/api-routes/route-helpers.js +58 -0
- package/lib/api-routes/settings-routes.js +357 -0
- package/lib/api-routes/stats-routes.js +111 -0
- package/lib/api-routes/submit-routes.js +461 -0
- package/lib/api-routes/template-routes.js +60 -75
- package/lib/api-routes/token-routes.js +297 -0
- package/lib/api-routes/webhook-route-routes.js +181 -0
- package/lib/autodetect-imap-settings.js +0 -2
- package/lib/consts.js +5 -0
- package/lib/email-client/base-client.js +28 -6
- package/lib/email-client/gmail-client.js +133 -112
- package/lib/email-client/imap/mailbox.js +34 -11
- package/lib/email-client/imap/subconnection.js +20 -13
- package/lib/email-client/imap/sync-operations.js +131 -3
- package/lib/email-client/imap-client.js +152 -75
- package/lib/email-client/notification-handler.js +1 -4
- package/lib/email-client/outlook-client.js +134 -75
- package/lib/export.js +97 -20
- package/lib/feature-flags.js +2 -2
- package/lib/gateway.js +4 -9
- package/lib/get-raw-email.js +5 -5
- package/lib/imapproxy/imap-core/lib/commands/starttls.js +18 -0
- package/lib/imapproxy/imap-core/lib/imap-command.js +6 -1
- package/lib/imapproxy/imap-core/lib/imap-connection.js +106 -24
- package/lib/imapproxy/imap-core/lib/imap-server.js +24 -0
- package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
- package/lib/logger.js +24 -21
- package/lib/message-port-stream.js +113 -16
- package/lib/metrics-collector.js +0 -2
- package/lib/oauth2-apps.js +13 -4
- package/lib/outbox.js +24 -40
- package/lib/redis-operations.js +1 -1
- package/lib/reject-worker-calls.js +42 -0
- package/lib/routes-ui.js +37 -8778
- package/lib/schemas.js +429 -84
- package/lib/sentry.js +139 -0
- package/lib/settings.js +9 -3
- package/lib/stream-encrypt.js +1 -1
- package/lib/templates.js +1 -1
- package/lib/tokens.js +5 -3
- package/lib/tools.js +70 -4
- package/lib/ui-routes/account-routes.js +45 -212
- package/lib/ui-routes/admin-config-routes.js +928 -489
- package/lib/ui-routes/admin-entities-routes.js +1 -0
- package/lib/ui-routes/auth-routes.js +1339 -0
- package/lib/ui-routes/dashboard-routes.js +188 -0
- package/lib/ui-routes/document-store-routes.js +800 -0
- package/lib/ui-routes/export-routes.js +217 -0
- package/lib/ui-routes/internals-routes.js +354 -0
- package/lib/ui-routes/network-config-routes.js +759 -0
- package/lib/ui-routes/{oauth-routes.js → oauth-config-routes.js} +369 -91
- package/lib/ui-routes/route-helpers.js +314 -0
- package/lib/ui-routes/smtp-test-routes.js +236 -0
- package/lib/ui-routes/unsubscribe-routes.js +232 -0
- package/lib/webhook-request.js +36 -0
- package/lib/webhooks.js +8 -4
- package/package.json +13 -12
- package/sbom.json +1 -1
- package/server.js +222 -39
- package/static/licenses.html +160 -300
- package/translations/messages.pot +112 -132
- package/update-info.sh +19 -1
- package/views/config/logging.hbs +48 -0
- package/views/dashboard.hbs +7 -26
- package/views/internals/index.hbs +15 -0
- package/views/tokens/index.hbs +9 -0
- package/workers/api.js +200 -4424
- package/workers/documents.js +2 -22
- package/workers/export.js +103 -104
- package/workers/imap-proxy.js +3 -23
- package/workers/imap.js +32 -36
- package/workers/smtp.js +2 -22
- package/workers/submit.js +26 -35
- package/workers/webhooks.js +9 -43
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Shared helpers used by more than one extracted UI route module - and still by
|
|
4
|
+
// lib/routes-ui.js for the route groups not yet extracted. Lifting these here lets each
|
|
5
|
+
// consumer import the single canonical copy instead of the monolith, so a route group can
|
|
6
|
+
// be extracted without stranding a helper its sibling groups still need. Pure functions
|
|
7
|
+
// and cached data only - this module registers no routes.
|
|
8
|
+
//
|
|
9
|
+
// Every symbol below was moved verbatim from lib/routes-ui.js. The only change is in
|
|
10
|
+
// cachedTemplates: its __dirname-relative paths gain one extra '..' because this file
|
|
11
|
+
// lives one directory deeper (lib/ui-routes/) than the original (lib/).
|
|
12
|
+
|
|
13
|
+
const Boom = require('@hapi/boom');
|
|
14
|
+
const util = require('util');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const pathlib = require('path');
|
|
17
|
+
const psl = require('psl');
|
|
18
|
+
|
|
19
|
+
const settings = require('../settings');
|
|
20
|
+
const { redis } = require('../db');
|
|
21
|
+
const { REDIS_PREFIX } = require('../consts');
|
|
22
|
+
const { oauth2ProviderData } = require('../oauth2-apps');
|
|
23
|
+
const exampleDocumentsPayloads = require('../payload-examples-documents.json');
|
|
24
|
+
|
|
25
|
+
const OPEN_AI_MODELS = [
|
|
26
|
+
{
|
|
27
|
+
name: 'GPT-3 (instruct)',
|
|
28
|
+
id: 'gpt-3.5-turbo-instruct'
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
name: 'GPT-3 (chat)',
|
|
33
|
+
id: 'gpt-3.5-turbo'
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
{
|
|
37
|
+
name: 'GPT-4',
|
|
38
|
+
id: 'gpt-4'
|
|
39
|
+
}
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const cachedTemplates = {
|
|
43
|
+
addressList: fs.readFileSync(pathlib.join(__dirname, '..', '..', 'views', 'partials', 'address_list.hbs'), 'utf-8'),
|
|
44
|
+
testSend: fs.readFileSync(pathlib.join(__dirname, '..', '..', 'views', 'partials', 'test_send.hbs'), 'utf-8')
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const getOpenAiModels = async (models, selectedModel) => {
|
|
48
|
+
let modelList = (await settings.get('openAiModels')) || structuredClone(models);
|
|
49
|
+
|
|
50
|
+
if (selectedModel && !modelList.find(model => model.id === selectedModel)) {
|
|
51
|
+
modelList.unshift({
|
|
52
|
+
name: selectedModel,
|
|
53
|
+
id: selectedModel
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return modelList.map(model => {
|
|
58
|
+
model.selected = model.id === selectedModel;
|
|
59
|
+
return model;
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function formatAccountData(account, gt) {
|
|
64
|
+
account.type = {};
|
|
65
|
+
|
|
66
|
+
if (account.oauth2 && account.oauth2.app) {
|
|
67
|
+
let providerData = oauth2ProviderData(account.oauth2.app.provider);
|
|
68
|
+
account.type = providerData;
|
|
69
|
+
} else if (account.oauth2 && account.oauth2.provider) {
|
|
70
|
+
account.type = oauth2ProviderData(account.oauth2.provider);
|
|
71
|
+
} else if (account.imap && !account.imap.disabled) {
|
|
72
|
+
account.type.icon = 'fa fa-envelope-square';
|
|
73
|
+
account.type.name = 'IMAP';
|
|
74
|
+
account.type.comment = psl.get(account.imap.host) || account.imap.host;
|
|
75
|
+
} else if (account.smtp) {
|
|
76
|
+
account.type.icon = 'fa fa-paper-plane';
|
|
77
|
+
account.type.name = 'SMTP';
|
|
78
|
+
account.type.comment = psl.get(account.smtp.host) || account.smtp.host;
|
|
79
|
+
} else if (account.oauth2 && account.oauth2.auth && account.oauth2.auth.delegatedAccount) {
|
|
80
|
+
account.type.icon = 'fa fa-arrow-alt-circle-right';
|
|
81
|
+
account.type.name = gt.gettext('Delegated');
|
|
82
|
+
account.type.comment = util.format(gt.gettext('Using credentials from "%s"'), account.oauth2.auth.delegatedAccount);
|
|
83
|
+
} else {
|
|
84
|
+
account.type.name = 'N/A';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
switch (account.state) {
|
|
88
|
+
case 'init':
|
|
89
|
+
account.stateLabel = {
|
|
90
|
+
type: 'info',
|
|
91
|
+
name: 'Initializing',
|
|
92
|
+
spinner: true
|
|
93
|
+
};
|
|
94
|
+
break;
|
|
95
|
+
|
|
96
|
+
case 'connecting':
|
|
97
|
+
account.stateLabel = {
|
|
98
|
+
type: 'info',
|
|
99
|
+
name: 'Connecting'
|
|
100
|
+
};
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
case 'syncing':
|
|
104
|
+
account.stateLabel = {
|
|
105
|
+
type: 'info',
|
|
106
|
+
name: 'Syncing',
|
|
107
|
+
spinner: true
|
|
108
|
+
};
|
|
109
|
+
break;
|
|
110
|
+
|
|
111
|
+
case 'connected':
|
|
112
|
+
account.stateLabel = {
|
|
113
|
+
type: 'success',
|
|
114
|
+
name: 'Connected'
|
|
115
|
+
};
|
|
116
|
+
break;
|
|
117
|
+
|
|
118
|
+
case 'disabled':
|
|
119
|
+
account.stateLabel = {
|
|
120
|
+
type: 'secondary',
|
|
121
|
+
name: 'Disabled',
|
|
122
|
+
error: account.disabledReason
|
|
123
|
+
};
|
|
124
|
+
break;
|
|
125
|
+
|
|
126
|
+
case 'authenticationError':
|
|
127
|
+
case 'connectError': {
|
|
128
|
+
let errorMessage = account.lastErrorState ? account.lastErrorState.response : false;
|
|
129
|
+
if (account.lastErrorState) {
|
|
130
|
+
switch (account.lastErrorState.serverResponseCode) {
|
|
131
|
+
case 'ETIMEDOUT':
|
|
132
|
+
errorMessage = gt.gettext('Connection timed out. This usually occurs if you are behind a firewall or connecting to the wrong port.');
|
|
133
|
+
break;
|
|
134
|
+
case 'ClosedAfterConnectTLS':
|
|
135
|
+
errorMessage = gt.gettext('The server unexpectedly closed the connection.');
|
|
136
|
+
break;
|
|
137
|
+
case 'ClosedAfterConnectText':
|
|
138
|
+
errorMessage = gt.gettext(
|
|
139
|
+
'The server unexpectedly closed the connection. This usually happens when attempting to connect to a TLS port without TLS enabled.'
|
|
140
|
+
);
|
|
141
|
+
break;
|
|
142
|
+
case 'ECONNREFUSED':
|
|
143
|
+
errorMessage = gt.gettext(
|
|
144
|
+
'The server refused the connection. This typically occurs if the server is not running, is overloaded, or you are connecting to the wrong host or port.'
|
|
145
|
+
);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
account.stateLabel = {
|
|
151
|
+
type: 'danger',
|
|
152
|
+
name: 'Failed',
|
|
153
|
+
error: errorMessage
|
|
154
|
+
};
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case 'unset':
|
|
158
|
+
account.stateLabel = {
|
|
159
|
+
type: 'light',
|
|
160
|
+
name: 'Not syncing'
|
|
161
|
+
};
|
|
162
|
+
break;
|
|
163
|
+
case 'disconnected':
|
|
164
|
+
account.stateLabel = {
|
|
165
|
+
type: 'warning',
|
|
166
|
+
name: 'Disconnected'
|
|
167
|
+
};
|
|
168
|
+
break;
|
|
169
|
+
case 'paused':
|
|
170
|
+
account.stateLabel = {
|
|
171
|
+
type: 'secondary',
|
|
172
|
+
name: 'Paused'
|
|
173
|
+
};
|
|
174
|
+
break;
|
|
175
|
+
default:
|
|
176
|
+
account.stateLabel = {
|
|
177
|
+
type: 'secondary',
|
|
178
|
+
name: 'N/A'
|
|
179
|
+
};
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Check if IMAP was disabled due to errors - override state label to show error
|
|
184
|
+
if (account.imap && account.imap.disabled && account.lastErrorState) {
|
|
185
|
+
account.stateLabel = {
|
|
186
|
+
type: 'danger',
|
|
187
|
+
name: 'Failed',
|
|
188
|
+
error: account.lastErrorState.description || account.lastErrorState.response
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (account.oauth2) {
|
|
193
|
+
account.oauth2.scopes = []
|
|
194
|
+
.concat(account.oauth2.scope || [])
|
|
195
|
+
.concat(account.oauth2.scopes || [])
|
|
196
|
+
.flatMap(entry => entry.split(/\s+/))
|
|
197
|
+
.map(entry => entry.trim())
|
|
198
|
+
.filter(entry => entry);
|
|
199
|
+
|
|
200
|
+
account.oauth2.expiresStr = account.oauth2.expires ? account.oauth2.expires.toISOString() : false;
|
|
201
|
+
account.oauth2.generatedStr = account.oauth2.generated ? account.oauth2.generated.toISOString() : false;
|
|
202
|
+
|
|
203
|
+
if (account.outlookSubscription) {
|
|
204
|
+
account.outlookSubscription.subscriptionExpiresStr = account.outlookSubscription.expirationDateTime
|
|
205
|
+
? account.outlookSubscription.expirationDateTime.toISOString()
|
|
206
|
+
: false;
|
|
207
|
+
|
|
208
|
+
let state = account.outlookSubscription.state || {};
|
|
209
|
+
|
|
210
|
+
account.outlookSubscription.isValid =
|
|
211
|
+
state.state !== 'error' && account.outlookSubscription.expirationDateTime && account.outlookSubscription.expirationDateTime > new Date();
|
|
212
|
+
|
|
213
|
+
account.outlookSubscription.stateLabel = (state.state || '').replace(/^./, c => c.toUpperCase());
|
|
214
|
+
|
|
215
|
+
if ((state.state === 'created' && !account.outlookSubscription.expirationDateTime) || account.outlookSubscription.expirationDateTime < new Date()) {
|
|
216
|
+
account.outlookSubscription.stateLabel = 'Expired';
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return account;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function formatServerState(state, payload) {
|
|
225
|
+
switch (state) {
|
|
226
|
+
case 'suspended':
|
|
227
|
+
case 'exited':
|
|
228
|
+
case 'disabled':
|
|
229
|
+
return {
|
|
230
|
+
type: 'warning',
|
|
231
|
+
name: state
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
case 'spawning':
|
|
235
|
+
case 'initializing':
|
|
236
|
+
return {
|
|
237
|
+
type: 'info',
|
|
238
|
+
name: state,
|
|
239
|
+
spinner: true
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
case 'listening':
|
|
243
|
+
return {
|
|
244
|
+
type: 'success',
|
|
245
|
+
name: state
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
case 'failed':
|
|
249
|
+
return {
|
|
250
|
+
type: 'danger',
|
|
251
|
+
name: state,
|
|
252
|
+
error: (payload && payload.error && payload.error.message) || null
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
default:
|
|
256
|
+
return {
|
|
257
|
+
type: 'secondary',
|
|
258
|
+
name: 'N/A'
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function getExampleDocumentsPayloads() {
|
|
264
|
+
let date = new Date().toISOString();
|
|
265
|
+
|
|
266
|
+
let examplePayloads = structuredClone(exampleDocumentsPayloads);
|
|
267
|
+
|
|
268
|
+
examplePayloads.forEach(payload => {
|
|
269
|
+
if (payload && payload.content) {
|
|
270
|
+
if (typeof payload.content.date === 'string') {
|
|
271
|
+
payload.content.date = date;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (typeof payload.content.created === 'string') {
|
|
275
|
+
payload.content.created = date;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
return examplePayloads;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function getServerStatus(type) {
|
|
283
|
+
let serverStatus = await redis.hgetall(`${REDIS_PREFIX}${type}`);
|
|
284
|
+
let state = (serverStatus && serverStatus.state) || 'disabled';
|
|
285
|
+
let payload;
|
|
286
|
+
try {
|
|
287
|
+
payload = (serverStatus && typeof serverStatus.payload === 'string' && JSON.parse(serverStatus.payload)) || {};
|
|
288
|
+
} catch (err) {
|
|
289
|
+
// ignore
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return { state, payload, label: formatServerState(state, payload) };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function throwAsBoom(err) {
|
|
296
|
+
if (Boom.isBoom(err)) {
|
|
297
|
+
throw err;
|
|
298
|
+
}
|
|
299
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
300
|
+
if (err.code) {
|
|
301
|
+
error.output.payload.code = err.code;
|
|
302
|
+
}
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
module.exports = {
|
|
307
|
+
OPEN_AI_MODELS,
|
|
308
|
+
cachedTemplates,
|
|
309
|
+
getOpenAiModels,
|
|
310
|
+
formatAccountData,
|
|
311
|
+
getExampleDocumentsPayloads,
|
|
312
|
+
getServerStatus,
|
|
313
|
+
throwAsBoom
|
|
314
|
+
};
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Admin UI routes for the SMTP deliverability test tool (the "Send a test email" feature
|
|
4
|
+
// on an account page). Extracted verbatim from lib/routes-ui.js. These two endpoints call
|
|
5
|
+
// the external Nodemailer test service (api.nodemailer.com) to send a probe message and
|
|
6
|
+
// then fetch its DKIM/SPF analysis.
|
|
7
|
+
|
|
8
|
+
const Joi = require('joi');
|
|
9
|
+
const { fetch: fetchCmd } = require('undici');
|
|
10
|
+
const { Account } = require('../account');
|
|
11
|
+
const { redis } = require('../db');
|
|
12
|
+
const getSecret = require('../get-secret');
|
|
13
|
+
const { failAction, httpAgent } = require('../tools');
|
|
14
|
+
const { accountIdSchema } = require('../schemas');
|
|
15
|
+
const { REDIS_PREFIX } = require('../consts');
|
|
16
|
+
const packageData = require('../../package.json');
|
|
17
|
+
|
|
18
|
+
const SMTP_TEST_HOST = 'https://api.nodemailer.com';
|
|
19
|
+
|
|
20
|
+
function init(args) {
|
|
21
|
+
const { server, call } = args;
|
|
22
|
+
|
|
23
|
+
server.route({
|
|
24
|
+
method: 'POST',
|
|
25
|
+
path: '/admin/smtp/create-test',
|
|
26
|
+
async handler(request) {
|
|
27
|
+
let account = request.payload.account;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
request.logger.info({ msg: 'Request SMTP test', account });
|
|
31
|
+
|
|
32
|
+
let accountObject = new Account({ redis, account, call, secret: await getSecret() });
|
|
33
|
+
|
|
34
|
+
let accountData;
|
|
35
|
+
try {
|
|
36
|
+
accountData = await accountObject.loadAccountData();
|
|
37
|
+
} catch (err) {
|
|
38
|
+
return {
|
|
39
|
+
error: err.message
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let headers = {
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
let res = await fetchCmd(`${SMTP_TEST_HOST}/test-address`, {
|
|
49
|
+
method: 'post',
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
version: packageData.version,
|
|
52
|
+
requestor: '@postalsys/emailengine-app'
|
|
53
|
+
}),
|
|
54
|
+
headers,
|
|
55
|
+
dispatcher: httpAgent.retry
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
let err = new Error(res.statusText || `Invalid response: ${res.status} ${res.statusText}`);
|
|
60
|
+
err.statusCode = res.status;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
err.response = await res.json();
|
|
64
|
+
} catch (err) {
|
|
65
|
+
// ignore
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let testAccount = await res.json();
|
|
72
|
+
if (!testAccount || !testAccount.user) {
|
|
73
|
+
let err = new Error(`Invalid test account`);
|
|
74
|
+
err.status = 500;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
err.response = testAccount;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
// ignore
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
let now = new Date().toISOString();
|
|
87
|
+
let queueResponse = await accountObject.queueMessage(
|
|
88
|
+
{
|
|
89
|
+
account: accountData.account,
|
|
90
|
+
subject: `Delivery test ${now}`,
|
|
91
|
+
text: `Hello
|
|
92
|
+
|
|
93
|
+
This is an automated email to test deliverability settings. If you see this email, you can safely delete it.
|
|
94
|
+
|
|
95
|
+
${now}`,
|
|
96
|
+
html: `<p>Hello</p>
|
|
97
|
+
<p>This is an automated email to test deliverability settings. If you see this email, you can safely delete it.</p>
|
|
98
|
+
<p>${now}</p>`,
|
|
99
|
+
|
|
100
|
+
from: {
|
|
101
|
+
name: accountData.name,
|
|
102
|
+
address: accountData.email
|
|
103
|
+
},
|
|
104
|
+
to: [{ name: 'Delivery Test Server', address: testAccount.address }],
|
|
105
|
+
copy: false,
|
|
106
|
+
gateway: request.payload.gateway,
|
|
107
|
+
feedbackKey: `${REDIS_PREFIX}test-send:${testAccount.user}`,
|
|
108
|
+
deliveryAttempts: 1
|
|
109
|
+
},
|
|
110
|
+
{ source: 'test' }
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return Object.assign(testAccount, queueResponse || {});
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return {
|
|
116
|
+
error: err.message
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
request.logger.error({ msg: 'Failed to request test account', err, account });
|
|
121
|
+
return { success: false, error: err.message };
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
options: {
|
|
125
|
+
validate: {
|
|
126
|
+
options: {
|
|
127
|
+
stripUnknown: true,
|
|
128
|
+
abortEarly: false,
|
|
129
|
+
convert: true
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
failAction,
|
|
133
|
+
|
|
134
|
+
payload: Joi.object({
|
|
135
|
+
account: accountIdSchema.required(),
|
|
136
|
+
gateway: Joi.string().empty('').max(256).example('sendgun').description('Gateway ID')
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
server.route({
|
|
143
|
+
method: 'POST',
|
|
144
|
+
path: '/admin/smtp/check-test',
|
|
145
|
+
async handler(request) {
|
|
146
|
+
let user = request.payload.user;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
request.logger.info({ msg: 'Request SMTP test response', user });
|
|
150
|
+
|
|
151
|
+
let deliveryStatus = (await redis.hgetall(`${REDIS_PREFIX}test-send:${user}`)) || {};
|
|
152
|
+
if (deliveryStatus.success === 'false') {
|
|
153
|
+
let err = new Error(`Failed to deliver email: ${deliveryStatus.error}`);
|
|
154
|
+
err.status = 500;
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let headers = {
|
|
159
|
+
'Content-Type': 'application/json',
|
|
160
|
+
'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
let res = await fetchCmd(`${SMTP_TEST_HOST}/test-address/${user}`, {
|
|
164
|
+
method: 'get',
|
|
165
|
+
headers,
|
|
166
|
+
dispatcher: httpAgent.retry
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (!res.ok) {
|
|
170
|
+
let err = new Error(res.statusText || `Invalid response: ${res.status} ${res.statusText}`);
|
|
171
|
+
err.statusCode = res.status;
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
err.response = await res.json();
|
|
175
|
+
} catch (err) {
|
|
176
|
+
// ignore
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let testResponse = await res.json();
|
|
183
|
+
|
|
184
|
+
if (testResponse) {
|
|
185
|
+
let mainSig =
|
|
186
|
+
testResponse.dkim &&
|
|
187
|
+
testResponse.dkim.results &&
|
|
188
|
+
testResponse.dkim.results.find(entry => entry && entry.status && entry.status.result === 'pass' && entry.status.aligned);
|
|
189
|
+
|
|
190
|
+
if (!mainSig) {
|
|
191
|
+
mainSig =
|
|
192
|
+
testResponse.dkim &&
|
|
193
|
+
testResponse.dkim.results &&
|
|
194
|
+
testResponse.dkim.results.find(entry => entry && entry.status && entry.status.result === 'pass');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!mainSig) {
|
|
198
|
+
mainSig = testResponse.dkim && testResponse.dkim.results && testResponse.dkim.results[0];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
testResponse.mainSig = mainSig || {
|
|
202
|
+
status: {
|
|
203
|
+
result: 'none'
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
if (testResponse.spf && testResponse.spf.status && testResponse.spf.status.comment) {
|
|
208
|
+
testResponse.spf.status.comment = testResponse.spf.status.comment.replace(/^[^:\s]+:s*/, '');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return testResponse;
|
|
213
|
+
} catch (err) {
|
|
214
|
+
request.logger.error({ msg: 'Failed to request test response', err, user });
|
|
215
|
+
return { status: 'error', error: err.message };
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
options: {
|
|
219
|
+
validate: {
|
|
220
|
+
options: {
|
|
221
|
+
stripUnknown: true,
|
|
222
|
+
abortEarly: false,
|
|
223
|
+
convert: true
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
failAction,
|
|
227
|
+
|
|
228
|
+
payload: Joi.object({
|
|
229
|
+
user: Joi.string().guid().description('Test ID')
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = init;
|