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.
Files changed (74) hide show
  1. package/.github/codeql/codeql-config.yml +16 -0
  2. package/.github/workflows/codeql.yml +102 -0
  3. package/.github/workflows/deploy.yml +8 -0
  4. package/.github/workflows/release.yaml +4 -0
  5. package/.github/workflows/test.yml +3 -0
  6. package/CHANGELOG.md +49 -0
  7. package/SECURITY.md +80 -0
  8. package/SECURITY.txt +27 -0
  9. package/config/default.toml +2 -0
  10. package/data/google-crawlers.json +13 -1
  11. package/lib/account.js +62 -25
  12. package/lib/api-routes/account-routes.js +493 -75
  13. package/lib/api-routes/blocklist-routes.js +337 -0
  14. package/lib/api-routes/delivery-test-routes.js +321 -0
  15. package/lib/api-routes/export-routes.js +1 -12
  16. package/lib/api-routes/gateway-routes.js +376 -0
  17. package/lib/api-routes/license-routes.js +142 -0
  18. package/lib/api-routes/mailbox-routes.js +318 -0
  19. package/lib/api-routes/message-routes.js +21 -129
  20. package/lib/api-routes/oauth2-app-routes.js +631 -0
  21. package/lib/api-routes/outbox-routes.js +173 -0
  22. package/lib/api-routes/pubsub-routes.js +98 -0
  23. package/lib/api-routes/route-helpers.js +45 -0
  24. package/lib/api-routes/settings-routes.js +331 -0
  25. package/lib/api-routes/stats-routes.js +77 -0
  26. package/lib/api-routes/submit-routes.js +472 -0
  27. package/lib/api-routes/template-routes.js +7 -55
  28. package/lib/api-routes/token-routes.js +297 -0
  29. package/lib/api-routes/webhook-route-routes.js +152 -0
  30. package/lib/email-client/gmail-client.js +14 -0
  31. package/lib/email-client/imap/mailbox.js +34 -11
  32. package/lib/email-client/imap/subconnection.js +20 -12
  33. package/lib/email-client/imap/sync-operations.js +130 -2
  34. package/lib/email-client/imap-client.js +116 -58
  35. package/lib/email-client/outlook-client.js +85 -13
  36. package/lib/export.js +60 -19
  37. package/lib/imapproxy/imap-core/lib/commands/starttls.js +18 -0
  38. package/lib/imapproxy/imap-core/lib/imap-command.js +7 -2
  39. package/lib/imapproxy/imap-core/lib/imap-connection.js +113 -23
  40. package/lib/imapproxy/imap-core/lib/imap-server.js +25 -1
  41. package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
  42. package/lib/imapproxy/imap-server.js +92 -29
  43. package/lib/message-port-stream.js +113 -16
  44. package/lib/reject-worker-calls.js +42 -0
  45. package/lib/routes-ui.js +37 -8778
  46. package/lib/schemas.js +26 -1
  47. package/lib/tools.js +73 -0
  48. package/lib/ui-routes/account-routes.js +40 -210
  49. package/lib/ui-routes/admin-config-routes.js +913 -487
  50. package/lib/ui-routes/admin-entities-routes.js +1 -0
  51. package/lib/ui-routes/auth-routes.js +1339 -0
  52. package/lib/ui-routes/dashboard-routes.js +188 -0
  53. package/lib/ui-routes/document-store-routes.js +800 -0
  54. package/lib/ui-routes/export-routes.js +217 -0
  55. package/lib/ui-routes/internals-routes.js +354 -0
  56. package/lib/ui-routes/network-config-routes.js +759 -0
  57. package/lib/ui-routes/{oauth-routes.js → oauth-config-routes.js} +371 -91
  58. package/lib/ui-routes/route-helpers.js +316 -0
  59. package/lib/ui-routes/smtp-test-routes.js +236 -0
  60. package/lib/ui-routes/unsubscribe-routes.js +234 -0
  61. package/lib/webhook-request.js +36 -0
  62. package/package.json +17 -17
  63. package/sbom.json +1 -1
  64. package/server.js +217 -19
  65. package/static/licenses.html +52 -182
  66. package/translations/messages.pot +131 -151
  67. package/views/dashboard.hbs +7 -26
  68. package/views/internals/index.hbs +15 -0
  69. package/views/tokens/index.hbs +9 -0
  70. package/workers/api.js +198 -4401
  71. package/workers/export.js +87 -54
  72. package/workers/imap.js +29 -13
  73. package/workers/submit.js +20 -11
  74. package/workers/webhooks.js +6 -20
@@ -5,9 +5,12 @@ const { redis } = require('../db');
5
5
  const { Account } = require('../account');
6
6
  const getSecret = require('../get-secret');
7
7
  const { oauth2Apps, SERVICE_ACCOUNT_PROVIDERS } = require('../oauth2-apps');
8
+ const settings = require('../settings');
9
+ const { autodetectImapSettings } = require('../autodetect-imap-settings');
8
10
  const Boom = require('@hapi/boom');
9
11
  const Joi = require('joi');
10
- const { failAction } = require('../tools');
12
+ const { failAction, getSignedFormData, getLogs, verifyAccountInfo } = require('../tools');
13
+ const { handleError } = require('./route-helpers');
11
14
 
12
15
  const {
13
16
  settingsSchema,
@@ -21,7 +24,9 @@ const {
21
24
  smtpSchema,
22
25
  smtpUpdateSchema,
23
26
  oauth2Schema,
24
- oauth2UpdateSchema
27
+ oauth2UpdateSchema,
28
+ defaultAccountTypeSchema,
29
+ shortMailboxesSchema
25
30
  } = require('../schemas');
26
31
 
27
32
  const { REDIS_PREFIX, MAX_FORM_TTL, NONCE_BYTES } = require('../consts');
@@ -46,7 +51,9 @@ async function init(args) {
46
51
  imapSchema: imapSchemaArg,
47
52
  smtpSchema: smtpSchemaArg,
48
53
  CORS_CONFIG,
49
- AccountTypeSchema
54
+ AccountTypeSchema,
55
+ OAuth2ProviderSchema,
56
+ metrics
50
57
  } = args;
51
58
 
52
59
  // POST /v1/account - Create account
@@ -133,15 +140,7 @@ async function init(args) {
133
140
  let result = await accountObject.create(request.payload);
134
141
  return result;
135
142
  } catch (err) {
136
- request.logger.error({ msg: 'API request failed', err });
137
- if (Boom.isBoom(err)) {
138
- throw err;
139
- }
140
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
141
- if (err.code) {
142
- error.output.payload.code = err.code;
143
- }
144
- throw error;
143
+ handleError(request, err);
145
144
  }
146
145
  },
147
146
 
@@ -281,15 +280,7 @@ async function init(args) {
281
280
 
282
281
  return await accountObject.update(request.payload);
283
282
  } catch (err) {
284
- request.logger.error({ msg: 'API request failed', err });
285
- if (Boom.isBoom(err)) {
286
- throw err;
287
- }
288
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
289
- if (err.code) {
290
- error.output.payload.code = err.code;
291
- }
292
- throw error;
283
+ handleError(request, err);
293
284
  }
294
285
  },
295
286
  options: {
@@ -397,15 +388,7 @@ async function init(args) {
397
388
  try {
398
389
  return { reconnect: await accountObject.requestReconnect(request.payload) };
399
390
  } catch (err) {
400
- request.logger.error({ msg: 'API request failed', err });
401
- if (Boom.isBoom(err)) {
402
- throw err;
403
- }
404
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
405
- if (err.code) {
406
- error.output.payload.code = err.code;
407
- }
408
- throw error;
391
+ handleError(request, err);
409
392
  }
410
393
  },
411
394
  options: {
@@ -464,15 +447,7 @@ async function init(args) {
464
447
  try {
465
448
  return { sync: await accountObject.requestSync(request.payload) };
466
449
  } catch (err) {
467
- request.logger.error({ msg: 'API request failed', err });
468
- if (Boom.isBoom(err)) {
469
- throw err;
470
- }
471
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
472
- if (err.code) {
473
- error.output.payload.code = err.code;
474
- }
475
- throw error;
450
+ handleError(request, err);
476
451
  }
477
452
  },
478
453
  options: {
@@ -532,15 +507,7 @@ async function init(args) {
532
507
  try {
533
508
  return await accountObject.delete({ revoke: request.query.revoke });
534
509
  } catch (err) {
535
- request.logger.error({ msg: 'API request failed', err });
536
- if (Boom.isBoom(err)) {
537
- throw err;
538
- }
539
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
540
- if (err.code) {
541
- error.output.payload.code = err.code;
542
- }
543
- throw error;
510
+ handleError(request, err);
544
511
  }
545
512
  },
546
513
  options: {
@@ -608,15 +575,7 @@ async function init(args) {
608
575
  try {
609
576
  return { flush: await accountObject.flush(request.payload) };
610
577
  } catch (err) {
611
- request.logger.error({ msg: 'API request failed', err });
612
- if (Boom.isBoom(err)) {
613
- throw err;
614
- }
615
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
616
- if (err.code) {
617
- error.output.payload.code = err.code;
618
- }
619
- throw error;
578
+ handleError(request, err);
620
579
  }
621
580
  },
622
581
  options: {
@@ -678,15 +637,7 @@ async function init(args) {
678
637
 
679
638
  return await accountObject.listAccounts(request.query.state, request.query.query, request.query.page, request.query.pageSize);
680
639
  } catch (err) {
681
- request.logger.error({ msg: 'API request failed', err });
682
- if (Boom.isBoom(err)) {
683
- throw err;
684
- }
685
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
686
- if (err.code) {
687
- error.output.payload.code = err.code;
688
- }
689
- throw error;
640
+ handleError(request, err);
690
641
  }
691
642
  },
692
643
 
@@ -910,15 +861,7 @@ async function init(args) {
910
861
 
911
862
  return result;
912
863
  } catch (err) {
913
- request.logger.error({ msg: 'API request failed', err });
914
- if (Boom.isBoom(err)) {
915
- throw err;
916
- }
917
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
918
- if (err.code) {
919
- error.output.payload.code = err.code;
920
- }
921
- throw error;
864
+ handleError(request, err);
922
865
  }
923
866
  },
924
867
  options: {
@@ -1057,6 +1000,481 @@ async function init(args) {
1057
1000
  }
1058
1001
  }
1059
1002
  });
1003
+
1004
+ server.route({
1005
+ method: 'GET',
1006
+ path: '/v1/account/{account}/oauth-token',
1007
+
1008
+ async handler(request) {
1009
+ let enableOAuthTokensApi = await settings.get('enableOAuthTokensApi');
1010
+ if (!enableOAuthTokensApi) {
1011
+ let error = Boom.boomify(new Error('Disabled API endpoint'), { statusCode: 403 });
1012
+ error.output.payload.code = 'ApiEndpointDisabled';
1013
+ throw error;
1014
+ }
1015
+
1016
+ let accountObject = new Account({
1017
+ redis,
1018
+ account: request.params.account,
1019
+ call,
1020
+ secret: await getSecret(),
1021
+ timeout: request.headers['x-ee-timeout']
1022
+ });
1023
+
1024
+ try {
1025
+ const tokenData = await accountObject.getActiveAccessTokenData();
1026
+
1027
+ // Record metric if token was actually refreshed (not cached)
1028
+ if (!tokenData.cached) {
1029
+ const provider = tokenData.provider || 'unknown';
1030
+ metrics(request.logger, 'oauth2TokenRefresh', 'inc', { status: 'success', provider, statusCode: '200' });
1031
+ }
1032
+
1033
+ return tokenData;
1034
+ } catch (err) {
1035
+ // Record failed token refresh
1036
+ const statusCode = String(err.statusCode || 0);
1037
+ metrics(request.logger, 'oauth2TokenRefresh', 'inc', { status: 'failure', provider: 'unknown', statusCode });
1038
+
1039
+ handleError(request, err);
1040
+ }
1041
+ },
1042
+
1043
+ options: {
1044
+ description: 'Get OAuth2 access token',
1045
+ 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.',
1046
+ tags: ['api', 'Account'],
1047
+
1048
+ plugins: {},
1049
+
1050
+ auth: {
1051
+ strategy: 'api-token',
1052
+ mode: 'required'
1053
+ },
1054
+ cors: CORS_CONFIG,
1055
+
1056
+ validate: {
1057
+ options: {
1058
+ stripUnknown: false,
1059
+ abortEarly: false,
1060
+ convert: true
1061
+ },
1062
+ failAction,
1063
+ params: Joi.object({
1064
+ account: accountIdSchema.required()
1065
+ })
1066
+ },
1067
+
1068
+ response: {
1069
+ schema: Joi.object({
1070
+ account: accountIdSchema.required(),
1071
+ user: Joi.string().max(256).required().example('user@example.com').description('Username'),
1072
+ accessToken: Joi.string().max(256).required().example('aGVsbG8gd29ybGQ=').description('Access Token').label('OAuthAccessToken'),
1073
+ provider: OAuth2ProviderSchema
1074
+ }).label('AccountTokenResponse'),
1075
+ failAction: 'log'
1076
+ }
1077
+ }
1078
+ });
1079
+
1080
+ server.route({
1081
+ method: 'GET',
1082
+ path: '/v1/account/{account}/server-signatures',
1083
+
1084
+ async handler(request) {
1085
+ let accountObject = new Account({
1086
+ redis,
1087
+ account: request.params.account,
1088
+ call,
1089
+ secret: await getSecret(),
1090
+ timeout: request.headers['x-ee-timeout']
1091
+ });
1092
+ try {
1093
+ return await accountObject.listSignatures(request.query);
1094
+ } catch (err) {
1095
+ handleError(request, err);
1096
+ }
1097
+ },
1098
+
1099
+ options: {
1100
+ description: 'List Account Signatures',
1101
+ notes: 'Returns signatures associated with the account. Currently only Gmail is supported, and only "new message" signatures from the "sendAs" list are returned.',
1102
+ tags: ['api', 'Account'],
1103
+
1104
+ plugins: {},
1105
+
1106
+ auth: {
1107
+ strategy: 'api-token',
1108
+ mode: 'required'
1109
+ },
1110
+ cors: CORS_CONFIG,
1111
+
1112
+ validate: {
1113
+ options: {
1114
+ stripUnknown: false,
1115
+ abortEarly: false,
1116
+ convert: true
1117
+ },
1118
+ failAction,
1119
+ params: Joi.object({
1120
+ account: accountIdSchema.required()
1121
+ })
1122
+ },
1123
+
1124
+ response: {
1125
+ schema: Joi.object({
1126
+ signatures: Joi.array()
1127
+ .items(
1128
+ Joi.object({
1129
+ address: Joi.string().email().example('user@example.com').description('Email address associated with the signature').required(),
1130
+ signature: Joi.string().example('<div>Best regards,</div>').description('Signature HTML code').required()
1131
+ }).label('SignatureResponseItem')
1132
+ )
1133
+ .label('SignatureEntries')
1134
+ }).label('AccountSignaturesResponse'),
1135
+ failAction: 'log'
1136
+ }
1137
+ }
1138
+ });
1139
+
1140
+ server.route({
1141
+ method: 'POST',
1142
+ path: '/v1/authentication/form',
1143
+
1144
+ async handler(request) {
1145
+ try {
1146
+ let { data, signature } = await getSignedFormData({
1147
+ account: request.payload.account,
1148
+ name: request.payload.name,
1149
+ email: request.payload.email,
1150
+ syncFrom: request.payload.syncFrom,
1151
+ notifyFrom: request.payload.notifyFrom,
1152
+ subconnections: request.payload.subconnections,
1153
+ redirectUrl: request.payload.redirectUrl,
1154
+ delegated: request.payload.delegated,
1155
+ path: request.payload.path && !request.payload.path.includes('*') ? request.payload.path : null,
1156
+ // identify request
1157
+ n: crypto.randomBytes(NONCE_BYTES).toString('base64url'),
1158
+ t: Date.now()
1159
+ });
1160
+
1161
+ let serviceUrl = await settings.get('serviceUrl');
1162
+ if (!serviceUrl) {
1163
+ let err = new Error('Service URL not set up');
1164
+ err.code = 'MissingServiceURLSetup';
1165
+ throw err;
1166
+ }
1167
+
1168
+ let url = new URL(`accounts/new`, serviceUrl);
1169
+
1170
+ url.searchParams.append('data', data);
1171
+ if (signature) {
1172
+ url.searchParams.append('sig', signature);
1173
+ }
1174
+
1175
+ let type = request.payload.type;
1176
+
1177
+ if (type && type !== 'imap') {
1178
+ let oauth2app = await oauth2Apps.get(type);
1179
+ if (!oauth2app || !oauth2app.enabled) {
1180
+ type = false;
1181
+ }
1182
+ }
1183
+
1184
+ if (!type) {
1185
+ let oauth2apps = (await oauth2Apps.list(0, 100)).apps.filter(app => app.includeInListing);
1186
+ if (!oauth2apps.length) {
1187
+ type = 'imap';
1188
+ }
1189
+ }
1190
+
1191
+ if (type) {
1192
+ url.searchParams.append('type', type);
1193
+ }
1194
+
1195
+ return {
1196
+ url: url.href
1197
+ };
1198
+ } catch (err) {
1199
+ handleError(request, err);
1200
+ }
1201
+ },
1202
+
1203
+ options: {
1204
+ description: 'Generate authentication link',
1205
+ notes: 'Generates a redirect link to the hosted authentication form',
1206
+ tags: ['api', 'Account'],
1207
+
1208
+ plugins: {},
1209
+
1210
+ auth: {
1211
+ strategy: 'api-token',
1212
+ mode: 'required'
1213
+ },
1214
+ cors: CORS_CONFIG,
1215
+
1216
+ validate: {
1217
+ options: {
1218
+ stripUnknown: false,
1219
+ abortEarly: false,
1220
+ convert: true
1221
+ },
1222
+ failAction,
1223
+
1224
+ payload: Joi.object({
1225
+ account: Joi.string()
1226
+ .empty('')
1227
+ .trim()
1228
+ .max(256)
1229
+ .allow(null)
1230
+ .example('example')
1231
+ .default(null)
1232
+ .description(
1233
+ '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'
1234
+ ),
1235
+
1236
+ name: Joi.string().empty('').max(256).example('My Email Account').description('Display name for the account'),
1237
+
1238
+ email: Joi.string()
1239
+ .empty('')
1240
+ .email()
1241
+ .example('user@example.com')
1242
+ .description('Specifies the default email address for this account. Users can change it if needed.'),
1243
+
1244
+ delegated: Joi.boolean()
1245
+ .empty('')
1246
+ .truthy('Y', 'true', '1')
1247
+ .falsy('N', 'false', 0)
1248
+ .default(false)
1249
+ .description('If true, configures this account as a shared mailbox. Currently supported by MS365 OAuth2 accounts'),
1250
+
1251
+ syncFrom: accountSchemas.syncFrom,
1252
+ notifyFrom: accountSchemas.notifyFrom,
1253
+
1254
+ subconnections: accountSchemas.subconnections,
1255
+
1256
+ path: accountPathSchema.example(['*']).label('AccountFormPath'),
1257
+ redirectUrl: Joi.string()
1258
+ .empty('')
1259
+ .uri({ scheme: ['http', 'https'], allowRelative: false })
1260
+ .required()
1261
+ .example('https://myapp/account/settings.php')
1262
+ .description('After the authentication process is completed, the user is redirected to this URL'),
1263
+
1264
+ type: defaultAccountTypeSchema
1265
+ }).label('RequestAuthForm')
1266
+ },
1267
+
1268
+ response: {
1269
+ schema: Joi.object({
1270
+ url: Joi.string()
1271
+ .empty('')
1272
+ .uri({ scheme: ['http', 'https'], allowRelative: false })
1273
+ .required()
1274
+ .example('https://ee.example.com/accounts/new?data=eyJhY2NvdW50IjoiZXhh...L0W_BkFH5HW6Krwmr7c&type=imap')
1275
+ .description('Generated URL to the hosted authentication form')
1276
+ }).label('RequestAuthFormResponse'),
1277
+ failAction: 'log'
1278
+ }
1279
+ }
1280
+ });
1281
+
1282
+ server.route({
1283
+ method: 'GET',
1284
+ path: '/v1/logs/{account}',
1285
+
1286
+ async handler(request) {
1287
+ return getLogs(redis, request.params.account);
1288
+ },
1289
+ options: {
1290
+ description: 'Return IMAP logs for an account',
1291
+ notes: 'Output is a downloadable text file',
1292
+ tags: ['api', 'Logs'],
1293
+
1294
+ auth: {
1295
+ strategy: 'api-token',
1296
+ mode: 'required'
1297
+ },
1298
+ cors: CORS_CONFIG,
1299
+
1300
+ plugins: {
1301
+ 'hapi-swagger': {
1302
+ produces: ['text/plain']
1303
+ }
1304
+ },
1305
+
1306
+ validate: {
1307
+ options: {
1308
+ stripUnknown: false,
1309
+ abortEarly: false,
1310
+ convert: true
1311
+ },
1312
+ failAction,
1313
+
1314
+ params: Joi.object({
1315
+ account: accountIdSchema.required()
1316
+ })
1317
+ }
1318
+ }
1319
+ });
1320
+
1321
+ server.route({
1322
+ method: 'POST',
1323
+ path: '/v1/verifyAccount',
1324
+
1325
+ async handler(request) {
1326
+ try {
1327
+ return await verifyAccountInfo(redis, request.payload, request.logger.child({ action: 'verify-account' }));
1328
+ } catch (err) {
1329
+ handleError(request, err);
1330
+ }
1331
+ },
1332
+ options: {
1333
+ description: 'Verify IMAP and SMTP settings',
1334
+ notes: 'Checks if can connect and authenticate using provided account info',
1335
+ tags: ['api', 'Account'],
1336
+
1337
+ plugins: {},
1338
+
1339
+ auth: {
1340
+ strategy: 'api-token',
1341
+ mode: 'required'
1342
+ },
1343
+ cors: CORS_CONFIG,
1344
+
1345
+ validate: {
1346
+ options: {
1347
+ stripUnknown: false,
1348
+ abortEarly: false,
1349
+ convert: true
1350
+ },
1351
+ failAction,
1352
+
1353
+ payload: Joi.object({
1354
+ mailboxes: Joi.boolean().example(false).description('Include mailbox listing in response').default(false).label('IncludeMailboxes'),
1355
+ imap: Joi.object(imapSchema).allow(false).description('IMAP configuration').label('ImapConfiguration'),
1356
+ smtp: Joi.object(smtpSchema).allow(false).description('SMTP configuration').label('SmtpConfiguration'),
1357
+ proxy: settingsSchema.proxyUrl,
1358
+ smtpEhloName: settingsSchema.smtpEhloName
1359
+ }).label('VerifyAccount')
1360
+ },
1361
+ response: {
1362
+ schema: Joi.object({
1363
+ imap: Joi.object({
1364
+ success: Joi.boolean().example(true).description('Was IMAP account verified').label('VerifyImapSuccess'),
1365
+ error: Joi.string()
1366
+ .example('Something went wrong')
1367
+ .description('Error messages for IMAP verification. Only present if success=false')
1368
+ .label('VerifyImapError'),
1369
+ code: Joi.string()
1370
+ .example('ERR_SSL_WRONG_VERSION_NUMBER')
1371
+ .description('Error code. Only present if success=false')
1372
+ .label('VerifyImapCode')
1373
+ }).label('VerifyImapResult'),
1374
+ smtp: Joi.object({
1375
+ success: Joi.boolean().example(true).description('Was SMTP account verified').label('VerifySmtpSuccess'),
1376
+ error: Joi.string()
1377
+ .example('Something went wrong')
1378
+ .description('Error messages for SMTP verification. Only present if success=false')
1379
+ .label('VerifySmtpError'),
1380
+ code: Joi.string()
1381
+ .example('ERR_SSL_WRONG_VERSION_NUMBER')
1382
+ .description('Error code. Only present if success=false')
1383
+ .label('VerifySmtpCode')
1384
+ }).label('VerifySmtpResult'),
1385
+ mailboxes: shortMailboxesSchema
1386
+ }).label('VerifyAccountResponse'),
1387
+ failAction: 'log'
1388
+ }
1389
+ }
1390
+ });
1391
+
1392
+ server.route({
1393
+ method: 'GET',
1394
+ path: '/v1/autoconfig',
1395
+
1396
+ async handler(request) {
1397
+ try {
1398
+ let serverSettings = await autodetectImapSettings(request.query.email, request.app.gt);
1399
+ return serverSettings;
1400
+ } catch (err) {
1401
+ request.logger.error({ msg: 'API request failed', err });
1402
+ if (Boom.isBoom(err)) {
1403
+ throw err;
1404
+ }
1405
+ return { imap: false, smtp: false, _source: 'unknown' };
1406
+ }
1407
+ },
1408
+
1409
+ options: {
1410
+ description: 'Discover Email settings',
1411
+ notes: 'Try to discover IMAP and SMTP settings for an email account',
1412
+ tags: ['api', 'Settings'],
1413
+
1414
+ plugins: {},
1415
+
1416
+ auth: {
1417
+ strategy: 'api-token',
1418
+ mode: 'required'
1419
+ },
1420
+ cors: CORS_CONFIG,
1421
+
1422
+ validate: {
1423
+ options: {
1424
+ stripUnknown: false,
1425
+ abortEarly: false,
1426
+ convert: true
1427
+ },
1428
+ failAction,
1429
+
1430
+ query: Joi.object({
1431
+ email: Joi.string()
1432
+ .email()
1433
+ .required()
1434
+ .example('sender@example.com')
1435
+ .description('Email address to discover email settings for')
1436
+ .label('EmailAddress')
1437
+ }).label('AutodiscoverQuery')
1438
+ },
1439
+
1440
+ response: {
1441
+ schema: Joi.object({
1442
+ imap: Joi.object({
1443
+ auth: Joi.object({
1444
+ user: Joi.string().max(256).example('myuser@gmail.com').description('Account username')
1445
+ }).label('DetectedAuthenticationInfo'),
1446
+
1447
+ host: Joi.string().hostname().required().example('imap.gmail.com').description('Hostname to connect to'),
1448
+ port: Joi.number()
1449
+ .integer()
1450
+ .min(1)
1451
+ .max(64 * 1024)
1452
+ .required()
1453
+ .example(993)
1454
+ .description('Service port number'),
1455
+ secure: Joi.boolean().default(false).example(true).description('Should connection use TLS. Usually true for port 993')
1456
+ }).label('ResolvedServerSettings'),
1457
+ smtp: Joi.object({
1458
+ auth: Joi.object({
1459
+ user: Joi.string().max(256).example('myuser@gmail.com').description('Account username')
1460
+ }).label('DetectedAuthenticationInfo'),
1461
+
1462
+ host: Joi.string().hostname().required().example('imap.gmail.com').description('Hostname to connect to'),
1463
+ port: Joi.number()
1464
+ .integer()
1465
+ .min(1)
1466
+ .max(64 * 1024)
1467
+ .required()
1468
+ .example(993)
1469
+ .description('Service port number'),
1470
+ secure: Joi.boolean().default(false).example(true).description('Should connection use TLS. Usually true for port 993')
1471
+ }).label('DiscoveredServerSettings'),
1472
+ _source: Joi.string().example('srv').description('Source for the detected info')
1473
+ }).label('DiscoveredEmailSettings'),
1474
+ failAction: 'log'
1475
+ }
1476
+ }
1477
+ });
1060
1478
  }
1061
1479
 
1062
1480
  module.exports = init;