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
|
@@ -4,10 +4,13 @@ const crypto = require('crypto');
|
|
|
4
4
|
const { redis } = require('../db');
|
|
5
5
|
const { Account } = require('../account');
|
|
6
6
|
const getSecret = require('../get-secret');
|
|
7
|
-
const { oauth2Apps, SERVICE_ACCOUNT_PROVIDERS } = require('../oauth2-apps');
|
|
7
|
+
const { oauth2Apps, LEGACY_KEYS, 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,11 +24,44 @@ const {
|
|
|
21
24
|
smtpSchema,
|
|
22
25
|
smtpUpdateSchema,
|
|
23
26
|
oauth2Schema,
|
|
24
|
-
oauth2UpdateSchema
|
|
27
|
+
oauth2UpdateSchema,
|
|
28
|
+
defaultAccountTypeSchema,
|
|
29
|
+
shortMailboxesSchema,
|
|
30
|
+
ACCOUNT_DISPLAY_STATES,
|
|
31
|
+
errorResponses
|
|
25
32
|
} = require('../schemas');
|
|
26
33
|
|
|
27
34
|
const { REDIS_PREFIX, MAX_FORM_TTL, NONCE_BYTES } = require('../consts');
|
|
28
35
|
|
|
36
|
+
// OAuth2 scope list stored on the account (accountData.oauth2.scope). Reused with a per-route
|
|
37
|
+
// description/label where the same value is exposed under a different field name
|
|
38
|
+
const accountOauth2ScopesSchema = Joi.array()
|
|
39
|
+
.items(Joi.string().example('https://mail.google.com/').label('AccountScopeEntry'))
|
|
40
|
+
.description('OAuth2 scopes granted for this account')
|
|
41
|
+
.label('AccountOauth2Scopes');
|
|
42
|
+
|
|
43
|
+
// Per-protocol result block for the account verification response - the IMAP and SMTP results
|
|
44
|
+
// carry the same fields built by verifyAccountInfo, only labels, wording, and the server
|
|
45
|
+
// response example differ
|
|
46
|
+
const verifyResultSchema = (proto, responseTextExample) =>
|
|
47
|
+
Joi.object({
|
|
48
|
+
success: Joi.boolean().example(true).description(`Was ${proto.toUpperCase()} account verified`).label(`Verify${proto}Success`),
|
|
49
|
+
error: Joi.string()
|
|
50
|
+
.example('Something went wrong')
|
|
51
|
+
.description(`Error messages for ${proto.toUpperCase()} verification. Only present if success=false`)
|
|
52
|
+
.label(`Verify${proto}Error`),
|
|
53
|
+
code: Joi.string().example('ERR_SSL_WRONG_VERSION_NUMBER').description('Error code. Only present if success=false').label(`Verify${proto}Code`),
|
|
54
|
+
statusCode: Joi.number()
|
|
55
|
+
.integer()
|
|
56
|
+
.example(500)
|
|
57
|
+
.description('HTTP-style status code for the error. Only present if success=false')
|
|
58
|
+
.label(`Verify${proto}StatusCode`),
|
|
59
|
+
responseText: Joi.string()
|
|
60
|
+
.example(responseTextExample)
|
|
61
|
+
.description('Server response for the failed verification. Only present if success=false')
|
|
62
|
+
.label(`Verify${proto}ResponseText`)
|
|
63
|
+
}).label(`Verify${proto}Result`);
|
|
64
|
+
|
|
29
65
|
/**
|
|
30
66
|
* Validates that delegation fields are only used with OAuth2 accounts.
|
|
31
67
|
*/
|
|
@@ -46,7 +82,9 @@ async function init(args) {
|
|
|
46
82
|
imapSchema: imapSchemaArg,
|
|
47
83
|
smtpSchema: smtpSchemaArg,
|
|
48
84
|
CORS_CONFIG,
|
|
49
|
-
AccountTypeSchema
|
|
85
|
+
AccountTypeSchema,
|
|
86
|
+
OAuth2ProviderSchema,
|
|
87
|
+
metrics
|
|
50
88
|
} = args;
|
|
51
89
|
|
|
52
90
|
// POST /v1/account - Create account
|
|
@@ -133,15 +171,7 @@ async function init(args) {
|
|
|
133
171
|
let result = await accountObject.create(request.payload);
|
|
134
172
|
return result;
|
|
135
173
|
} catch (err) {
|
|
136
|
-
|
|
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;
|
|
174
|
+
handleError(request, err);
|
|
145
175
|
}
|
|
146
176
|
},
|
|
147
177
|
|
|
@@ -150,7 +180,11 @@ async function init(args) {
|
|
|
150
180
|
notes: 'Registers new IMAP account to be synced',
|
|
151
181
|
tags: ['api', 'Account'],
|
|
152
182
|
|
|
153
|
-
plugins: {
|
|
183
|
+
plugins: {
|
|
184
|
+
'hapi-swagger': {
|
|
185
|
+
responses: errorResponses(400, 401, 403, 404, 429, 500)
|
|
186
|
+
}
|
|
187
|
+
},
|
|
154
188
|
|
|
155
189
|
auth: {
|
|
156
190
|
strategy: 'api-token',
|
|
@@ -248,13 +282,19 @@ async function init(args) {
|
|
|
248
282
|
|
|
249
283
|
response: {
|
|
250
284
|
schema: Joi.object({
|
|
251
|
-
account: accountIdSchema
|
|
285
|
+
account: accountIdSchema,
|
|
252
286
|
state: Joi.string()
|
|
253
|
-
.required()
|
|
254
287
|
.valid('existing', 'new')
|
|
255
288
|
.example('new')
|
|
256
|
-
.description('Is the account new or updated existing')
|
|
257
|
-
.label('CreateAccountState')
|
|
289
|
+
.description('Is the account new or updated existing. Not present when a redirect URL is returned')
|
|
290
|
+
.label('CreateAccountState'),
|
|
291
|
+
redirect: Joi.string()
|
|
292
|
+
.uri()
|
|
293
|
+
.example('https://emailengine.example.com/oauth?account=example')
|
|
294
|
+
.description(
|
|
295
|
+
'OAuth2 authorization URL. Returned instead of account and state when the request used oauth2.authorize=true - send the user to this URL to complete the OAuth2 flow'
|
|
296
|
+
)
|
|
297
|
+
.label('CreateAccountRedirect')
|
|
258
298
|
}).label('CreateAccountResponse'),
|
|
259
299
|
failAction: 'log'
|
|
260
300
|
}
|
|
@@ -281,15 +321,7 @@ async function init(args) {
|
|
|
281
321
|
|
|
282
322
|
return await accountObject.update(request.payload);
|
|
283
323
|
} catch (err) {
|
|
284
|
-
|
|
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;
|
|
324
|
+
handleError(request, err);
|
|
293
325
|
}
|
|
294
326
|
},
|
|
295
327
|
options: {
|
|
@@ -297,7 +329,11 @@ async function init(args) {
|
|
|
297
329
|
notes: 'Updates account information',
|
|
298
330
|
tags: ['api', 'Account'],
|
|
299
331
|
|
|
300
|
-
plugins: {
|
|
332
|
+
plugins: {
|
|
333
|
+
'hapi-swagger': {
|
|
334
|
+
responses: errorResponses(400, 401, 403, 404, 429, 500)
|
|
335
|
+
}
|
|
336
|
+
},
|
|
301
337
|
|
|
302
338
|
auth: {
|
|
303
339
|
strategy: 'api-token',
|
|
@@ -397,15 +433,7 @@ async function init(args) {
|
|
|
397
433
|
try {
|
|
398
434
|
return { reconnect: await accountObject.requestReconnect(request.payload) };
|
|
399
435
|
} catch (err) {
|
|
400
|
-
|
|
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;
|
|
436
|
+
handleError(request, err);
|
|
409
437
|
}
|
|
410
438
|
},
|
|
411
439
|
options: {
|
|
@@ -413,7 +441,11 @@ async function init(args) {
|
|
|
413
441
|
notes: 'Requests connection to be reconnected',
|
|
414
442
|
tags: ['api', 'Account'],
|
|
415
443
|
|
|
416
|
-
plugins: {
|
|
444
|
+
plugins: {
|
|
445
|
+
'hapi-swagger': {
|
|
446
|
+
responses: errorResponses(400, 401, 403, 404, 429, 500)
|
|
447
|
+
}
|
|
448
|
+
},
|
|
417
449
|
|
|
418
450
|
auth: {
|
|
419
451
|
strategy: 'api-token',
|
|
@@ -464,15 +496,7 @@ async function init(args) {
|
|
|
464
496
|
try {
|
|
465
497
|
return { sync: await accountObject.requestSync(request.payload) };
|
|
466
498
|
} catch (err) {
|
|
467
|
-
|
|
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;
|
|
499
|
+
handleError(request, err);
|
|
476
500
|
}
|
|
477
501
|
},
|
|
478
502
|
options: {
|
|
@@ -480,7 +504,11 @@ async function init(args) {
|
|
|
480
504
|
notes: 'Immediately trigger account syncing for IMAP accounts',
|
|
481
505
|
tags: ['api', 'Account'],
|
|
482
506
|
|
|
483
|
-
plugins: {
|
|
507
|
+
plugins: {
|
|
508
|
+
'hapi-swagger': {
|
|
509
|
+
responses: errorResponses(400, 401, 403, 404, 429, 500)
|
|
510
|
+
}
|
|
511
|
+
},
|
|
484
512
|
|
|
485
513
|
auth: {
|
|
486
514
|
strategy: 'api-token',
|
|
@@ -532,15 +560,7 @@ async function init(args) {
|
|
|
532
560
|
try {
|
|
533
561
|
return await accountObject.delete({ revoke: request.query.revoke });
|
|
534
562
|
} catch (err) {
|
|
535
|
-
|
|
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;
|
|
563
|
+
handleError(request, err);
|
|
544
564
|
}
|
|
545
565
|
},
|
|
546
566
|
options: {
|
|
@@ -549,7 +569,11 @@ async function init(args) {
|
|
|
549
569
|
|
|
550
570
|
tags: ['api', 'Account'],
|
|
551
571
|
|
|
552
|
-
plugins: {
|
|
572
|
+
plugins: {
|
|
573
|
+
'hapi-swagger': {
|
|
574
|
+
responses: errorResponses(400, 401, 403, 404, 429, 500)
|
|
575
|
+
}
|
|
576
|
+
},
|
|
553
577
|
|
|
554
578
|
auth: {
|
|
555
579
|
strategy: 'api-token',
|
|
@@ -608,15 +632,7 @@ async function init(args) {
|
|
|
608
632
|
try {
|
|
609
633
|
return { flush: await accountObject.flush(request.payload) };
|
|
610
634
|
} catch (err) {
|
|
611
|
-
|
|
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;
|
|
635
|
+
handleError(request, err);
|
|
620
636
|
}
|
|
621
637
|
},
|
|
622
638
|
options: {
|
|
@@ -624,7 +640,11 @@ async function init(args) {
|
|
|
624
640
|
notes: 'Deletes all email indexes from Redis and ElasticSearch and re-creates the index for that account. You can only run a single flush operation at a time, so you must wait until the previous flush has finished before initiating a new one.',
|
|
625
641
|
tags: ['api', 'Account'],
|
|
626
642
|
|
|
627
|
-
plugins: {
|
|
643
|
+
plugins: {
|
|
644
|
+
'hapi-swagger': {
|
|
645
|
+
responses: errorResponses(400, 401, 403, 404, 429, 500)
|
|
646
|
+
}
|
|
647
|
+
},
|
|
628
648
|
|
|
629
649
|
auth: {
|
|
630
650
|
strategy: 'api-token',
|
|
@@ -678,15 +698,7 @@ async function init(args) {
|
|
|
678
698
|
|
|
679
699
|
return await accountObject.listAccounts(request.query.state, request.query.query, request.query.page, request.query.pageSize);
|
|
680
700
|
} catch (err) {
|
|
681
|
-
|
|
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;
|
|
701
|
+
handleError(request, err);
|
|
690
702
|
}
|
|
691
703
|
},
|
|
692
704
|
|
|
@@ -695,7 +707,11 @@ async function init(args) {
|
|
|
695
707
|
notes: 'Lists registered accounts',
|
|
696
708
|
tags: ['api', 'Account'],
|
|
697
709
|
|
|
698
|
-
plugins: {
|
|
710
|
+
plugins: {
|
|
711
|
+
'hapi-swagger': {
|
|
712
|
+
responses: errorResponses(400, 401, 403, 429, 500)
|
|
713
|
+
}
|
|
714
|
+
},
|
|
699
715
|
|
|
700
716
|
auth: {
|
|
701
717
|
strategy: 'api-token',
|
|
@@ -722,7 +738,7 @@ async function init(args) {
|
|
|
722
738
|
.label('PageNumber'),
|
|
723
739
|
pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize'),
|
|
724
740
|
state: Joi.string()
|
|
725
|
-
.valid(
|
|
741
|
+
.valid(...ACCOUNT_DISPLAY_STATES)
|
|
726
742
|
.example('connected')
|
|
727
743
|
.description('Filter accounts by state')
|
|
728
744
|
.label('AccountState'),
|
|
@@ -736,17 +752,27 @@ async function init(args) {
|
|
|
736
752
|
page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
|
|
737
753
|
pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
|
|
738
754
|
|
|
755
|
+
query: Joi.string()
|
|
756
|
+
.allow(false)
|
|
757
|
+
.example(false)
|
|
758
|
+
.description('Search term used for filtering, or false when no filter was applied')
|
|
759
|
+
.label('AccountsQueryFilter'),
|
|
760
|
+
state: Joi.string()
|
|
761
|
+
.example('*')
|
|
762
|
+
.description('Account state filter used for the listing, or "*" when no filter was applied')
|
|
763
|
+
.label('AccountsStateFilter'),
|
|
764
|
+
|
|
739
765
|
accounts: Joi.array()
|
|
740
766
|
.items(
|
|
741
767
|
Joi.object({
|
|
742
768
|
account: accountIdSchema.required(),
|
|
743
|
-
name: Joi.string().max(256).example('My Email Account').description('Display name for the account'),
|
|
769
|
+
name: Joi.string().allow('').max(256).example('My Email Account').description('Display name for the account'),
|
|
744
770
|
email: Joi.string().empty('').email().example('user@example.com').description('Default email address of the account'),
|
|
745
771
|
type: AccountTypeSchema,
|
|
746
772
|
app: Joi.string().max(256).example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
|
|
747
773
|
state: Joi.string()
|
|
748
774
|
.required()
|
|
749
|
-
.valid(
|
|
775
|
+
.valid(...ACCOUNT_DISPLAY_STATES)
|
|
750
776
|
.example('connected')
|
|
751
777
|
.description('Account state')
|
|
752
778
|
.label('AccountListState'),
|
|
@@ -762,8 +788,11 @@ async function init(args) {
|
|
|
762
788
|
|
|
763
789
|
counters: accountCountersSchema,
|
|
764
790
|
|
|
765
|
-
syncTime: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('Last sync time'),
|
|
766
|
-
lastError: lastErrorSchema.allow(null)
|
|
791
|
+
syncTime: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('Last sync time (IMAP accounts only)'),
|
|
792
|
+
lastError: lastErrorSchema.allow(null),
|
|
793
|
+
delegationError: Joi.string()
|
|
794
|
+
.example('Delegated account was not found')
|
|
795
|
+
.description('Error message if the delegated account could not be resolved')
|
|
767
796
|
}).label('AccountResponseItem')
|
|
768
797
|
)
|
|
769
798
|
.label('AccountEntries')
|
|
@@ -866,16 +895,15 @@ async function init(args) {
|
|
|
866
895
|
oauth2App = await oauth2Apps.get(accountData.oauth2.provider);
|
|
867
896
|
|
|
868
897
|
if (oauth2App) {
|
|
898
|
+
result.type = oauth2App.provider;
|
|
869
899
|
// Check if account is already marked as send-only
|
|
870
900
|
if (accountData.sendOnly) {
|
|
871
901
|
result.sendOnly = true;
|
|
872
|
-
} else {
|
|
873
|
-
result.type = oauth2App.provider;
|
|
874
902
|
}
|
|
875
903
|
if (oauth2App.id !== oauth2App.provider) {
|
|
876
904
|
result.app = oauth2App.id;
|
|
877
905
|
}
|
|
878
|
-
result.baseScopes = oauth2App.
|
|
906
|
+
result.baseScopes = oauth2App.baseScopes || 'imap';
|
|
879
907
|
} else {
|
|
880
908
|
result.type = 'oauth2';
|
|
881
909
|
}
|
|
@@ -910,15 +938,7 @@ async function init(args) {
|
|
|
910
938
|
|
|
911
939
|
return result;
|
|
912
940
|
} catch (err) {
|
|
913
|
-
|
|
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;
|
|
941
|
+
handleError(request, err);
|
|
922
942
|
}
|
|
923
943
|
},
|
|
924
944
|
options: {
|
|
@@ -926,6 +946,12 @@ async function init(args) {
|
|
|
926
946
|
notes: 'Returns stored information about the account. Passwords are not included.',
|
|
927
947
|
tags: ['api', 'Account'],
|
|
928
948
|
|
|
949
|
+
plugins: {
|
|
950
|
+
'hapi-swagger': {
|
|
951
|
+
responses: errorResponses(400, 401, 403, 404, 429, 500)
|
|
952
|
+
}
|
|
953
|
+
},
|
|
954
|
+
|
|
929
955
|
auth: {
|
|
930
956
|
strategy: 'api-token',
|
|
931
957
|
mode: 'required'
|
|
@@ -958,7 +984,7 @@ async function init(args) {
|
|
|
958
984
|
schema: Joi.object({
|
|
959
985
|
account: accountIdSchema.required(),
|
|
960
986
|
|
|
961
|
-
name: Joi.string().max(256).example('My Email Account').description('Display name for the account'),
|
|
987
|
+
name: Joi.string().allow('').max(256).example('My Email Account').description('Display name for the account'),
|
|
962
988
|
email: Joi.string().empty('').email().example('user@example.com').description('Default email address of the account'),
|
|
963
989
|
|
|
964
990
|
copy: Joi.boolean().example(true).description('Copy submitted messages to Sent folder'),
|
|
@@ -974,6 +1000,7 @@ async function init(args) {
|
|
|
974
1000
|
subconnections: accountSchemas.subconnections,
|
|
975
1001
|
|
|
976
1002
|
webhooks: Joi.string()
|
|
1003
|
+
.allow('')
|
|
977
1004
|
.uri({
|
|
978
1005
|
scheme: ['http', 'https'],
|
|
979
1006
|
allowRelative: false
|
|
@@ -987,10 +1014,22 @@ async function init(args) {
|
|
|
987
1014
|
|
|
988
1015
|
smtp: Joi.object(smtpSchema).description('SMTP configuration').label('SMTPResponse'),
|
|
989
1016
|
|
|
990
|
-
oauth2: Joi.object(oauth2Schema)
|
|
1017
|
+
oauth2: Joi.object(oauth2Schema)
|
|
1018
|
+
.keys({
|
|
1019
|
+
scope: accountOauth2ScopesSchema,
|
|
1020
|
+
tokenType: Joi.string().example('Bearer').description('OAuth2 token type'),
|
|
1021
|
+
generated: Joi.date().iso().example('2021-03-22T13:13:31.000Z').description('When the current access token was generated'),
|
|
1022
|
+
refreshTokenGenerated: Joi.date()
|
|
1023
|
+
.iso()
|
|
1024
|
+
.example('2021-03-22T13:13:31.000Z')
|
|
1025
|
+
.description('When the current refresh token was generated'),
|
|
1026
|
+
userFlag: Joi.object().unknown().description('Account-level OAuth2 error flag, if any').label('AccountOauth2UserFlag')
|
|
1027
|
+
})
|
|
1028
|
+
.description('OAuth2 configuration')
|
|
1029
|
+
.label('Oauth2Response'),
|
|
991
1030
|
|
|
992
1031
|
state: Joi.string()
|
|
993
|
-
.valid(
|
|
1032
|
+
.valid(...ACCOUNT_DISPLAY_STATES)
|
|
994
1033
|
.example('connected')
|
|
995
1034
|
.description('Informational account state')
|
|
996
1035
|
.label('AccountInfoState'),
|
|
@@ -1006,11 +1045,30 @@ async function init(args) {
|
|
|
1006
1045
|
description: Joi.string().example('Authentication failed').description('Error information'),
|
|
1007
1046
|
responseCode: Joi.number().integer().example(500).description('Error status code'),
|
|
1008
1047
|
code: Joi.string().example('EAUTH').description('Error type identifier'),
|
|
1009
|
-
command: Joi.string().example('AUTH PLAIN').description('SMTP command that failed')
|
|
1048
|
+
command: Joi.string().example('AUTH PLAIN').description('SMTP command that failed'),
|
|
1049
|
+
networkRouting: Joi.object()
|
|
1050
|
+
.unknown()
|
|
1051
|
+
.allow(null)
|
|
1052
|
+
.description('Network routing information for the delivery attempt')
|
|
1053
|
+
.label('SMTPStatusNetworkRouting')
|
|
1010
1054
|
})
|
|
1011
|
-
.
|
|
1055
|
+
.allow(null)
|
|
1056
|
+
.description('Information about the last SMTP connection attempt. Null when no SMTP connection has been attempted')
|
|
1012
1057
|
.label('SMTPInfoStatus'),
|
|
1013
1058
|
|
|
1059
|
+
syncError: Joi.object({
|
|
1060
|
+
path: Joi.string().example('INBOX').description('Mailbox folder path where the error occurred'),
|
|
1061
|
+
time: Joi.date().iso().example('2021-07-08T07:06:34.336Z').description('When the error occurred'),
|
|
1062
|
+
error: Joi.string().example('Failed to open mailbox').description('Error message')
|
|
1063
|
+
})
|
|
1064
|
+
.unknown()
|
|
1065
|
+
.description('Information about the last mailbox sync error (IMAP accounts only)')
|
|
1066
|
+
.label('AccountSyncError'),
|
|
1067
|
+
|
|
1068
|
+
connections: Joi.number().integer().example(2).description('Number of open IMAP connections for this account (IMAP accounts only)'),
|
|
1069
|
+
|
|
1070
|
+
sendOnly: Joi.boolean().example(false).description('Whether this is a send-only account that does not sync messages'),
|
|
1071
|
+
|
|
1014
1072
|
webhooksCustomHeaders: settingsSchema.webhooksCustomHeaders.label('AccountWebhooksCustomHeaders'),
|
|
1015
1073
|
|
|
1016
1074
|
locale: Joi.string().empty('').max(100).example('fr').description('Optional locale'),
|
|
@@ -1039,17 +1097,23 @@ async function init(args) {
|
|
|
1039
1097
|
'Account quota information if query argument quota=true. This value will be false if the server does not provide quota information.'
|
|
1040
1098
|
),
|
|
1041
1099
|
|
|
1042
|
-
syncTime: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('Last sync time'),
|
|
1100
|
+
syncTime: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('Last sync time (IMAP accounts only)'),
|
|
1043
1101
|
|
|
1044
1102
|
outlookSubscription: Joi.object({
|
|
1045
1103
|
id: Joi.string().description('Microsoft Graph subscription ID'),
|
|
1046
|
-
expirationDateTime: Joi.date().iso().description('When the subscription expires'),
|
|
1104
|
+
expirationDateTime: Joi.date().iso().allow(null).description('When the subscription expires'),
|
|
1047
1105
|
state: Joi.object({
|
|
1048
|
-
state: Joi.string().valid('creating', 'created', 'error').description('Subscription state'),
|
|
1106
|
+
state: Joi.string().valid('creating', 'created', 'renewing', 'error').description('Subscription state'),
|
|
1049
1107
|
time: Joi.number().description('Timestamp of last state change'),
|
|
1050
|
-
error: Joi.string().description('Error message if state is error')
|
|
1051
|
-
|
|
1052
|
-
|
|
1108
|
+
error: Joi.string().allow(null).description('Error message if state is error, null after a successful renewal'),
|
|
1109
|
+
retryCount: Joi.number().integer().description('How many times the subscription renewal has been retried'),
|
|
1110
|
+
createRetryCount: Joi.number().integer().description('How many times the subscription creation has been retried')
|
|
1111
|
+
})
|
|
1112
|
+
.unknown()
|
|
1113
|
+
.description('Current subscription state')
|
|
1114
|
+
})
|
|
1115
|
+
.unknown()
|
|
1116
|
+
.description('Microsoft Graph subscription details (Outlook accounts only)'),
|
|
1053
1117
|
|
|
1054
1118
|
lastError: lastErrorSchema.allow(null)
|
|
1055
1119
|
}).label('AccountResponse'),
|
|
@@ -1057,6 +1121,520 @@ async function init(args) {
|
|
|
1057
1121
|
}
|
|
1058
1122
|
}
|
|
1059
1123
|
});
|
|
1124
|
+
|
|
1125
|
+
server.route({
|
|
1126
|
+
method: 'GET',
|
|
1127
|
+
path: '/v1/account/{account}/oauth-token',
|
|
1128
|
+
|
|
1129
|
+
async handler(request) {
|
|
1130
|
+
let enableOAuthTokensApi = await settings.get('enableOAuthTokensApi');
|
|
1131
|
+
if (!enableOAuthTokensApi) {
|
|
1132
|
+
let error = Boom.boomify(new Error('Disabled API endpoint'), { statusCode: 403 });
|
|
1133
|
+
error.output.payload.code = 'ApiEndpointDisabled';
|
|
1134
|
+
throw error;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
let accountObject = new Account({
|
|
1138
|
+
redis,
|
|
1139
|
+
account: request.params.account,
|
|
1140
|
+
call,
|
|
1141
|
+
secret: await getSecret(),
|
|
1142
|
+
timeout: request.headers['x-ee-timeout']
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
try {
|
|
1146
|
+
const tokenData = await accountObject.getActiveAccessTokenData();
|
|
1147
|
+
|
|
1148
|
+
// Account data stores the OAuth2 app ID as the provider value. Resolve it to the
|
|
1149
|
+
// provider name and expose the app ID separately. Legacy app IDs already equal
|
|
1150
|
+
// their provider name, so skip the lookup for these.
|
|
1151
|
+
if (tokenData.provider && !LEGACY_KEYS.includes(tokenData.provider)) {
|
|
1152
|
+
let oauth2App = await oauth2Apps.get(tokenData.provider);
|
|
1153
|
+
if (oauth2App) {
|
|
1154
|
+
if (oauth2App.id !== oauth2App.provider) {
|
|
1155
|
+
tokenData.app = oauth2App.id;
|
|
1156
|
+
}
|
|
1157
|
+
tokenData.provider = oauth2App.provider;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// Record metric if token was actually refreshed (not cached)
|
|
1162
|
+
if (!tokenData.cached) {
|
|
1163
|
+
const provider = tokenData.provider || 'unknown';
|
|
1164
|
+
metrics(request.logger, 'oauth2TokenRefresh', 'inc', { status: 'success', provider, statusCode: '200' });
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
return tokenData;
|
|
1168
|
+
} catch (err) {
|
|
1169
|
+
// Record failed token refresh
|
|
1170
|
+
const statusCode = String(err.statusCode || 0);
|
|
1171
|
+
metrics(request.logger, 'oauth2TokenRefresh', 'inc', { status: 'failure', provider: 'unknown', statusCode });
|
|
1172
|
+
|
|
1173
|
+
handleError(request, err);
|
|
1174
|
+
}
|
|
1175
|
+
},
|
|
1176
|
+
|
|
1177
|
+
options: {
|
|
1178
|
+
description: 'Get OAuth2 access token',
|
|
1179
|
+
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.',
|
|
1180
|
+
tags: ['api', 'Account'],
|
|
1181
|
+
|
|
1182
|
+
plugins: {
|
|
1183
|
+
'hapi-swagger': {
|
|
1184
|
+
responses: errorResponses(400, 401, 403, 404, 429, 500)
|
|
1185
|
+
}
|
|
1186
|
+
},
|
|
1187
|
+
|
|
1188
|
+
auth: {
|
|
1189
|
+
strategy: 'api-token',
|
|
1190
|
+
mode: 'required'
|
|
1191
|
+
},
|
|
1192
|
+
cors: CORS_CONFIG,
|
|
1193
|
+
|
|
1194
|
+
validate: {
|
|
1195
|
+
options: {
|
|
1196
|
+
stripUnknown: false,
|
|
1197
|
+
abortEarly: false,
|
|
1198
|
+
convert: true
|
|
1199
|
+
},
|
|
1200
|
+
failAction,
|
|
1201
|
+
params: Joi.object({
|
|
1202
|
+
account: accountIdSchema.required()
|
|
1203
|
+
})
|
|
1204
|
+
},
|
|
1205
|
+
|
|
1206
|
+
response: {
|
|
1207
|
+
schema: Joi.object({
|
|
1208
|
+
account: accountIdSchema.required(),
|
|
1209
|
+
user: Joi.string().max(256).required().example('user@example.com').description('Username'),
|
|
1210
|
+
accessToken: Joi.string()
|
|
1211
|
+
.max(4 * 4096)
|
|
1212
|
+
.example('aGVsbG8gd29ybGQ=')
|
|
1213
|
+
.description('Access Token. Can be missing if the external authentication server provided password-based credentials')
|
|
1214
|
+
.label('OAuthAccessToken'),
|
|
1215
|
+
provider: OAuth2ProviderSchema,
|
|
1216
|
+
app: Joi.string().max(256).example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
|
|
1217
|
+
registeredScopes: accountOauth2ScopesSchema.description('OAuth2 scopes registered for this account').label('RegisteredScopes'),
|
|
1218
|
+
expires: Joi.date().iso().example('2021-03-22T13:13:31.000Z').description('When the access token expires'),
|
|
1219
|
+
cached: Joi.boolean().example(false).description('Whether the token was returned from cache or was freshly renewed')
|
|
1220
|
+
}).label('AccountTokenResponse'),
|
|
1221
|
+
failAction: 'log'
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
server.route({
|
|
1227
|
+
method: 'GET',
|
|
1228
|
+
path: '/v1/account/{account}/server-signatures',
|
|
1229
|
+
|
|
1230
|
+
async handler(request) {
|
|
1231
|
+
let accountObject = new Account({
|
|
1232
|
+
redis,
|
|
1233
|
+
account: request.params.account,
|
|
1234
|
+
call,
|
|
1235
|
+
secret: await getSecret(),
|
|
1236
|
+
timeout: request.headers['x-ee-timeout']
|
|
1237
|
+
});
|
|
1238
|
+
try {
|
|
1239
|
+
return await accountObject.listSignatures(request.query);
|
|
1240
|
+
} catch (err) {
|
|
1241
|
+
handleError(request, err);
|
|
1242
|
+
}
|
|
1243
|
+
},
|
|
1244
|
+
|
|
1245
|
+
options: {
|
|
1246
|
+
description: 'List Account Signatures',
|
|
1247
|
+
notes: 'Returns signatures associated with the account. Currently only Gmail is supported, and only "new message" signatures from the "sendAs" list are returned.',
|
|
1248
|
+
tags: ['api', 'Account'],
|
|
1249
|
+
|
|
1250
|
+
plugins: {
|
|
1251
|
+
'hapi-swagger': {
|
|
1252
|
+
responses: errorResponses(400, 401, 403, 404, 429, 500)
|
|
1253
|
+
}
|
|
1254
|
+
},
|
|
1255
|
+
|
|
1256
|
+
auth: {
|
|
1257
|
+
strategy: 'api-token',
|
|
1258
|
+
mode: 'required'
|
|
1259
|
+
},
|
|
1260
|
+
cors: CORS_CONFIG,
|
|
1261
|
+
|
|
1262
|
+
validate: {
|
|
1263
|
+
options: {
|
|
1264
|
+
stripUnknown: false,
|
|
1265
|
+
abortEarly: false,
|
|
1266
|
+
convert: true
|
|
1267
|
+
},
|
|
1268
|
+
failAction,
|
|
1269
|
+
params: Joi.object({
|
|
1270
|
+
account: accountIdSchema.required()
|
|
1271
|
+
})
|
|
1272
|
+
},
|
|
1273
|
+
|
|
1274
|
+
response: {
|
|
1275
|
+
schema: Joi.object({
|
|
1276
|
+
signatures: Joi.array()
|
|
1277
|
+
.items(
|
|
1278
|
+
Joi.object({
|
|
1279
|
+
address: Joi.string().email().example('user@example.com').description('Email address associated with the signature').required(),
|
|
1280
|
+
signature: Joi.string().example('<div>Best regards,</div>').description('Signature HTML code').required()
|
|
1281
|
+
}).label('SignatureResponseItem')
|
|
1282
|
+
)
|
|
1283
|
+
.label('SignatureEntries'),
|
|
1284
|
+
signaturesSupported: Joi.boolean()
|
|
1285
|
+
.example(true)
|
|
1286
|
+
.description('Whether the account type supports listing signatures (currently Gmail API accounts only)')
|
|
1287
|
+
}).label('AccountSignaturesResponse'),
|
|
1288
|
+
failAction: 'log'
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
server.route({
|
|
1294
|
+
method: 'POST',
|
|
1295
|
+
path: '/v1/authentication/form',
|
|
1296
|
+
|
|
1297
|
+
async handler(request) {
|
|
1298
|
+
try {
|
|
1299
|
+
let { data, signature } = await getSignedFormData({
|
|
1300
|
+
account: request.payload.account,
|
|
1301
|
+
name: request.payload.name,
|
|
1302
|
+
email: request.payload.email,
|
|
1303
|
+
syncFrom: request.payload.syncFrom,
|
|
1304
|
+
notifyFrom: request.payload.notifyFrom,
|
|
1305
|
+
subconnections: request.payload.subconnections,
|
|
1306
|
+
redirectUrl: request.payload.redirectUrl,
|
|
1307
|
+
delegated: request.payload.delegated,
|
|
1308
|
+
path: request.payload.path && !request.payload.path.includes('*') ? request.payload.path : null,
|
|
1309
|
+
// identify request
|
|
1310
|
+
n: crypto.randomBytes(NONCE_BYTES).toString('base64url'),
|
|
1311
|
+
t: Date.now()
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
let serviceUrl = await settings.get('serviceUrl');
|
|
1315
|
+
if (!serviceUrl) {
|
|
1316
|
+
let err = new Error('Service URL not set up');
|
|
1317
|
+
err.code = 'MissingServiceURLSetup';
|
|
1318
|
+
throw err;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
let url = new URL(`accounts/new`, serviceUrl);
|
|
1322
|
+
|
|
1323
|
+
url.searchParams.append('data', data);
|
|
1324
|
+
if (signature) {
|
|
1325
|
+
url.searchParams.append('sig', signature);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
let type = request.payload.type;
|
|
1329
|
+
|
|
1330
|
+
if (type && type !== 'imap') {
|
|
1331
|
+
let oauth2app = await oauth2Apps.get(type);
|
|
1332
|
+
if (!oauth2app || !oauth2app.enabled) {
|
|
1333
|
+
type = false;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
if (!type) {
|
|
1338
|
+
let oauth2apps = (await oauth2Apps.list(0, 100)).apps.filter(app => app.includeInListing);
|
|
1339
|
+
if (!oauth2apps.length) {
|
|
1340
|
+
type = 'imap';
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
if (type) {
|
|
1345
|
+
url.searchParams.append('type', type);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
return {
|
|
1349
|
+
url: url.href
|
|
1350
|
+
};
|
|
1351
|
+
} catch (err) {
|
|
1352
|
+
handleError(request, err);
|
|
1353
|
+
}
|
|
1354
|
+
},
|
|
1355
|
+
|
|
1356
|
+
options: {
|
|
1357
|
+
description: 'Generate authentication link',
|
|
1358
|
+
notes: 'Generates a redirect link to the hosted authentication form',
|
|
1359
|
+
tags: ['api', 'Account'],
|
|
1360
|
+
|
|
1361
|
+
plugins: {
|
|
1362
|
+
'hapi-swagger': {
|
|
1363
|
+
responses: errorResponses(400, 401, 403, 404, 429, 500)
|
|
1364
|
+
}
|
|
1365
|
+
},
|
|
1366
|
+
|
|
1367
|
+
auth: {
|
|
1368
|
+
strategy: 'api-token',
|
|
1369
|
+
mode: 'required'
|
|
1370
|
+
},
|
|
1371
|
+
cors: CORS_CONFIG,
|
|
1372
|
+
|
|
1373
|
+
validate: {
|
|
1374
|
+
options: {
|
|
1375
|
+
stripUnknown: false,
|
|
1376
|
+
abortEarly: false,
|
|
1377
|
+
convert: true
|
|
1378
|
+
},
|
|
1379
|
+
failAction,
|
|
1380
|
+
|
|
1381
|
+
payload: Joi.object({
|
|
1382
|
+
account: Joi.string()
|
|
1383
|
+
.empty('')
|
|
1384
|
+
.trim()
|
|
1385
|
+
.max(256)
|
|
1386
|
+
.allow(null)
|
|
1387
|
+
.example('example')
|
|
1388
|
+
.default(null)
|
|
1389
|
+
.description(
|
|
1390
|
+
'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'
|
|
1391
|
+
),
|
|
1392
|
+
|
|
1393
|
+
name: Joi.string().empty('').max(256).example('My Email Account').description('Display name for the account'),
|
|
1394
|
+
|
|
1395
|
+
email: Joi.string()
|
|
1396
|
+
.empty('')
|
|
1397
|
+
.email()
|
|
1398
|
+
.example('user@example.com')
|
|
1399
|
+
.description('Specifies the default email address for this account. Users can change it if needed.'),
|
|
1400
|
+
|
|
1401
|
+
delegated: Joi.boolean()
|
|
1402
|
+
.empty('')
|
|
1403
|
+
.truthy('Y', 'true', '1')
|
|
1404
|
+
.falsy('N', 'false', 0)
|
|
1405
|
+
.default(false)
|
|
1406
|
+
.description('If true, configures this account as a shared mailbox. Currently supported by MS365 OAuth2 accounts'),
|
|
1407
|
+
|
|
1408
|
+
syncFrom: accountSchemas.syncFrom,
|
|
1409
|
+
notifyFrom: accountSchemas.notifyFrom,
|
|
1410
|
+
|
|
1411
|
+
subconnections: accountSchemas.subconnections,
|
|
1412
|
+
|
|
1413
|
+
path: accountPathSchema.example(['*']).label('AccountFormPath'),
|
|
1414
|
+
redirectUrl: Joi.string()
|
|
1415
|
+
.empty('')
|
|
1416
|
+
.uri({ scheme: ['http', 'https'], allowRelative: false })
|
|
1417
|
+
.required()
|
|
1418
|
+
.example('https://myapp/account/settings.php')
|
|
1419
|
+
.description('After the authentication process is completed, the user is redirected to this URL'),
|
|
1420
|
+
|
|
1421
|
+
type: defaultAccountTypeSchema
|
|
1422
|
+
}).label('RequestAuthForm')
|
|
1423
|
+
},
|
|
1424
|
+
|
|
1425
|
+
response: {
|
|
1426
|
+
schema: Joi.object({
|
|
1427
|
+
url: Joi.string()
|
|
1428
|
+
.empty('')
|
|
1429
|
+
.uri({ scheme: ['http', 'https'], allowRelative: false })
|
|
1430
|
+
.required()
|
|
1431
|
+
.example('https://ee.example.com/accounts/new?data=eyJhY2NvdW50IjoiZXhh...L0W_BkFH5HW6Krwmr7c&type=imap')
|
|
1432
|
+
.description('Generated URL to the hosted authentication form')
|
|
1433
|
+
}).label('RequestAuthFormResponse'),
|
|
1434
|
+
failAction: 'log'
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
server.route({
|
|
1440
|
+
method: 'GET',
|
|
1441
|
+
path: '/v1/logs/{account}',
|
|
1442
|
+
|
|
1443
|
+
async handler(request) {
|
|
1444
|
+
return getLogs(redis, request.params.account);
|
|
1445
|
+
},
|
|
1446
|
+
options: {
|
|
1447
|
+
description: 'Return IMAP logs for an account',
|
|
1448
|
+
notes: 'Output is a downloadable text file',
|
|
1449
|
+
tags: ['api', 'Logs'],
|
|
1450
|
+
|
|
1451
|
+
auth: {
|
|
1452
|
+
strategy: 'api-token',
|
|
1453
|
+
mode: 'required'
|
|
1454
|
+
},
|
|
1455
|
+
cors: CORS_CONFIG,
|
|
1456
|
+
|
|
1457
|
+
plugins: {
|
|
1458
|
+
'hapi-swagger': {
|
|
1459
|
+
produces: ['text/plain'],
|
|
1460
|
+
responses: errorResponses(400, 401, 403, 429, 500)
|
|
1461
|
+
}
|
|
1462
|
+
},
|
|
1463
|
+
|
|
1464
|
+
validate: {
|
|
1465
|
+
options: {
|
|
1466
|
+
stripUnknown: false,
|
|
1467
|
+
abortEarly: false,
|
|
1468
|
+
convert: true
|
|
1469
|
+
},
|
|
1470
|
+
failAction,
|
|
1471
|
+
|
|
1472
|
+
params: Joi.object({
|
|
1473
|
+
account: accountIdSchema.required()
|
|
1474
|
+
})
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
server.route({
|
|
1480
|
+
method: 'POST',
|
|
1481
|
+
path: '/v1/verifyAccount',
|
|
1482
|
+
|
|
1483
|
+
async handler(request) {
|
|
1484
|
+
try {
|
|
1485
|
+
return await verifyAccountInfo(redis, request.payload, request.logger.child({ action: 'verify-account' }));
|
|
1486
|
+
} catch (err) {
|
|
1487
|
+
handleError(request, err);
|
|
1488
|
+
}
|
|
1489
|
+
},
|
|
1490
|
+
options: {
|
|
1491
|
+
description: 'Verify IMAP and SMTP settings',
|
|
1492
|
+
notes: 'Checks if can connect and authenticate using provided account info',
|
|
1493
|
+
tags: ['api', 'Account'],
|
|
1494
|
+
|
|
1495
|
+
plugins: {
|
|
1496
|
+
'hapi-swagger': {
|
|
1497
|
+
responses: errorResponses(400, 401, 403, 429, 500)
|
|
1498
|
+
}
|
|
1499
|
+
},
|
|
1500
|
+
|
|
1501
|
+
auth: {
|
|
1502
|
+
strategy: 'api-token',
|
|
1503
|
+
mode: 'required'
|
|
1504
|
+
},
|
|
1505
|
+
cors: CORS_CONFIG,
|
|
1506
|
+
|
|
1507
|
+
validate: {
|
|
1508
|
+
options: {
|
|
1509
|
+
stripUnknown: false,
|
|
1510
|
+
abortEarly: false,
|
|
1511
|
+
convert: true
|
|
1512
|
+
},
|
|
1513
|
+
failAction,
|
|
1514
|
+
|
|
1515
|
+
payload: Joi.object({
|
|
1516
|
+
mailboxes: Joi.boolean().example(false).description('Include mailbox listing in response').default(false).label('IncludeMailboxes'),
|
|
1517
|
+
imap: Joi.object(imapSchema).allow(false).description('IMAP configuration').label('ImapConfiguration'),
|
|
1518
|
+
smtp: Joi.object(smtpSchema).allow(false).description('SMTP configuration').label('SmtpConfiguration'),
|
|
1519
|
+
proxy: settingsSchema.proxyUrl,
|
|
1520
|
+
smtpEhloName: settingsSchema.smtpEhloName
|
|
1521
|
+
}).label('VerifyAccount')
|
|
1522
|
+
},
|
|
1523
|
+
response: {
|
|
1524
|
+
schema: Joi.object({
|
|
1525
|
+
imap: verifyResultSchema('Imap', 'NO [AUTHENTICATIONFAILED] Invalid credentials'),
|
|
1526
|
+
smtp: verifyResultSchema('Smtp', '535 Authentication failed'),
|
|
1527
|
+
mailboxes: shortMailboxesSchema
|
|
1528
|
+
}).label('VerifyAccountResponse'),
|
|
1529
|
+
failAction: 'log'
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
server.route({
|
|
1535
|
+
method: 'GET',
|
|
1536
|
+
path: '/v1/autoconfig',
|
|
1537
|
+
|
|
1538
|
+
async handler(request) {
|
|
1539
|
+
try {
|
|
1540
|
+
let serverSettings = await autodetectImapSettings(request.query.email, request.app.gt);
|
|
1541
|
+
return serverSettings;
|
|
1542
|
+
} catch (err) {
|
|
1543
|
+
request.logger.error({ msg: 'API request failed', err });
|
|
1544
|
+
if (Boom.isBoom(err)) {
|
|
1545
|
+
throw err;
|
|
1546
|
+
}
|
|
1547
|
+
return { imap: false, smtp: false, _source: 'unknown' };
|
|
1548
|
+
}
|
|
1549
|
+
},
|
|
1550
|
+
|
|
1551
|
+
options: {
|
|
1552
|
+
description: 'Discover Email settings',
|
|
1553
|
+
notes: 'Try to discover IMAP and SMTP settings for an email account',
|
|
1554
|
+
tags: ['api', 'Settings'],
|
|
1555
|
+
|
|
1556
|
+
plugins: {
|
|
1557
|
+
'hapi-swagger': {
|
|
1558
|
+
responses: errorResponses(400, 401, 403, 429, 500)
|
|
1559
|
+
}
|
|
1560
|
+
},
|
|
1561
|
+
|
|
1562
|
+
auth: {
|
|
1563
|
+
strategy: 'api-token',
|
|
1564
|
+
mode: 'required'
|
|
1565
|
+
},
|
|
1566
|
+
cors: CORS_CONFIG,
|
|
1567
|
+
|
|
1568
|
+
validate: {
|
|
1569
|
+
options: {
|
|
1570
|
+
stripUnknown: false,
|
|
1571
|
+
abortEarly: false,
|
|
1572
|
+
convert: true
|
|
1573
|
+
},
|
|
1574
|
+
failAction,
|
|
1575
|
+
|
|
1576
|
+
query: Joi.object({
|
|
1577
|
+
email: Joi.string()
|
|
1578
|
+
.email()
|
|
1579
|
+
.required()
|
|
1580
|
+
.example('sender@example.com')
|
|
1581
|
+
.description('Email address to discover email settings for')
|
|
1582
|
+
.label('EmailAddress')
|
|
1583
|
+
}).label('AutodiscoverQuery')
|
|
1584
|
+
},
|
|
1585
|
+
|
|
1586
|
+
response: {
|
|
1587
|
+
schema: Joi.object({
|
|
1588
|
+
imap: Joi.object({
|
|
1589
|
+
auth: Joi.object({
|
|
1590
|
+
user: Joi.string().max(256).example('myuser@gmail.com').description('Account username')
|
|
1591
|
+
}).label('DetectedAuthenticationInfo'),
|
|
1592
|
+
|
|
1593
|
+
host: Joi.string().hostname().example('imap.gmail.com').description('Hostname to connect to'),
|
|
1594
|
+
port: Joi.number()
|
|
1595
|
+
.integer()
|
|
1596
|
+
.min(1)
|
|
1597
|
+
.max(64 * 1024)
|
|
1598
|
+
.example(993)
|
|
1599
|
+
.description('Service port number'),
|
|
1600
|
+
secure: Joi.boolean().default(false).example(true).description('Should connection use TLS. Usually true for port 993')
|
|
1601
|
+
})
|
|
1602
|
+
.allow(false)
|
|
1603
|
+
.description('Discovered IMAP settings. False if IMAP settings were not found')
|
|
1604
|
+
.label('ResolvedServerSettings'),
|
|
1605
|
+
smtp: Joi.object({
|
|
1606
|
+
auth: Joi.object({
|
|
1607
|
+
user: Joi.string().max(256).example('myuser@gmail.com').description('Account username')
|
|
1608
|
+
}).label('DetectedAuthenticationInfo'),
|
|
1609
|
+
|
|
1610
|
+
host: Joi.string().hostname().example('smtp.gmail.com').description('Hostname to connect to'),
|
|
1611
|
+
port: Joi.number()
|
|
1612
|
+
.integer()
|
|
1613
|
+
.min(1)
|
|
1614
|
+
.max(64 * 1024)
|
|
1615
|
+
.example(465)
|
|
1616
|
+
.description('Service port number'),
|
|
1617
|
+
secure: Joi.boolean().default(false).example(true).description('Should connection use TLS. Usually true for port 465')
|
|
1618
|
+
})
|
|
1619
|
+
.allow(false)
|
|
1620
|
+
.description('Discovered SMTP settings. False if SMTP settings were not found')
|
|
1621
|
+
.label('DiscoveredServerSettings'),
|
|
1622
|
+
appPassword: Joi.object({
|
|
1623
|
+
required: Joi.boolean().example(true).description('Whether the provider requires an app password'),
|
|
1624
|
+
provider: Joi.string().example('Gmail').description('Provider name'),
|
|
1625
|
+
instructions: Joi.string()
|
|
1626
|
+
.example('Use an app password instead of the regular account password')
|
|
1627
|
+
.description('Instructions for setting up an app password')
|
|
1628
|
+
})
|
|
1629
|
+
.unknown()
|
|
1630
|
+
.description('App password requirements for the provider, if known')
|
|
1631
|
+
.label('DiscoveredAppPasswordInfo'),
|
|
1632
|
+
_source: Joi.string().example('srv').description('Source for the detected info')
|
|
1633
|
+
}).label('DiscoveredEmailSettings'),
|
|
1634
|
+
failAction: 'log'
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
});
|
|
1060
1638
|
}
|
|
1061
1639
|
|
|
1062
1640
|
module.exports = init;
|