emailengine-app 2.69.0 → 2.71.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 (97) hide show
  1. package/.github/workflows/deploy.yml +6 -3
  2. package/.github/workflows/release.yaml +2 -0
  3. package/.github/workflows/test.yml +73 -12
  4. package/.ncurc.js +3 -3
  5. package/CHANGELOG.md +37 -0
  6. package/Gruntfile.js +21 -23
  7. package/bin/emailengine.js +8 -1
  8. package/config/default.toml +5 -0
  9. package/config/test.toml +5 -0
  10. package/data/google-crawlers.json +1 -1
  11. package/getswagger.sh +44 -4
  12. package/gettext-extract.js +163 -0
  13. package/lib/account.js +104 -72
  14. package/lib/api-routes/account-routes.js +231 -71
  15. package/lib/api-routes/blocklist-routes.js +25 -18
  16. package/lib/api-routes/chat-routes.js +32 -14
  17. package/lib/api-routes/delivery-test-routes.js +30 -5
  18. package/lib/api-routes/export-routes.js +27 -2
  19. package/lib/api-routes/gateway-routes.js +63 -12
  20. package/lib/api-routes/license-routes.js +18 -4
  21. package/lib/api-routes/mailbox-routes.js +33 -7
  22. package/lib/api-routes/message-routes.js +291 -145
  23. package/lib/api-routes/oauth2-app-routes.js +90 -24
  24. package/lib/api-routes/outbox-routes.js +16 -4
  25. package/lib/api-routes/pubsub-routes.js +8 -4
  26. package/lib/api-routes/route-helpers.js +14 -1
  27. package/lib/api-routes/settings-routes.js +51 -25
  28. package/lib/api-routes/stats-routes.js +37 -3
  29. package/lib/api-routes/submit-routes.js +31 -42
  30. package/lib/api-routes/template-routes.js +54 -21
  31. package/lib/api-routes/token-routes.js +67 -67
  32. package/lib/api-routes/webhook-route-routes.js +37 -8
  33. package/lib/autodetect-imap-settings.js +0 -2
  34. package/lib/consts.js +5 -0
  35. package/lib/document-store.js +22 -1
  36. package/lib/email-client/base-client.js +31 -8
  37. package/lib/email-client/gmail-client.js +119 -112
  38. package/lib/email-client/imap/mailbox.js +2 -2
  39. package/lib/email-client/imap/subconnection.js +0 -1
  40. package/lib/email-client/imap/sync-operations.js +1 -1
  41. package/lib/email-client/imap-client.js +36 -17
  42. package/lib/email-client/notification-handler.js +3 -6
  43. package/lib/email-client/outlook-client.js +49 -62
  44. package/lib/export.js +49 -1
  45. package/lib/feature-flags.js +8 -2
  46. package/lib/gateway.js +4 -9
  47. package/lib/get-raw-email.js +5 -5
  48. package/lib/imapproxy/imap-core/lib/imap-connection.js +0 -1
  49. package/lib/license-beacon.js +367 -0
  50. package/lib/logger.js +35 -22
  51. package/lib/metrics-collector.js +0 -2
  52. package/lib/oauth2-apps.js +13 -4
  53. package/lib/outbox.js +24 -40
  54. package/lib/redis-operations.js +1 -1
  55. package/lib/routes-ui.js +2 -1
  56. package/lib/schemas.js +403 -83
  57. package/lib/sentry.js +139 -0
  58. package/lib/settings.js +9 -3
  59. package/lib/stream-encrypt.js +1 -1
  60. package/lib/templates.js +1 -1
  61. package/lib/tokens.js +5 -3
  62. package/lib/tools.js +28 -6
  63. package/lib/ui-routes/account-routes.js +7 -4
  64. package/lib/ui-routes/admin-config-routes.js +20 -6
  65. package/lib/ui-routes/document-store-routes.js +7 -1
  66. package/lib/ui-routes/oauth-config-routes.js +0 -2
  67. package/lib/ui-routes/route-helpers.js +0 -2
  68. package/lib/ui-routes/unsubscribe-routes.js +0 -2
  69. package/lib/webhooks.js +8 -4
  70. package/package.json +23 -19
  71. package/sbom.json +1 -1
  72. package/server.js +38 -31
  73. package/static/licenses.html +171 -391
  74. package/translations/de.mo +0 -0
  75. package/translations/de.po +154 -142
  76. package/translations/et.mo +0 -0
  77. package/translations/et.po +129 -131
  78. package/translations/fr.mo +0 -0
  79. package/translations/fr.po +133 -136
  80. package/translations/ja.mo +0 -0
  81. package/translations/ja.po +126 -129
  82. package/translations/messages.pot +107 -107
  83. package/translations/nl.mo +0 -0
  84. package/translations/nl.po +128 -130
  85. package/translations/pl.mo +0 -0
  86. package/translations/pl.po +125 -128
  87. package/update-info.sh +19 -1
  88. package/views/config/logging.hbs +48 -0
  89. package/views/dashboard.hbs +22 -0
  90. package/workers/api.js +33 -37
  91. package/workers/documents.js +2 -22
  92. package/workers/export.js +73 -92
  93. package/workers/imap-proxy.js +3 -23
  94. package/workers/imap.js +2 -22
  95. package/workers/smtp.js +2 -22
  96. package/workers/submit.js +6 -24
  97. package/workers/webhooks.js +2 -22
@@ -4,7 +4,7 @@ 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
8
  const settings = require('../settings');
9
9
  const { autodetectImapSettings } = require('../autodetect-imap-settings');
10
10
  const Boom = require('@hapi/boom');
@@ -26,11 +26,42 @@ const {
26
26
  oauth2Schema,
27
27
  oauth2UpdateSchema,
28
28
  defaultAccountTypeSchema,
29
- shortMailboxesSchema
29
+ shortMailboxesSchema,
30
+ ACCOUNT_DISPLAY_STATES,
31
+ errorResponses
30
32
  } = require('../schemas');
31
33
 
32
34
  const { REDIS_PREFIX, MAX_FORM_TTL, NONCE_BYTES } = require('../consts');
33
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
+
34
65
  /**
35
66
  * Validates that delegation fields are only used with OAuth2 accounts.
36
67
  */
@@ -149,7 +180,11 @@ async function init(args) {
149
180
  notes: 'Registers new IMAP account to be synced',
150
181
  tags: ['api', 'Account'],
151
182
 
152
- plugins: {},
183
+ plugins: {
184
+ 'hapi-swagger': {
185
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
186
+ }
187
+ },
153
188
 
154
189
  auth: {
155
190
  strategy: 'api-token',
@@ -247,13 +282,19 @@ async function init(args) {
247
282
 
248
283
  response: {
249
284
  schema: Joi.object({
250
- account: accountIdSchema.required(),
285
+ account: accountIdSchema,
251
286
  state: Joi.string()
252
- .required()
253
287
  .valid('existing', 'new')
254
288
  .example('new')
255
- .description('Is the account new or updated existing')
256
- .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')
257
298
  }).label('CreateAccountResponse'),
258
299
  failAction: 'log'
259
300
  }
@@ -288,7 +329,11 @@ async function init(args) {
288
329
  notes: 'Updates account information',
289
330
  tags: ['api', 'Account'],
290
331
 
291
- plugins: {},
332
+ plugins: {
333
+ 'hapi-swagger': {
334
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
335
+ }
336
+ },
292
337
 
293
338
  auth: {
294
339
  strategy: 'api-token',
@@ -396,7 +441,11 @@ async function init(args) {
396
441
  notes: 'Requests connection to be reconnected',
397
442
  tags: ['api', 'Account'],
398
443
 
399
- plugins: {},
444
+ plugins: {
445
+ 'hapi-swagger': {
446
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
447
+ }
448
+ },
400
449
 
401
450
  auth: {
402
451
  strategy: 'api-token',
@@ -455,7 +504,11 @@ async function init(args) {
455
504
  notes: 'Immediately trigger account syncing for IMAP accounts',
456
505
  tags: ['api', 'Account'],
457
506
 
458
- plugins: {},
507
+ plugins: {
508
+ 'hapi-swagger': {
509
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
510
+ }
511
+ },
459
512
 
460
513
  auth: {
461
514
  strategy: 'api-token',
@@ -516,7 +569,11 @@ async function init(args) {
516
569
 
517
570
  tags: ['api', 'Account'],
518
571
 
519
- plugins: {},
572
+ plugins: {
573
+ 'hapi-swagger': {
574
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
575
+ }
576
+ },
520
577
 
521
578
  auth: {
522
579
  strategy: 'api-token',
@@ -583,7 +640,11 @@ async function init(args) {
583
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.',
584
641
  tags: ['api', 'Account'],
585
642
 
586
- plugins: {},
643
+ plugins: {
644
+ 'hapi-swagger': {
645
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
646
+ }
647
+ },
587
648
 
588
649
  auth: {
589
650
  strategy: 'api-token',
@@ -646,7 +707,11 @@ async function init(args) {
646
707
  notes: 'Lists registered accounts',
647
708
  tags: ['api', 'Account'],
648
709
 
649
- plugins: {},
710
+ plugins: {
711
+ 'hapi-swagger': {
712
+ responses: errorResponses(400, 401, 403, 429, 500)
713
+ }
714
+ },
650
715
 
651
716
  auth: {
652
717
  strategy: 'api-token',
@@ -673,7 +738,7 @@ async function init(args) {
673
738
  .label('PageNumber'),
674
739
  pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize'),
675
740
  state: Joi.string()
676
- .valid('init', 'syncing', 'connecting', 'connected', 'authenticationError', 'connectError', 'unset', 'disconnected')
741
+ .valid(...ACCOUNT_DISPLAY_STATES)
677
742
  .example('connected')
678
743
  .description('Filter accounts by state')
679
744
  .label('AccountState'),
@@ -687,17 +752,27 @@ async function init(args) {
687
752
  page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
688
753
  pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
689
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
+
690
765
  accounts: Joi.array()
691
766
  .items(
692
767
  Joi.object({
693
768
  account: accountIdSchema.required(),
694
- 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'),
695
770
  email: Joi.string().empty('').email().example('user@example.com').description('Default email address of the account'),
696
771
  type: AccountTypeSchema,
697
772
  app: Joi.string().max(256).example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
698
773
  state: Joi.string()
699
774
  .required()
700
- .valid('init', 'syncing', 'connecting', 'connected', 'authenticationError', 'connectError', 'unset', 'disconnected')
775
+ .valid(...ACCOUNT_DISPLAY_STATES)
701
776
  .example('connected')
702
777
  .description('Account state')
703
778
  .label('AccountListState'),
@@ -713,8 +788,11 @@ async function init(args) {
713
788
 
714
789
  counters: accountCountersSchema,
715
790
 
716
- syncTime: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('Last sync time'),
717
- 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')
718
796
  }).label('AccountResponseItem')
719
797
  )
720
798
  .label('AccountEntries')
@@ -817,16 +895,15 @@ async function init(args) {
817
895
  oauth2App = await oauth2Apps.get(accountData.oauth2.provider);
818
896
 
819
897
  if (oauth2App) {
898
+ result.type = oauth2App.provider;
820
899
  // Check if account is already marked as send-only
821
900
  if (accountData.sendOnly) {
822
901
  result.sendOnly = true;
823
- } else {
824
- result.type = oauth2App.provider;
825
902
  }
826
903
  if (oauth2App.id !== oauth2App.provider) {
827
904
  result.app = oauth2App.id;
828
905
  }
829
- result.baseScopes = oauth2App.baseScope || 'imap';
906
+ result.baseScopes = oauth2App.baseScopes || 'imap';
830
907
  } else {
831
908
  result.type = 'oauth2';
832
909
  }
@@ -869,6 +946,12 @@ async function init(args) {
869
946
  notes: 'Returns stored information about the account. Passwords are not included.',
870
947
  tags: ['api', 'Account'],
871
948
 
949
+ plugins: {
950
+ 'hapi-swagger': {
951
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
952
+ }
953
+ },
954
+
872
955
  auth: {
873
956
  strategy: 'api-token',
874
957
  mode: 'required'
@@ -901,7 +984,7 @@ async function init(args) {
901
984
  schema: Joi.object({
902
985
  account: accountIdSchema.required(),
903
986
 
904
- 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'),
905
988
  email: Joi.string().empty('').email().example('user@example.com').description('Default email address of the account'),
906
989
 
907
990
  copy: Joi.boolean().example(true).description('Copy submitted messages to Sent folder'),
@@ -917,6 +1000,7 @@ async function init(args) {
917
1000
  subconnections: accountSchemas.subconnections,
918
1001
 
919
1002
  webhooks: Joi.string()
1003
+ .allow('')
920
1004
  .uri({
921
1005
  scheme: ['http', 'https'],
922
1006
  allowRelative: false
@@ -930,10 +1014,22 @@ async function init(args) {
930
1014
 
931
1015
  smtp: Joi.object(smtpSchema).description('SMTP configuration').label('SMTPResponse'),
932
1016
 
933
- oauth2: Joi.object(oauth2Schema).description('OAuth2 configuration').label('Oauth2Response'),
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'),
934
1030
 
935
1031
  state: Joi.string()
936
- .valid('init', 'syncing', 'connecting', 'connected', 'authenticationError', 'connectError', 'unset', 'disconnected')
1032
+ .valid(...ACCOUNT_DISPLAY_STATES)
937
1033
  .example('connected')
938
1034
  .description('Informational account state')
939
1035
  .label('AccountInfoState'),
@@ -949,11 +1045,30 @@ async function init(args) {
949
1045
  description: Joi.string().example('Authentication failed').description('Error information'),
950
1046
  responseCode: Joi.number().integer().example(500).description('Error status code'),
951
1047
  code: Joi.string().example('EAUTH').description('Error type identifier'),
952
- 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')
953
1054
  })
954
- .description('Information about the last SMTP connection attempt')
1055
+ .allow(null)
1056
+ .description('Information about the last SMTP connection attempt. Null when no SMTP connection has been attempted')
955
1057
  .label('SMTPInfoStatus'),
956
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
+
957
1072
  webhooksCustomHeaders: settingsSchema.webhooksCustomHeaders.label('AccountWebhooksCustomHeaders'),
958
1073
 
959
1074
  locale: Joi.string().empty('').max(100).example('fr').description('Optional locale'),
@@ -982,17 +1097,23 @@ async function init(args) {
982
1097
  'Account quota information if query argument quota=true. This value will be false if the server does not provide quota information.'
983
1098
  ),
984
1099
 
985
- 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)'),
986
1101
 
987
1102
  outlookSubscription: Joi.object({
988
1103
  id: Joi.string().description('Microsoft Graph subscription ID'),
989
- expirationDateTime: Joi.date().iso().description('When the subscription expires'),
1104
+ expirationDateTime: Joi.date().iso().allow(null).description('When the subscription expires'),
990
1105
  state: Joi.object({
991
- state: Joi.string().valid('creating', 'created', 'error').description('Subscription state'),
1106
+ state: Joi.string().valid('creating', 'created', 'renewing', 'error').description('Subscription state'),
992
1107
  time: Joi.number().description('Timestamp of last state change'),
993
- error: Joi.string().description('Error message if state is error')
994
- }).description('Current subscription state')
995
- }).description('Microsoft Graph subscription details (Outlook accounts only)'),
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)'),
996
1117
 
997
1118
  lastError: lastErrorSchema.allow(null)
998
1119
  }).label('AccountResponse'),
@@ -1024,6 +1145,19 @@ async function init(args) {
1024
1145
  try {
1025
1146
  const tokenData = await accountObject.getActiveAccessTokenData();
1026
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
+
1027
1161
  // Record metric if token was actually refreshed (not cached)
1028
1162
  if (!tokenData.cached) {
1029
1163
  const provider = tokenData.provider || 'unknown';
@@ -1045,7 +1179,11 @@ async function init(args) {
1045
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.',
1046
1180
  tags: ['api', 'Account'],
1047
1181
 
1048
- plugins: {},
1182
+ plugins: {
1183
+ 'hapi-swagger': {
1184
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
1185
+ }
1186
+ },
1049
1187
 
1050
1188
  auth: {
1051
1189
  strategy: 'api-token',
@@ -1069,8 +1207,16 @@ async function init(args) {
1069
1207
  schema: Joi.object({
1070
1208
  account: accountIdSchema.required(),
1071
1209
  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
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')
1074
1220
  }).label('AccountTokenResponse'),
1075
1221
  failAction: 'log'
1076
1222
  }
@@ -1101,7 +1247,11 @@ async function init(args) {
1101
1247
  notes: 'Returns signatures associated with the account. Currently only Gmail is supported, and only "new message" signatures from the "sendAs" list are returned.',
1102
1248
  tags: ['api', 'Account'],
1103
1249
 
1104
- plugins: {},
1250
+ plugins: {
1251
+ 'hapi-swagger': {
1252
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
1253
+ }
1254
+ },
1105
1255
 
1106
1256
  auth: {
1107
1257
  strategy: 'api-token',
@@ -1130,7 +1280,10 @@ async function init(args) {
1130
1280
  signature: Joi.string().example('<div>Best regards,</div>').description('Signature HTML code').required()
1131
1281
  }).label('SignatureResponseItem')
1132
1282
  )
1133
- .label('SignatureEntries')
1283
+ .label('SignatureEntries'),
1284
+ signaturesSupported: Joi.boolean()
1285
+ .example(true)
1286
+ .description('Whether the account type supports listing signatures (currently Gmail API accounts only)')
1134
1287
  }).label('AccountSignaturesResponse'),
1135
1288
  failAction: 'log'
1136
1289
  }
@@ -1205,7 +1358,11 @@ async function init(args) {
1205
1358
  notes: 'Generates a redirect link to the hosted authentication form',
1206
1359
  tags: ['api', 'Account'],
1207
1360
 
1208
- plugins: {},
1361
+ plugins: {
1362
+ 'hapi-swagger': {
1363
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
1364
+ }
1365
+ },
1209
1366
 
1210
1367
  auth: {
1211
1368
  strategy: 'api-token',
@@ -1299,7 +1456,8 @@ async function init(args) {
1299
1456
 
1300
1457
  plugins: {
1301
1458
  'hapi-swagger': {
1302
- produces: ['text/plain']
1459
+ produces: ['text/plain'],
1460
+ responses: errorResponses(400, 401, 403, 429, 500)
1303
1461
  }
1304
1462
  },
1305
1463
 
@@ -1334,7 +1492,11 @@ async function init(args) {
1334
1492
  notes: 'Checks if can connect and authenticate using provided account info',
1335
1493
  tags: ['api', 'Account'],
1336
1494
 
1337
- plugins: {},
1495
+ plugins: {
1496
+ 'hapi-swagger': {
1497
+ responses: errorResponses(400, 401, 403, 429, 500)
1498
+ }
1499
+ },
1338
1500
 
1339
1501
  auth: {
1340
1502
  strategy: 'api-token',
@@ -1360,28 +1522,8 @@ async function init(args) {
1360
1522
  },
1361
1523
  response: {
1362
1524
  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'),
1525
+ imap: verifyResultSchema('Imap', 'NO [AUTHENTICATIONFAILED] Invalid credentials'),
1526
+ smtp: verifyResultSchema('Smtp', '535 Authentication failed'),
1385
1527
  mailboxes: shortMailboxesSchema
1386
1528
  }).label('VerifyAccountResponse'),
1387
1529
  failAction: 'log'
@@ -1411,7 +1553,11 @@ async function init(args) {
1411
1553
  notes: 'Try to discover IMAP and SMTP settings for an email account',
1412
1554
  tags: ['api', 'Settings'],
1413
1555
 
1414
- plugins: {},
1556
+ plugins: {
1557
+ 'hapi-swagger': {
1558
+ responses: errorResponses(400, 401, 403, 429, 500)
1559
+ }
1560
+ },
1415
1561
 
1416
1562
  auth: {
1417
1563
  strategy: 'api-token',
@@ -1444,31 +1590,45 @@ async function init(args) {
1444
1590
  user: Joi.string().max(256).example('myuser@gmail.com').description('Account username')
1445
1591
  }).label('DetectedAuthenticationInfo'),
1446
1592
 
1447
- host: Joi.string().hostname().required().example('imap.gmail.com').description('Hostname to connect to'),
1593
+ host: Joi.string().hostname().example('imap.gmail.com').description('Hostname to connect to'),
1448
1594
  port: Joi.number()
1449
1595
  .integer()
1450
1596
  .min(1)
1451
1597
  .max(64 * 1024)
1452
- .required()
1453
1598
  .example(993)
1454
1599
  .description('Service port number'),
1455
1600
  secure: Joi.boolean().default(false).example(true).description('Should connection use TLS. Usually true for port 993')
1456
- }).label('ResolvedServerSettings'),
1601
+ })
1602
+ .allow(false)
1603
+ .description('Discovered IMAP settings. False if IMAP settings were not found')
1604
+ .label('ResolvedServerSettings'),
1457
1605
  smtp: Joi.object({
1458
1606
  auth: Joi.object({
1459
1607
  user: Joi.string().max(256).example('myuser@gmail.com').description('Account username')
1460
1608
  }).label('DetectedAuthenticationInfo'),
1461
1609
 
1462
- host: Joi.string().hostname().required().example('imap.gmail.com').description('Hostname to connect to'),
1610
+ host: Joi.string().hostname().example('smtp.gmail.com').description('Hostname to connect to'),
1463
1611
  port: Joi.number()
1464
1612
  .integer()
1465
1613
  .min(1)
1466
1614
  .max(64 * 1024)
1467
- .required()
1468
- .example(993)
1615
+ .example(465)
1469
1616
  .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'),
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'),
1472
1632
  _source: Joi.string().example('srv').description('Source for the detected info')
1473
1633
  }).label('DiscoveredEmailSettings'),
1474
1634
  failAction: 'log'
@@ -8,7 +8,7 @@ const getSecret = require('../get-secret');
8
8
  const { lists } = require('../lists');
9
9
  const { failAction } = require('../tools');
10
10
  const { handleError } = require('./route-helpers');
11
- const { accountIdSchema } = require('../schemas');
11
+ const { accountIdSchema, errorResponses } = require('../schemas');
12
12
  const { REDIS_PREFIX } = require('../consts');
13
13
 
14
14
  async function init(args) {
@@ -31,7 +31,11 @@ async function init(args) {
31
31
  notes: 'List blocklists with blocked addresses',
32
32
  tags: ['api', 'Blocklists'],
33
33
 
34
- plugins: {},
34
+ plugins: {
35
+ 'hapi-swagger': {
36
+ responses: errorResponses(400, 401, 403, 429, 500)
37
+ }
38
+ },
35
39
 
36
40
  auth: {
37
41
  strategy: 'api-token',
@@ -97,7 +101,11 @@ async function init(args) {
97
101
  notes: 'List blocked addresses for a list',
98
102
  tags: ['api', 'Blocklists'],
99
103
 
100
- plugins: {},
104
+ plugins: {
105
+ 'hapi-swagger': {
106
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
107
+ }
108
+ },
101
109
 
102
110
  auth: {
103
111
  strategy: 'api-token',
@@ -145,10 +153,10 @@ async function init(args) {
145
153
  .items(
146
154
  Joi.object({
147
155
  recipient: Joi.string().email().example('user@example.com').description('Listed email address').required(),
148
- account: accountIdSchema.required().required(),
156
+ account: accountIdSchema.required(),
149
157
  messageId: Joi.string().example('<test123@example.com>').description('Message ID'),
150
158
  source: Joi.string().example('api').description('Which mechanism was used to add the entry'),
151
- reason: Joi.string().example('api').description('Why this entry was added'),
159
+ reason: Joi.string().example('block').description('Why this entry was added'),
152
160
  remoteAddress: Joi.string()
153
161
  .ip({
154
162
  version: ['ipv4', 'ipv6'],
@@ -203,18 +211,7 @@ async function init(args) {
203
211
  added: !!added
204
212
  };
205
213
  } catch (err) {
206
- request.logger.error({ msg: 'API request failed', err });
207
- if (Boom.isBoom(err)) {
208
- throw err;
209
- }
210
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
211
- if (err.code) {
212
- error.output.payload.code = err.code;
213
- }
214
- if (err.details) {
215
- error.output.payload.details = err.details;
216
- }
217
- throw error;
214
+ handleError(request, err);
218
215
  }
219
216
  },
220
217
  options: {
@@ -222,6 +219,12 @@ async function init(args) {
222
219
  notes: 'Add an email address to a blocklist',
223
220
  tags: ['api', 'Blocklists'],
224
221
 
222
+ plugins: {
223
+ 'hapi-swagger': {
224
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
225
+ }
226
+ },
227
+
225
228
  auth: {
226
229
  strategy: 'api-token',
227
230
  mode: 'required'
@@ -294,7 +297,11 @@ async function init(args) {
294
297
  notes: 'Delete a blocked email address from a list',
295
298
  tags: ['api', 'Blocklists'],
296
299
 
297
- plugins: {},
300
+ plugins: {
301
+ 'hapi-swagger': {
302
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
303
+ }
304
+ },
298
305
 
299
306
  auth: {
300
307
  strategy: 'api-token',