emailengine-app 2.61.1 → 2.61.3

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 (136) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/data/google-crawlers.json +1 -1
  3. package/lib/account/account-state.js +248 -0
  4. package/lib/account.js +45 -193
  5. package/lib/api-routes/account-routes.js +1023 -0
  6. package/lib/api-routes/message-routes.js +1377 -0
  7. package/lib/consts.js +12 -2
  8. package/lib/email-client/base-client.js +282 -771
  9. package/lib/email-client/gmail/gmail-api.js +243 -0
  10. package/lib/email-client/gmail-client.js +145 -53
  11. package/lib/email-client/imap/mailbox.js +24 -698
  12. package/lib/email-client/imap/sync-operations.js +812 -0
  13. package/lib/email-client/imap-client.js +1 -1
  14. package/lib/email-client/message-builder.js +566 -0
  15. package/lib/email-client/notification-handler.js +314 -0
  16. package/lib/email-client/outlook/graph-api.js +326 -0
  17. package/lib/email-client/outlook-client.js +159 -113
  18. package/lib/email-client/smtp-pool-manager.js +196 -0
  19. package/lib/imapproxy/imap-server.js +3 -12
  20. package/lib/oauth/gmail.js +4 -4
  21. package/lib/oauth/mail-ru.js +30 -5
  22. package/lib/oauth/outlook.js +57 -3
  23. package/lib/oauth/pubsub/google.js +30 -11
  24. package/lib/oauth/scope-checker.js +202 -0
  25. package/lib/oauth2-apps.js +8 -4
  26. package/lib/redis-operations.js +484 -0
  27. package/lib/routes-ui.js +283 -2582
  28. package/lib/tools.js +4 -196
  29. package/lib/ui-routes/account-routes.js +1931 -0
  30. package/lib/ui-routes/admin-config-routes.js +1233 -0
  31. package/lib/ui-routes/admin-entities-routes.js +2367 -0
  32. package/lib/ui-routes/oauth-routes.js +992 -0
  33. package/lib/utils/network.js +237 -0
  34. package/package.json +10 -10
  35. package/sbom.json +1 -1
  36. package/static/js/app.js +5 -5
  37. package/static/licenses.html +79 -19
  38. package/translations/de.mo +0 -0
  39. package/translations/de.po +97 -86
  40. package/translations/en.mo +0 -0
  41. package/translations/en.po +80 -75
  42. package/translations/et.mo +0 -0
  43. package/translations/et.po +96 -86
  44. package/translations/fr.mo +0 -0
  45. package/translations/fr.po +97 -86
  46. package/translations/ja.mo +0 -0
  47. package/translations/ja.po +96 -86
  48. package/translations/messages.pot +105 -91
  49. package/translations/nl.mo +0 -0
  50. package/translations/nl.po +98 -86
  51. package/translations/pl.mo +0 -0
  52. package/translations/pl.po +96 -86
  53. package/views/account/security.hbs +4 -4
  54. package/views/accounts/account.hbs +13 -13
  55. package/views/accounts/register/imap-server.hbs +12 -12
  56. package/views/config/document-store/pre-processing/index.hbs +4 -2
  57. package/views/config/oauth/app.hbs +6 -7
  58. package/views/config/oauth/index.hbs +2 -2
  59. package/views/config/service.hbs +3 -4
  60. package/views/dashboard.hbs +5 -7
  61. package/views/error.hbs +22 -7
  62. package/views/gateways/gateway.hbs +2 -2
  63. package/views/partials/add_account_modal.hbs +7 -10
  64. package/views/partials/document_store_header.hbs +1 -1
  65. package/views/partials/editor_scope_info.hbs +0 -1
  66. package/views/partials/oauth_config_header.hbs +1 -1
  67. package/views/partials/side_menu.hbs +3 -3
  68. package/views/partials/webhook_form.hbs +2 -2
  69. package/views/templates/index.hbs +1 -1
  70. package/views/templates/template.hbs +8 -8
  71. package/views/tokens/index.hbs +6 -6
  72. package/views/tokens/new.hbs +1 -1
  73. package/views/webhooks/index.hbs +4 -4
  74. package/views/webhooks/webhook.hbs +7 -7
  75. package/workers/api.js +148 -2436
  76. package/workers/smtp.js +2 -1
  77. package/lib/imapproxy/imap-core/test/client.js +0 -46
  78. package/lib/imapproxy/imap-core/test/fixtures/append.eml +0 -1196
  79. package/lib/imapproxy/imap-core/test/fixtures/chunks.js +0 -44
  80. package/lib/imapproxy/imap-core/test/fixtures/fix1.eml +0 -6
  81. package/lib/imapproxy/imap-core/test/fixtures/fix2.eml +0 -599
  82. package/lib/imapproxy/imap-core/test/fixtures/fix3.eml +0 -32
  83. package/lib/imapproxy/imap-core/test/fixtures/fix4.eml +0 -6
  84. package/lib/imapproxy/imap-core/test/fixtures/mimetorture.eml +0 -599
  85. package/lib/imapproxy/imap-core/test/fixtures/mimetorture.js +0 -2740
  86. package/lib/imapproxy/imap-core/test/fixtures/mimetorture.json +0 -1411
  87. package/lib/imapproxy/imap-core/test/fixtures/mimetree.js +0 -85
  88. package/lib/imapproxy/imap-core/test/fixtures/nodemailer.eml +0 -582
  89. package/lib/imapproxy/imap-core/test/fixtures/ryan_finnie_mime_torture.eml +0 -599
  90. package/lib/imapproxy/imap-core/test/fixtures/simple.eml +0 -42
  91. package/lib/imapproxy/imap-core/test/fixtures/simple.json +0 -164
  92. package/lib/imapproxy/imap-core/test/imap-compile-stream-test.js +0 -671
  93. package/lib/imapproxy/imap-core/test/imap-compiler-test.js +0 -272
  94. package/lib/imapproxy/imap-core/test/imap-indexer-test.js +0 -236
  95. package/lib/imapproxy/imap-core/test/imap-parser-test.js +0 -922
  96. package/lib/imapproxy/imap-core/test/memory-notifier.js +0 -129
  97. package/lib/imapproxy/imap-core/test/prepare.sh +0 -74
  98. package/lib/imapproxy/imap-core/test/protocol-test.js +0 -1756
  99. package/lib/imapproxy/imap-core/test/search-test.js +0 -1356
  100. package/lib/imapproxy/imap-core/test/test-client.js +0 -152
  101. package/lib/imapproxy/imap-core/test/test-server.js +0 -623
  102. package/lib/imapproxy/imap-core/test/tools-test.js +0 -22
  103. package/test/api-test.js +0 -899
  104. package/test/autoreply-test.js +0 -327
  105. package/test/bounce-test.js +0 -151
  106. package/test/complaint-test.js +0 -256
  107. package/test/fixtures/autoreply/LICENSE +0 -27
  108. package/test/fixtures/autoreply/rfc3834-01.eml +0 -23
  109. package/test/fixtures/autoreply/rfc3834-02.eml +0 -24
  110. package/test/fixtures/autoreply/rfc3834-03.eml +0 -26
  111. package/test/fixtures/autoreply/rfc3834-04.eml +0 -48
  112. package/test/fixtures/autoreply/rfc3834-05.eml +0 -19
  113. package/test/fixtures/autoreply/rfc3834-06.eml +0 -59
  114. package/test/fixtures/bounces/163.eml +0 -2521
  115. package/test/fixtures/bounces/fastmail.eml +0 -242
  116. package/test/fixtures/bounces/gmail.eml +0 -252
  117. package/test/fixtures/bounces/hotmail.eml +0 -655
  118. package/test/fixtures/bounces/mailru.eml +0 -121
  119. package/test/fixtures/bounces/outlook.eml +0 -1107
  120. package/test/fixtures/bounces/postfix.eml +0 -101
  121. package/test/fixtures/bounces/rambler.eml +0 -116
  122. package/test/fixtures/bounces/workmail.eml +0 -142
  123. package/test/fixtures/bounces/yahoo.eml +0 -139
  124. package/test/fixtures/bounces/zoho.eml +0 -83
  125. package/test/fixtures/bounces/zonemta.eml +0 -100
  126. package/test/fixtures/complaints/LICENSE +0 -27
  127. package/test/fixtures/complaints/amazonses.eml +0 -72
  128. package/test/fixtures/complaints/dmarc.eml +0 -59
  129. package/test/fixtures/complaints/hotmail.eml +0 -49
  130. package/test/fixtures/complaints/optout.eml +0 -40
  131. package/test/fixtures/complaints/standard-arf.eml +0 -68
  132. package/test/fixtures/complaints/yahoo.eml +0 -68
  133. package/test/oauth2-apps-test.js +0 -301
  134. package/test/sendonly-test.js +0 -160
  135. package/test/test-config.js +0 -34
  136. package/test/webhooks-server.js +0 -39
@@ -0,0 +1,1931 @@
1
+ 'use strict';
2
+
3
+ const Boom = require('@hapi/boom');
4
+ const Joi = require('joi');
5
+ const crypto = require('crypto');
6
+ const util = require('util');
7
+ const psl = require('psl');
8
+
9
+ const settings = require('../settings');
10
+ const tokens = require('../tokens');
11
+ const { redis, documentsQueue } = require('../db');
12
+ const {
13
+ failAction,
14
+ verifyAccountInfo,
15
+ getLogs,
16
+ flattenObjectKeys,
17
+ getSignedFormData,
18
+ getServiceHostname,
19
+ parseSignedFormData,
20
+ getBoolean,
21
+ readEnvValue
22
+ } = require('../tools');
23
+ const { Account } = require('../account');
24
+ const { Gateway } = require('../gateway');
25
+ const { oauth2Apps, oauth2ProviderData } = require('../oauth2-apps');
26
+ const { autodetectImapSettings } = require('../autodetect-imap-settings');
27
+ const getSecret = require('../get-secret');
28
+ const capa = require('../capa');
29
+ const consts = require('../consts');
30
+ const { settingsSchema, accountIdSchema, defaultAccountTypeSchema } = require('../schemas');
31
+ const fs = require('fs');
32
+ const pathlib = require('path');
33
+
34
+ const { DEFAULT_MAX_LOG_LINES, DEFAULT_PAGE_SIZE, REDIS_PREFIX, MAX_FORM_TTL, NONCE_BYTES } = consts;
35
+
36
+ const DISABLE_MESSAGE_BROWSER = getBoolean(readEnvValue('EENGINE_DISABLE_MESSAGE_BROWSER'));
37
+
38
+ const cachedTemplates = {
39
+ testSend: fs.readFileSync(pathlib.join(__dirname, '..', '..', 'views', 'partials', 'test_send.hbs'), 'utf-8')
40
+ };
41
+
42
+ function formatAccountData(account, gt) {
43
+ account.type = {};
44
+
45
+ if (account.oauth2 && account.oauth2.app) {
46
+ let providerData = oauth2ProviderData(account.oauth2.app.provider);
47
+ account.type = providerData;
48
+ } else if (account.oauth2 && account.oauth2.provider) {
49
+ account.type = oauth2ProviderData(account.oauth2.provider);
50
+ } else if (account.imap && !account.imap.disabled) {
51
+ account.type.icon = 'fa fa-envelope-square';
52
+ account.type.name = 'IMAP';
53
+ account.type.comment = psl.get(account.imap.host) || account.imap.host;
54
+ } else if (account.smtp) {
55
+ account.type.icon = 'fa fa-paper-plane';
56
+ account.type.name = 'SMTP';
57
+ account.type.comment = psl.get(account.smtp.host) || account.smtp.host;
58
+ } else if (account.oauth2 && account.oauth2.auth && account.oauth2.auth.delegatedAccount) {
59
+ account.type.icon = 'fa fa-arrow-alt-circle-right';
60
+ account.type.name = gt.gettext('Delegated');
61
+ account.type.comment = util.format(gt.gettext('Using credentials from "%s"'), account.oauth2.auth.delegatedAccount);
62
+ } else {
63
+ account.type.name = 'N/A';
64
+ }
65
+
66
+ switch (account.state) {
67
+ case 'init':
68
+ account.stateLabel = {
69
+ type: 'info',
70
+ name: 'Initializing',
71
+ spinner: true
72
+ };
73
+ break;
74
+
75
+ case 'connecting':
76
+ account.stateLabel = {
77
+ type: 'info',
78
+ name: 'Connecting'
79
+ };
80
+ break;
81
+
82
+ case 'syncing':
83
+ account.stateLabel = {
84
+ type: 'info',
85
+ name: 'Syncing',
86
+ spinner: true
87
+ };
88
+ break;
89
+
90
+ case 'connected':
91
+ account.stateLabel = {
92
+ type: 'success',
93
+ name: 'Connected'
94
+ };
95
+ break;
96
+
97
+ case 'disabled':
98
+ account.stateLabel = {
99
+ type: 'secondary',
100
+ name: 'Disabled',
101
+ error: account.disabledReason
102
+ };
103
+ break;
104
+
105
+ case 'authenticationError':
106
+ case 'connectError': {
107
+ let errorMessage = account.lastErrorState ? account.lastErrorState.response : false;
108
+ if (account.lastErrorState) {
109
+ switch (account.lastErrorState.serverResponseCode) {
110
+ case 'ETIMEDOUT':
111
+ errorMessage = gt.gettext('Connection timed out. This usually occurs if you are behind a firewall or connecting to the wrong port.');
112
+ break;
113
+ case 'ClosedAfterConnectTLS':
114
+ errorMessage = gt.gettext('The server unexpectedly closed the connection.');
115
+ break;
116
+ case 'ClosedAfterConnectText':
117
+ errorMessage = gt.gettext(
118
+ 'The server unexpectedly closed the connection. This usually happens when attempting to connect to a TLS port without TLS enabled.'
119
+ );
120
+ break;
121
+ case 'ECONNREFUSED':
122
+ errorMessage = gt.gettext(
123
+ 'The server refused the connection. This typically occurs if the server is not running, is overloaded, or you are connecting to the wrong host or port.'
124
+ );
125
+ break;
126
+ }
127
+ }
128
+
129
+ account.stateLabel = {
130
+ type: 'danger',
131
+ name: 'Failed',
132
+ error: errorMessage
133
+ };
134
+ break;
135
+ }
136
+ case 'unset':
137
+ account.stateLabel = {
138
+ type: 'light',
139
+ name: 'Not syncing'
140
+ };
141
+ break;
142
+ case 'disconnected':
143
+ account.stateLabel = {
144
+ type: 'warning',
145
+ name: 'Disconnected'
146
+ };
147
+ break;
148
+ case 'paused':
149
+ account.stateLabel = {
150
+ type: 'secondary',
151
+ name: 'Paused'
152
+ };
153
+ break;
154
+ default:
155
+ account.stateLabel = {
156
+ type: 'secondary',
157
+ name: 'N/A'
158
+ };
159
+ break;
160
+ }
161
+
162
+ if (account.oauth2) {
163
+ account.oauth2.scopes = []
164
+ .concat(account.oauth2.scope || [])
165
+ .concat(account.oauth2.scopes || [])
166
+ .flatMap(entry => entry.split(/\s+/))
167
+ .map(entry => entry.trim())
168
+ .filter(entry => entry);
169
+
170
+ account.oauth2.expiresStr = account.oauth2.expires ? account.oauth2.expires.toISOString() : false;
171
+ account.oauth2.generatedStr = account.oauth2.generated ? account.oauth2.generated.toISOString() : false;
172
+
173
+ if (account.outlookSubscription) {
174
+ account.outlookSubscription.subscriptionExpiresStr = account.outlookSubscription.expirationDateTime
175
+ ? account.outlookSubscription.expirationDateTime.toISOString()
176
+ : false;
177
+
178
+ let state = account.outlookSubscription.state || {};
179
+
180
+ account.outlookSubscription.isValid =
181
+ state.state !== 'error' && account.outlookSubscription.expirationDateTime && account.outlookSubscription.expirationDateTime > new Date();
182
+
183
+ account.outlookSubscription.stateLabel = (state.state || '').replace(/^./, c => c.toUpperCase());
184
+
185
+ if ((state.state === 'created' && !account.outlookSubscription.expirationDateTime) || account.outlookSubscription.expirationDateTime < new Date()) {
186
+ account.outlookSubscription.stateLabel = 'Expired';
187
+ }
188
+ }
189
+ }
190
+
191
+ return account;
192
+ }
193
+
194
+ async function getMailboxListing(accountObject) {
195
+ let mailboxes = [
196
+ {
197
+ path: 'INBOX',
198
+ listed: true,
199
+ specialUse: '\\Inbox',
200
+ name: 'INBOX',
201
+ subscribed: true
202
+ }
203
+ ];
204
+
205
+ try {
206
+ mailboxes = await accountObject.getMailboxListing();
207
+ mailboxes = mailboxes.sort((a, b) => {
208
+ if (a.path === 'INBOX') {
209
+ return -1;
210
+ } else if (b.path === 'INBOX') {
211
+ return 1;
212
+ }
213
+
214
+ if (a.specialUse && !b.specialUse) {
215
+ return -1;
216
+ } else if (!a.specialUse && b.specialUse) {
217
+ return 1;
218
+ }
219
+
220
+ return a.path.toLowerCase().localeCompare(b.path.toLowerCase());
221
+ });
222
+ } catch (err) {
223
+ // ignore
224
+ }
225
+
226
+ return mailboxes;
227
+ }
228
+
229
+ function init(args) {
230
+ const { server, call } = args;
231
+
232
+ // Account listing route
233
+ server.route({
234
+ method: 'GET',
235
+ path: '/admin/accounts',
236
+ async handler(request, h) {
237
+ let accountObject = new Account({ redis, call });
238
+
239
+ const runIndex = await call({
240
+ cmd: 'runIndex'
241
+ });
242
+
243
+ const accounts = await accountObject.listAccounts(request.query.state, request.query.query, request.query.page - 1, request.query.pageSize);
244
+
245
+ if (accounts.pages < request.query.page) {
246
+ request.query.page = accounts.pages;
247
+ }
248
+
249
+ for (let account of accounts.accounts) {
250
+ let accountObj = new Account({ redis, account: account.account });
251
+ account.data = await accountObj.loadAccountData(null, null, runIndex);
252
+
253
+ if (account.data && account.data.oauth2 && account.data.oauth2.provider) {
254
+ let oauth2App = await oauth2Apps.get(account.data.oauth2.provider);
255
+ if (oauth2App) {
256
+ account.data.oauth2.app = oauth2App;
257
+ }
258
+ }
259
+ }
260
+
261
+ let nextPage = false;
262
+ let prevPage = false;
263
+
264
+ let getPagingUrl = (page, state, query) => {
265
+ let url = new URL(`admin/accounts`, 'http://localhost');
266
+
267
+ if (page) {
268
+ url.searchParams.append('page', page);
269
+ }
270
+
271
+ if (request.query.pageSize && request.query.pageSize !== DEFAULT_PAGE_SIZE) {
272
+ url.searchParams.append('pageSize', request.query.pageSize);
273
+ }
274
+
275
+ if (query) {
276
+ url.searchParams.append('query', query);
277
+ }
278
+
279
+ if (state) {
280
+ url.searchParams.append('state', state);
281
+ }
282
+
283
+ return url.pathname + url.search;
284
+ };
285
+
286
+ if (accounts.pages > accounts.page + 1) {
287
+ nextPage = getPagingUrl(accounts.page + 2, request.query.state, request.query.query);
288
+ }
289
+
290
+ if (accounts.page > 0) {
291
+ prevPage = getPagingUrl(accounts.page, request.query.state, request.query.query);
292
+ }
293
+
294
+ let stateOptions = [
295
+ {
296
+ state: false,
297
+ label: 'All'
298
+ },
299
+
300
+ { divider: true },
301
+
302
+ {
303
+ state: 'init',
304
+ label: 'Initializing'
305
+ },
306
+
307
+ {
308
+ state: 'connecting',
309
+ label: 'Connecting'
310
+ },
311
+
312
+ {
313
+ state: 'syncing',
314
+ label: 'Syncing'
315
+ },
316
+
317
+ {
318
+ state: 'connected',
319
+ label: 'Connected'
320
+ },
321
+
322
+ {
323
+ state: 'disconnected',
324
+ label: 'Disconnected'
325
+ },
326
+
327
+ {
328
+ state: 'authenticationError',
329
+ label: 'Authentication failed'
330
+ },
331
+
332
+ {
333
+ state: 'connectError',
334
+ label: 'Connection failed'
335
+ },
336
+
337
+ {
338
+ state: 'unset',
339
+ label: 'Unset'
340
+ }
341
+ ].map(entry => {
342
+ let url = getPagingUrl(0, entry.state, request.query.query);
343
+ return Object.assign({ url, selected: entry.state ? entry.state === request.query.state : !request.query.state }, entry);
344
+ });
345
+
346
+ return h.view(
347
+ 'accounts/index',
348
+ {
349
+ pageTitle: 'Email Accounts',
350
+ menuAccounts: true,
351
+
352
+ query: request.query.query,
353
+ state: request.query.state,
354
+ pageSize: request.query.pageSize !== DEFAULT_PAGE_SIZE ? request.query.pageSize : false,
355
+
356
+ selectedState: stateOptions.find(entry => entry.state && entry.state === request.query.state),
357
+
358
+ searchTarget: '/admin/accounts',
359
+ searchPlaceholder: 'Search for accounts...',
360
+
361
+ showPaging: accounts.pages > 1,
362
+ nextPage,
363
+ prevPage,
364
+ firstPage: accounts.page === 0,
365
+ pageLinks: new Array(accounts.pages || 1).fill(0).map((z, i) => ({
366
+ url: getPagingUrl(i + 1, request.query.state, request.query.query),
367
+ title: i + 1,
368
+ active: i === accounts.page
369
+ })),
370
+
371
+ stateOptions,
372
+
373
+ accounts: accounts.accounts.map(account => formatAccountData(account.data || account, request.app.gt))
374
+ },
375
+ {
376
+ layout: 'app'
377
+ }
378
+ );
379
+ },
380
+
381
+ options: {
382
+ validate: {
383
+ options: {
384
+ stripUnknown: true,
385
+ abortEarly: false,
386
+ convert: true
387
+ },
388
+
389
+ async failAction(request, h /*, err*/) {
390
+ return h.redirect('/admin/accounts').takeover();
391
+ },
392
+
393
+ query: Joi.object({
394
+ page: Joi.number().integer().min(1).max(1000000).default(1),
395
+ pageSize: Joi.number().integer().min(1).max(250).default(DEFAULT_PAGE_SIZE),
396
+ query: Joi.string().example('user@example.com').description('Filter accounts by name/email match').label('AccountQuery'),
397
+ state: Joi.string()
398
+ .trim()
399
+ .empty('')
400
+ .valid('init', 'syncing', 'connecting', 'connected', 'authenticationError', 'connectError', 'unset', 'disconnected')
401
+ .example('connected')
402
+ .description('Filter accounts by state')
403
+ .label('AccountState')
404
+ })
405
+ }
406
+ }
407
+ });
408
+
409
+ // New account POST handler
410
+ server.route({
411
+ method: 'POST',
412
+ path: '/admin/accounts/new',
413
+
414
+ async handler(request, h) {
415
+ let { data, signature } = await getSignedFormData({
416
+ account: request.payload.account,
417
+ name: request.payload.name,
418
+
419
+ // identify request
420
+ n: crypto.randomBytes(NONCE_BYTES).toString('base64'),
421
+ t: Date.now()
422
+ });
423
+
424
+ let url = new URL(`accounts/new`, 'http://localhost');
425
+
426
+ url.searchParams.append('data', data);
427
+ if (signature) {
428
+ url.searchParams.append('sig', signature);
429
+ }
430
+
431
+ let oauth2apps = (await oauth2Apps.list(0, 100)).apps.filter(app => app.includeInListing);
432
+
433
+ if (!oauth2apps.length) {
434
+ url.searchParams.append('type', 'imap');
435
+ }
436
+
437
+ return h.redirect(url.pathname + url.search);
438
+ },
439
+ options: {
440
+ validate: {
441
+ options: {
442
+ stripUnknown: true,
443
+ abortEarly: false,
444
+ convert: true
445
+ },
446
+
447
+ async failAction(request, h, err) {
448
+ let errors = {};
449
+
450
+ if (err.details) {
451
+ err.details.forEach(detail => {
452
+ if (!errors[detail.path]) {
453
+ errors[detail.path] = detail.message;
454
+ }
455
+ });
456
+ }
457
+
458
+ await request.flash({ type: 'danger', message: `Failed to set up account${errors.account ? `: ${errors.account}` : ''}` });
459
+ request.logger.error({ msg: 'Failed to update configuration', err });
460
+
461
+ return h.redirect('/admin/accounts').takeover();
462
+ },
463
+
464
+ payload: Joi.object({
465
+ account: accountIdSchema.default(null),
466
+ name: Joi.string().empty('').max(256).example('John Smith').description('Account Name')
467
+ })
468
+ }
469
+ }
470
+ });
471
+
472
+ async function accountFormHandler(request, h) {
473
+ const data = await parseSignedFormData(redis, request.payload, request.app.gt);
474
+
475
+ const oauth2App = await oauth2Apps.get(request.payload.type);
476
+
477
+ if (oauth2App && oauth2App.enabled) {
478
+ // prepare account entry
479
+
480
+ let accountData = {
481
+ account: data.account
482
+ };
483
+
484
+ for (let key of ['name', 'email', 'syncFrom', 'path']) {
485
+ if (data[key]) {
486
+ accountData[key] = data[key];
487
+ }
488
+ }
489
+
490
+ accountData.notifyFrom = data.notifyFrom || new Date().toISOString();
491
+
492
+ for (let key of ['redirectUrl', 'n', 't']) {
493
+ if (!accountData._meta) {
494
+ accountData._meta = {};
495
+ }
496
+ accountData._meta[key] = data[key];
497
+ }
498
+
499
+ if (data.delegated) {
500
+ accountData.delegated = true;
501
+ } else {
502
+ accountData.copy = false;
503
+ }
504
+
505
+ accountData.oauth2 = {
506
+ provider: oauth2App.id
507
+ };
508
+
509
+ // throws if invalid or unknown app ID
510
+ const oAuth2Client = await oauth2Apps.getClient(oauth2App.id);
511
+
512
+ const nonce = data.n || crypto.randomBytes(NONCE_BYTES).toString('base64url');
513
+
514
+ // store account data with atomic SET + EX
515
+ await redis.set(`${REDIS_PREFIX}account:add:${nonce}`, JSON.stringify(accountData), 'EX', Math.floor(MAX_FORM_TTL / 1000));
516
+
517
+ // Generate the url that will be used for the consent dialog.
518
+
519
+ let requestPayload = {
520
+ state: `account:add:${nonce}`
521
+ };
522
+
523
+ if (accountData.email) {
524
+ requestPayload.email = accountData.email;
525
+ }
526
+
527
+ let authorizeUrl = oAuth2Client.generateAuthUrl(requestPayload);
528
+
529
+ return h.redirect(authorizeUrl);
530
+ }
531
+
532
+ return h.view(
533
+ 'accounts/register/imap',
534
+ {
535
+ pageTitleFull: request.app.gt.gettext('Email Account Setup'),
536
+ values: {
537
+ data: request.payload.data,
538
+ sig: request.payload.sig,
539
+
540
+ email: data.email,
541
+ name: data.name
542
+ }
543
+ },
544
+ {
545
+ layout: 'public'
546
+ }
547
+ );
548
+ }
549
+
550
+ // Public GET new account form
551
+ server.route({
552
+ method: 'GET',
553
+ path: '/accounts/new',
554
+ async handler(request, h) {
555
+ if (request.query.type) {
556
+ request.payload = request.query;
557
+ return accountFormHandler(request, h);
558
+ }
559
+
560
+ // throws if check fails
561
+ await parseSignedFormData(redis, request.query, request.app.gt);
562
+
563
+ let oauth2apps = (await oauth2Apps.list(0, 100)).apps.filter(app => app.includeInListing);
564
+ oauth2apps.forEach(app => {
565
+ app.providerData = oauth2ProviderData(app.provider, app.cloud);
566
+ });
567
+
568
+ return h.view(
569
+ 'accounts/register/index',
570
+ {
571
+ pageTitleFull: request.app.gt.gettext('Email Account Setup'),
572
+ values: {
573
+ data: request.query.data,
574
+ sig: request.query.sig
575
+ },
576
+
577
+ oauth2apps
578
+ },
579
+ {
580
+ layout: 'public'
581
+ }
582
+ );
583
+ },
584
+ options: {
585
+ auth: false,
586
+
587
+ validate: {
588
+ options: {
589
+ stripUnknown: true,
590
+ abortEarly: false,
591
+ convert: true
592
+ },
593
+
594
+ async failAction(request, h, err) {
595
+ request.logger.error({ msg: 'Failed to validate request arguments', err });
596
+ let error = Boom.boomify(new Error(request.app.gt.gettext('Invalid request. Check your input and try again.')), { statusCode: 400 });
597
+ if (err.code) {
598
+ error.output.payload.code = err.code;
599
+ }
600
+ throw error;
601
+ },
602
+
603
+ query: Joi.object({
604
+ data: Joi.string().base64({ paddingRequired: false, urlSafe: true }).required(),
605
+ sig: Joi.string().base64({ paddingRequired: false, urlSafe: true }),
606
+ type: defaultAccountTypeSchema
607
+ })
608
+ }
609
+ }
610
+ });
611
+
612
+ // Public POST new account form
613
+ server.route({
614
+ method: 'POST',
615
+ path: '/accounts/new',
616
+
617
+ handler: accountFormHandler,
618
+ options: {
619
+ auth: false,
620
+
621
+ validate: {
622
+ options: {
623
+ stripUnknown: true,
624
+ abortEarly: false,
625
+ convert: true
626
+ },
627
+
628
+ async failAction(request, h, err) {
629
+ request.logger.error({ msg: 'Failed to validate request arguments', err });
630
+ let error = Boom.boomify(new Error(request.app.gt.gettext('Invalid request. Check your input and try again.')), { statusCode: 400 });
631
+ if (err.code) {
632
+ error.output.payload.code = err.code;
633
+ }
634
+ throw error;
635
+ },
636
+
637
+ payload: Joi.object({
638
+ data: Joi.string().base64({ paddingRequired: false, urlSafe: true }).required(),
639
+ sig: Joi.string().base64({ paddingRequired: false, urlSafe: true }),
640
+ type: Joi.string()
641
+ .empty('')
642
+ .allow(false)
643
+ .default(false)
644
+ .example('imap')
645
+ .description(
646
+ 'Display the form for the specified account type (either "imap" or an OAuth2 app ID) instead of allowing the user to choose'
647
+ )
648
+ })
649
+ }
650
+ }
651
+ });
652
+
653
+ // IMAP account setup form
654
+ server.route({
655
+ method: 'POST',
656
+ path: '/accounts/new/imap',
657
+
658
+ async handler(request, h) {
659
+ await parseSignedFormData(redis, request.payload, request.app.gt);
660
+
661
+ let serverSettings;
662
+ try {
663
+ serverSettings = await autodetectImapSettings(request.payload.email, request.app.gt);
664
+ } catch (err) {
665
+ request.logger.error({ msg: 'Failed to resolve email server settings', email: request.payload.email, err });
666
+ }
667
+
668
+ let values = Object.assign(
669
+ {
670
+ name: request.payload.name,
671
+ email: request.payload.email,
672
+ password: request.payload.password,
673
+ data: request.payload.data,
674
+ sig: request.payload.sig
675
+ },
676
+ flattenObjectKeys(serverSettings)
677
+ );
678
+
679
+ values.imap_auth_user = values.imap_auth_user || request.payload.email;
680
+ values.smtp_auth_user = values.smtp_auth_user || request.payload.email;
681
+
682
+ values.imap_auth_pass = request.payload.password;
683
+ values.smtp_auth_pass = request.payload.password;
684
+
685
+ return h.view(
686
+ 'accounts/register/imap-server',
687
+ {
688
+ pageTitleFull: request.app.gt.gettext('Email Account Setup'),
689
+ values,
690
+ autoTest:
691
+ values._source &&
692
+ values.imap_auth_user &&
693
+ values.smtp_auth_user &&
694
+ values.imap_auth_pass &&
695
+ values.smtp_auth_pass &&
696
+ values.imap_host &&
697
+ values.smtp_host &&
698
+ values.imap_port &&
699
+ values.smtp_port &&
700
+ true
701
+ },
702
+ {
703
+ layout: 'public'
704
+ }
705
+ );
706
+ },
707
+ options: {
708
+ auth: false,
709
+
710
+ validate: {
711
+ options: {
712
+ stripUnknown: true,
713
+ abortEarly: false,
714
+ convert: true
715
+ },
716
+
717
+ async failAction(request, h, err) {
718
+ let errors = {};
719
+
720
+ if (err.details) {
721
+ err.details.forEach(detail => {
722
+ if (!errors[detail.path]) {
723
+ errors[detail.path] = detail.message;
724
+ }
725
+ });
726
+ }
727
+
728
+ await request.flash({ type: 'danger', message: request.app.gt.gettext("Couldn't set up account. Try again.") });
729
+ request.logger.error({ msg: 'Failed to process account', err });
730
+
731
+ return h
732
+ .view(
733
+ 'accounts/register/imap',
734
+ {
735
+ pageTitleFull: request.app.gt.gettext('Email Account Setup'),
736
+ errors
737
+ },
738
+ {
739
+ layout: 'public'
740
+ }
741
+ )
742
+ .takeover();
743
+ },
744
+
745
+ payload: Joi.object({
746
+ data: Joi.string().base64({ paddingRequired: false, urlSafe: true }).required(),
747
+ sig: Joi.string().base64({ paddingRequired: false, urlSafe: true }),
748
+ name: Joi.string().empty('').max(256).example('John Smith').description('Account Name'),
749
+ email: Joi.string().email().required().example('user@example.com').label('Email').description('Your account email'),
750
+ password: Joi.string().max(1024).min(1).required().example('secret').label('Password').description('Your account password')
751
+ })
752
+ }
753
+ }
754
+ });
755
+
756
+ // Test IMAP settings
757
+ server.route({
758
+ method: 'POST',
759
+ path: '/accounts/new/imap/test',
760
+ async handler(request) {
761
+ try {
762
+ let verifyResult = await verifyAccountInfo(
763
+ redis,
764
+ {
765
+ imap: {
766
+ host: request.payload.imap_host,
767
+ port: request.payload.imap_port,
768
+ secure: request.payload.imap_secure,
769
+ disabled: request.payload.imap_disabled,
770
+ auth: {
771
+ user: request.payload.imap_auth_user,
772
+ pass: request.payload.imap_auth_pass
773
+ }
774
+ },
775
+ smtp: {
776
+ host: request.payload.smtp_host,
777
+ port: request.payload.smtp_port,
778
+ secure: request.payload.smtp_secure,
779
+ auth: {
780
+ user: request.payload.smtp_auth_user,
781
+ pass: request.payload.smtp_auth_pass
782
+ }
783
+ }
784
+ },
785
+ request.logger.child({ action: 'verify-account' })
786
+ );
787
+
788
+ if (verifyResult) {
789
+ if (verifyResult.imap && verifyResult.imap.error && verifyResult.imap.code) {
790
+ switch (verifyResult.imap.code) {
791
+ case 'ENOTFOUND':
792
+ verifyResult.imap.error = request.app.gt.gettext('Server hostname was not found');
793
+ break;
794
+ case 'AUTHENTICATIONFAILED':
795
+ verifyResult.imap.error = request.app.gt.gettext('Invalid username or password');
796
+ break;
797
+ }
798
+ }
799
+
800
+ if (verifyResult.smtp && verifyResult.smtp.error && verifyResult.smtp.code) {
801
+ switch (verifyResult.smtp.code) {
802
+ case 'EDNS':
803
+ verifyResult.smtp.error = request.app.gt.gettext('Server hostname was not found');
804
+ break;
805
+ case 'EAUTH':
806
+ verifyResult.smtp.error = request.app.gt.gettext('Invalid username or password');
807
+ break;
808
+ case 'ESOCKET':
809
+ if (/openssl/.test(verifyResult.smtp.error)) {
810
+ verifyResult.smtp.error = request.app.gt.gettext('TLS protocol error');
811
+ }
812
+ break;
813
+ }
814
+ }
815
+ }
816
+
817
+ return verifyResult;
818
+ } catch (err) {
819
+ if (Boom.isBoom(err)) {
820
+ throw err;
821
+ }
822
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
823
+ if (err.code) {
824
+ error.output.payload.code = err.code;
825
+ }
826
+ throw error;
827
+ }
828
+ },
829
+ options: {
830
+ tags: ['test'],
831
+ auth: false,
832
+
833
+ validate: {
834
+ options: {
835
+ stripUnknown: true,
836
+ abortEarly: false,
837
+ convert: true
838
+ },
839
+
840
+ failAction,
841
+
842
+ payload: Joi.object({
843
+ imap_auth_user: Joi.string().empty('').trim().max(1024).required(),
844
+ imap_auth_pass: Joi.string().empty('').max(1024).required(),
845
+ imap_host: Joi.string().hostname().required().example('imap.gmail.com').description('Hostname to connect to').label('IMAP host'),
846
+ imap_port: Joi.number()
847
+ .integer()
848
+ .min(1)
849
+ .max(64 * 1024)
850
+ .required()
851
+ .example(993)
852
+ .description('Service port number')
853
+ .label('IMAP port'),
854
+ imap_secure: Joi.boolean()
855
+ .truthy('Y', 'true', '1', 'on')
856
+ .falsy('N', 'false', 0, '')
857
+ .default(false)
858
+ .example(true)
859
+ .description('Should connection use TLS. Usually true for port 993'),
860
+ imap_disabled: Joi.boolean()
861
+ .truthy('Y', 'true', '1', 'on')
862
+ .falsy('N', 'false', 0, '')
863
+ .default(false)
864
+ .example(true)
865
+ .description('Disable IMAP if you are using this email account to only send emails.'),
866
+
867
+ smtp_auth_user: Joi.string().empty('').trim().max(1024).required(),
868
+ smtp_auth_pass: Joi.string().empty('').max(1024).required(),
869
+ smtp_host: Joi.string().hostname().required().example('smtp.gmail.com').description('Hostname to connect to'),
870
+ smtp_port: Joi.number()
871
+ .integer()
872
+ .min(1)
873
+ .max(64 * 1024)
874
+ .required()
875
+ .example(465)
876
+ .description('Service port number')
877
+ .label('SMTP host'),
878
+ smtp_secure: Joi.boolean()
879
+ .truthy('Y', 'true', '1', 'on')
880
+ .falsy('N', 'false', 0, '')
881
+ .default(false)
882
+ .example(true)
883
+ .description('Should connection use TLS. Usually true for port 465')
884
+ .label('SMTP port')
885
+ })
886
+ }
887
+ }
888
+ });
889
+
890
+ // Submit IMAP server settings
891
+ server.route({
892
+ method: 'POST',
893
+ path: '/accounts/new/imap/server',
894
+
895
+ async handler(request, h) {
896
+ const data = await parseSignedFormData(redis, request.payload, request.app.gt);
897
+
898
+ const accountData = {
899
+ account: data.account || null,
900
+ name: request.payload.name || data.name,
901
+ email: request.payload.email,
902
+
903
+ tz: request.payload.tz,
904
+
905
+ notifyFrom: data.notifyFrom ? new Date(data.notifyFrom) : new Date(),
906
+
907
+ syncFrom: data.syncFrom || null,
908
+ path: data.path || null,
909
+
910
+ imap: {
911
+ host: request.payload.imap_host,
912
+ port: request.payload.imap_port,
913
+ secure: request.payload.imap_secure,
914
+ disabled: request.payload.imap_disabled,
915
+ auth: {
916
+ user: request.payload.imap_auth_user,
917
+ pass: request.payload.imap_auth_pass
918
+ }
919
+ },
920
+ smtp: {
921
+ host: request.payload.smtp_host,
922
+ port: request.payload.smtp_port,
923
+ secure: request.payload.smtp_secure,
924
+ auth: {
925
+ user: request.payload.smtp_auth_user,
926
+ pass: request.payload.smtp_auth_pass
927
+ }
928
+ }
929
+ };
930
+
931
+ if (data.subconnections && data.subconnections.length) {
932
+ accountData.subconnections = data.subconnections;
933
+ }
934
+
935
+ const accountObject = new Account({ redis, call, secret: await getSecret() });
936
+ const result = await accountObject.create(accountData);
937
+
938
+ if (data.n) {
939
+ // store nonce to prevent this URL to be reused
940
+ const keyName = `${REDIS_PREFIX}account:form:${data.n}`;
941
+ try {
942
+ await redis
943
+ .multi()
944
+ .set(keyName, (data.t || '0').toString())
945
+ .expire(keyName, Math.floor(MAX_FORM_TTL / 1000))
946
+ .exec();
947
+ } catch (err) {
948
+ request.logger.error({ msg: 'Failed to set nonce for an account form request', err });
949
+ }
950
+ }
951
+
952
+ let httpRedirectUrl;
953
+ if (data.redirectUrl) {
954
+ const serviceUrl = await settings.get('serviceUrl');
955
+ const url = new URL(data.redirectUrl, serviceUrl);
956
+ url.searchParams.set('account', result.account);
957
+ url.searchParams.set('state', result.state);
958
+ httpRedirectUrl = url.href;
959
+ } else {
960
+ httpRedirectUrl = `/admin/accounts/${result.account}`;
961
+ }
962
+
963
+ return h.view(
964
+ 'redirect',
965
+ {
966
+ pageTitleFull: request.app.gt.gettext('Email Account Setup'),
967
+ httpRedirectUrl
968
+ },
969
+ {
970
+ layout: 'public'
971
+ }
972
+ );
973
+ },
974
+ options: {
975
+ auth: false,
976
+
977
+ validate: {
978
+ options: {
979
+ stripUnknown: true,
980
+ abortEarly: false,
981
+ convert: true
982
+ },
983
+
984
+ async failAction(request, h, err) {
985
+ let errors = {};
986
+
987
+ if (err.details) {
988
+ err.details.forEach(detail => {
989
+ if (!errors[detail.path]) {
990
+ errors[detail.path] = detail.message;
991
+ }
992
+ });
993
+ }
994
+
995
+ await request.flash({ type: 'danger', message: request.app.gt.gettext("Couldn't set up account. Try again.") });
996
+ request.logger.error({ msg: 'Failed to process account', err });
997
+
998
+ return h
999
+ .view(
1000
+ 'accounts/register/imap-server',
1001
+ {
1002
+ pageTitleFull: request.app.gt.gettext('Email Account Setup'),
1003
+ errors
1004
+ },
1005
+ {
1006
+ layout: 'public'
1007
+ }
1008
+ )
1009
+ .takeover();
1010
+ },
1011
+
1012
+ payload: Joi.object({
1013
+ data: Joi.string().base64({ paddingRequired: false, urlSafe: true }).required(),
1014
+ sig: Joi.string().base64({ paddingRequired: false, urlSafe: true }),
1015
+ name: Joi.string().empty('').max(256).example('John Smith').description('Account Name'),
1016
+ tz: Joi.string().empty('').max(100).example('Europe/Tallinn').description('Optional timezone for autogenerated date strings'),
1017
+ email: Joi.string().email().required().example('user@example.com').label('Email').description('Your account email'),
1018
+ imap_auth_user: Joi.string().empty('').trim().max(1024).required(),
1019
+ imap_auth_pass: Joi.string().empty('').max(1024).required(),
1020
+ imap_host: Joi.string().hostname().required().example('imap.gmail.com').description('Hostname to connect to'),
1021
+ imap_port: Joi.number()
1022
+ .integer()
1023
+ .min(1)
1024
+ .max(64 * 1024)
1025
+ .required()
1026
+ .example(993)
1027
+ .description('Service port number'),
1028
+ imap_secure: Joi.boolean()
1029
+ .truthy('Y', 'true', '1', 'on')
1030
+ .falsy('N', 'false', 0, '')
1031
+ .default(false)
1032
+ .example(true)
1033
+ .description('Should connection use TLS. Usually true for port 993'),
1034
+
1035
+ imap_disabled: Joi.boolean()
1036
+ .truthy('Y', 'true', '1', 'on')
1037
+ .falsy('N', 'false', 0, '')
1038
+ .default(false)
1039
+ .example(true)
1040
+ .description('Disable IMAP if you are using this email account to only send emails.'),
1041
+
1042
+ smtp_auth_user: Joi.string().empty('').trim().max(1024).required(),
1043
+ smtp_auth_pass: Joi.string().empty('').max(1024).required(),
1044
+ smtp_host: Joi.string().hostname().required().example('smtp.gmail.com').description('Hostname to connect to'),
1045
+ smtp_port: Joi.number()
1046
+ .integer()
1047
+ .min(1)
1048
+ .max(64 * 1024)
1049
+ .required()
1050
+ .example(465)
1051
+ .description('Service port number'),
1052
+ smtp_secure: Joi.boolean()
1053
+ .truthy('Y', 'true', '1', 'on')
1054
+ .falsy('N', 'false', 0, '')
1055
+ .default(false)
1056
+ .example(true)
1057
+ .description('Should connection use TLS. Usually true for port 465')
1058
+ })
1059
+ }
1060
+ }
1061
+ });
1062
+
1063
+ // View account details
1064
+ server.route({
1065
+ method: 'GET',
1066
+ path: '/admin/accounts/{account}',
1067
+ async handler(request, h) {
1068
+ let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
1069
+ let accountData;
1070
+
1071
+ const runIndex = await call({
1072
+ cmd: 'runIndex'
1073
+ });
1074
+
1075
+ try {
1076
+ // throws if account does not exist
1077
+ accountData = await accountObject.loadAccountData(null, null, runIndex);
1078
+ } catch (err) {
1079
+ if (Boom.isBoom(err)) {
1080
+ throw err;
1081
+ }
1082
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
1083
+ if (err.code) {
1084
+ error.output.payload.code = err.code;
1085
+ }
1086
+ throw error;
1087
+ }
1088
+
1089
+ let subConnectionInfo;
1090
+ try {
1091
+ subConnectionInfo = await call({ cmd: 'subconnections', account: request.params.account });
1092
+ for (let subconnection of subConnectionInfo) {
1093
+ formatAccountData(subconnection, request.app.gt);
1094
+ }
1095
+ } catch (err) {
1096
+ subConnectionInfo = {
1097
+ err
1098
+ };
1099
+ }
1100
+
1101
+ if (accountData && accountData.oauth2 && accountData.oauth2.provider) {
1102
+ let oauth2App = await oauth2Apps.get(accountData.oauth2.provider);
1103
+ if (oauth2App) {
1104
+ accountData.oauth2.app = oauth2App;
1105
+ accountData.oauth2.providerData = oauth2ProviderData(oauth2App.provider, oauth2App.cloud);
1106
+ }
1107
+ }
1108
+
1109
+ accountData = formatAccountData(accountData, request.app.gt);
1110
+
1111
+ accountData.imap = accountData.imap || {
1112
+ disabled: !accountData.oauth2
1113
+ };
1114
+
1115
+ let gatewayObject = new Gateway({ redis });
1116
+ let gateways = await gatewayObject.listGateways(0, 100);
1117
+
1118
+ let capabilities = [];
1119
+ if (accountData.imapServerInfo && accountData.imapServerInfo.capabilities) {
1120
+ capabilities = await capa(accountData.imapServerInfo.capabilities);
1121
+ }
1122
+
1123
+ let authCapabilities = [];
1124
+ if (accountData.imapServerInfo && accountData.imapServerInfo.authCapabilities) {
1125
+ authCapabilities = await capa(accountData.imapServerInfo.authCapabilities, accountData.imapServerInfo.lastUsedAuthCapability);
1126
+ }
1127
+
1128
+ if (accountData.smtpServerEhlo && accountData.smtpServerEhlo.length) {
1129
+ let smtpAuthMechanisms = [];
1130
+ for (let i = accountData.smtpServerEhlo.length - 1; i >= 0; i--) {
1131
+ let entry = accountData.smtpServerEhlo[i];
1132
+ if (/^auth\b/i.test(entry)) {
1133
+ let authEntries = entry.split(/\s+/).slice(1);
1134
+ if (authEntries.length) {
1135
+ smtpAuthMechanisms = smtpAuthMechanisms.concat(authEntries);
1136
+ }
1137
+ accountData.smtpServerEhlo.splice(i, 1);
1138
+ }
1139
+ }
1140
+ accountData.smtpAuthMechanisms = Array.from(new Set(smtpAuthMechanisms));
1141
+
1142
+ for (let i = accountData.smtpAuthMechanisms.length - 1; i >= 0; i--) {
1143
+ let entry = accountData.smtpAuthMechanisms[i];
1144
+ switch (entry.toUpperCase()) {
1145
+ case 'LOGIN':
1146
+ accountData.smtpAuthMechanisms[i] = {
1147
+ auth: entry,
1148
+ rfc: 'draft-murchison-sasl-login',
1149
+ url: 'https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login'
1150
+ };
1151
+ break;
1152
+ case 'PLAIN':
1153
+ accountData.smtpAuthMechanisms[i] = {
1154
+ auth: entry,
1155
+ rfc: 'RFC4616',
1156
+ url: 'https://www.rfc-editor.org/rfc/rfc4616.html'
1157
+ };
1158
+ break;
1159
+ case 'XOAUTH2':
1160
+ accountData.smtpAuthMechanisms[i] = {
1161
+ auth: entry,
1162
+ rfc: 'xoauth2-protocol',
1163
+ url: 'https://developers.google.com/gmail/imap/xoauth2-protocol#smtp_protocol_exchange'
1164
+ };
1165
+ break;
1166
+ case 'OAUTHBEARER':
1167
+ accountData.smtpAuthMechanisms[i] = {
1168
+ auth: entry,
1169
+ rfc: 'RFC7628',
1170
+ url: 'https://www.rfc-editor.org/rfc/rfc7628.html'
1171
+ };
1172
+ break;
1173
+ }
1174
+ }
1175
+ }
1176
+
1177
+ let logInfo = (await settings.get('logs')) || {
1178
+ all: false,
1179
+ maxLogLines: DEFAULT_MAX_LOG_LINES
1180
+ };
1181
+
1182
+ if (!logInfo.maxLogLines) {
1183
+ logInfo.maxLogLines = DEFAULT_MAX_LOG_LINES;
1184
+ }
1185
+
1186
+ accountData.path = [].concat(accountData.path || '*');
1187
+ if (accountData.path.includes('*')) {
1188
+ accountData.path = null;
1189
+ }
1190
+
1191
+ let gmailWatch =
1192
+ accountData.watchResponse || accountData.watchFailure
1193
+ ? {
1194
+ lastCheckStr: accountData.lastWatch && accountData.lastWatch.toISOString(),
1195
+ expiresStr:
1196
+ accountData.watchResponse && accountData.watchResponse.expiration
1197
+ ? new Date(Number(accountData.watchResponse.expiration)).toISOString()
1198
+ : false
1199
+ }
1200
+ : false;
1201
+
1202
+ if (gmailWatch) {
1203
+ gmailWatch.active = gmailWatch.expiresStr && new Date(gmailWatch.expiresStr) > new Date();
1204
+ gmailWatch.stateLabel = gmailWatch.active ? 'Active' : 'Expired';
1205
+ if (accountData.watchFailure) {
1206
+ gmailWatch.error = accountData.watchFailure.err;
1207
+ if (!gmailWatch.active) {
1208
+ gmailWatch.stateLabel = 'Failed';
1209
+ }
1210
+
1211
+ if (accountData.watchFailure.req) {
1212
+ gmailWatch.request = {
1213
+ url: accountData.watchFailure.req.url,
1214
+ status: accountData.watchFailure.req.status,
1215
+ contentType: accountData.watchFailure.req.contentType,
1216
+ response: accountData.watchFailure.req.response
1217
+ };
1218
+ }
1219
+ }
1220
+ }
1221
+
1222
+ const canReadMail = (accountData.imap || accountData.oauth2) && !(accountData.imap && accountData.imap.disabled) && !DISABLE_MESSAGE_BROWSER;
1223
+
1224
+ return h.view(
1225
+ 'accounts/account',
1226
+ {
1227
+ pageTitle: `Email Accounts \u2013 ${accountData.email}`,
1228
+
1229
+ menuAccounts: true,
1230
+ account: accountData,
1231
+ logs: logInfo,
1232
+ smtpError: accountData.smtpStatus && accountData.smtpStatus.status === 'error',
1233
+
1234
+ showSmtp: accountData.smtp || (accountData.oauth2 && accountData.oauth2.provider),
1235
+
1236
+ canReadMail,
1237
+
1238
+ canSend: !!(
1239
+ accountData.smtp ||
1240
+ (accountData.oauth2 && accountData.oauth2.provider) ||
1241
+ (gateways && gateways.gateways && gateways.gateways.length)
1242
+ ),
1243
+ canUseSmtp: !!(
1244
+ accountData.smtp ||
1245
+ (accountData.oauth2 && (accountData.oauth2.provider || (accountData.oauth2.auth && accountData.oauth2.auth.delegatedAccount)))
1246
+ ),
1247
+ gateways: gateways && gateways.gateways,
1248
+
1249
+ testSendTemplate: cachedTemplates.testSend,
1250
+
1251
+ accountForm: await getSignedFormData({
1252
+ account: request.params.account,
1253
+ name: accountData.name,
1254
+ email: accountData.email,
1255
+ redirectUrl: `/admin/accounts/${request.params.account}`
1256
+ }),
1257
+
1258
+ showAdvanced: accountData.proxy || accountData.webhooks,
1259
+
1260
+ subConnectionInfo,
1261
+
1262
+ capabilities,
1263
+ authCapabilities,
1264
+
1265
+ gmailWatch
1266
+ },
1267
+ {
1268
+ layout: 'app'
1269
+ }
1270
+ );
1271
+ },
1272
+
1273
+ options: {
1274
+ validate: {
1275
+ options: {
1276
+ stripUnknown: true,
1277
+ abortEarly: false,
1278
+ convert: true
1279
+ },
1280
+
1281
+ async failAction(request, h, err) {
1282
+ await request.flash({ type: 'danger', message: `Invalid account request: ${err.message}` });
1283
+ return h.redirect('/admin/accounts').takeover();
1284
+ },
1285
+
1286
+ params: Joi.object({
1287
+ account: accountIdSchema.required()
1288
+ })
1289
+ }
1290
+ }
1291
+ });
1292
+
1293
+ // Delete account
1294
+ server.route({
1295
+ method: 'POST',
1296
+ path: '/admin/accounts/{account}/delete',
1297
+ async handler(request, h) {
1298
+ try {
1299
+ let accountObject = new Account({ redis, account: request.params.account, documentsQueue, call, secret: await getSecret() });
1300
+
1301
+ let deleted = await accountObject.delete();
1302
+ if (deleted) {
1303
+ await request.flash({ type: 'info', message: `Account deleted` });
1304
+ }
1305
+
1306
+ return h.redirect('/admin/accounts');
1307
+ } catch (err) {
1308
+ await request.flash({ type: 'danger', message: `Couldn't delete account. Try again.` });
1309
+ request.logger.error({ msg: 'Failed to delete the account', err, account: request.payload.account, remoteAddress: request.app.ip });
1310
+ return h.redirect(`/admin/accounts/${request.params.account}`);
1311
+ }
1312
+ },
1313
+ options: {
1314
+ validate: {
1315
+ options: {
1316
+ stripUnknown: true,
1317
+ abortEarly: false,
1318
+ convert: true
1319
+ },
1320
+
1321
+ async failAction(request, h, err) {
1322
+ await request.flash({ type: 'danger', message: `Couldn't delete account. Try again.` });
1323
+ request.logger.error({ msg: 'Failed to delete delete the account', err });
1324
+
1325
+ return h.redirect('/admin/accounts').takeover();
1326
+ },
1327
+
1328
+ params: Joi.object({
1329
+ account: accountIdSchema.required()
1330
+ })
1331
+ }
1332
+ }
1333
+ });
1334
+
1335
+ // Reconnect account
1336
+ server.route({
1337
+ method: 'POST',
1338
+ path: '/admin/accounts/{account}/reconnect',
1339
+ async handler(request) {
1340
+ let account = request.params.account;
1341
+ try {
1342
+ request.logger.info({ msg: 'Request reconnect for logging', account });
1343
+ try {
1344
+ await call({ cmd: 'reconnect', account });
1345
+ } catch (err) {
1346
+ request.logger.error({ msg: 'Reconnect request failed', action: 'request_reconnect', account, err });
1347
+ }
1348
+
1349
+ return {
1350
+ success: true
1351
+ };
1352
+ } catch (err) {
1353
+ request.logger.error({ msg: 'Failed to request reconnect', err, account });
1354
+ return { success: false, error: err.message };
1355
+ }
1356
+ },
1357
+ options: {
1358
+ validate: {
1359
+ options: {
1360
+ stripUnknown: true,
1361
+ abortEarly: false,
1362
+ convert: true
1363
+ },
1364
+
1365
+ failAction,
1366
+
1367
+ params: Joi.object({
1368
+ account: accountIdSchema.required()
1369
+ })
1370
+ }
1371
+ }
1372
+ });
1373
+
1374
+ // Sync account
1375
+ server.route({
1376
+ method: 'POST',
1377
+ path: '/admin/accounts/{account}/sync',
1378
+ async handler(request) {
1379
+ let account = request.params.account;
1380
+ try {
1381
+ request.logger.info({ msg: 'Request syncing', account });
1382
+ try {
1383
+ await call({ cmd: 'sync', account });
1384
+ } catch (err) {
1385
+ request.logger.error({ msg: 'Sync request failed', action: 'request_sync', account, err });
1386
+ }
1387
+
1388
+ return {
1389
+ success: true
1390
+ };
1391
+ } catch (err) {
1392
+ request.logger.error({ msg: 'Failed to request syncing', err, account });
1393
+ return { success: false, error: err.message };
1394
+ }
1395
+ },
1396
+ options: {
1397
+ validate: {
1398
+ options: {
1399
+ stripUnknown: true,
1400
+ abortEarly: false,
1401
+ convert: true
1402
+ },
1403
+
1404
+ failAction,
1405
+
1406
+ params: Joi.object({
1407
+ account: accountIdSchema.required()
1408
+ })
1409
+ }
1410
+ }
1411
+ });
1412
+
1413
+ // Toggle account logs
1414
+ server.route({
1415
+ method: 'POST',
1416
+ path: '/admin/accounts/{account}/logs',
1417
+ async handler(request) {
1418
+ let account = request.params.account;
1419
+ let accountObject = new Account({ redis, account });
1420
+ try {
1421
+ request.logger.info({ msg: 'Request to update account logging state', account, enabled: request.payload.enabled });
1422
+
1423
+ await redis.hSetExists(accountObject.getAccountKey(), 'logs', request.payload.enabled ? 'true' : 'false');
1424
+
1425
+ try {
1426
+ await call({ cmd: 'update', account });
1427
+ } catch (err) {
1428
+ request.logger.error({ msg: 'Reconnect request failed', action: 'request_reconnect', account, err });
1429
+ }
1430
+
1431
+ return {
1432
+ success: true,
1433
+ enabled: (await redis.hget(accountObject.getAccountKey(), 'logs')) === 'true'
1434
+ };
1435
+ } catch (err) {
1436
+ request.logger.error({ msg: 'Failed to update account logging state', err, account, enabled: request.payload.enabled });
1437
+ return { success: false, error: err.message };
1438
+ }
1439
+ },
1440
+ options: {
1441
+ validate: {
1442
+ options: {
1443
+ stripUnknown: true,
1444
+ abortEarly: false,
1445
+ convert: true
1446
+ },
1447
+
1448
+ failAction,
1449
+
1450
+ params: Joi.object({
1451
+ account: accountIdSchema.required()
1452
+ }),
1453
+
1454
+ payload: Joi.object({
1455
+ enabled: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false)
1456
+ })
1457
+ }
1458
+ }
1459
+ });
1460
+
1461
+ // Flush account logs
1462
+ server.route({
1463
+ method: 'POST',
1464
+ path: '/admin/accounts/{account}/logs-flush',
1465
+ async handler(request) {
1466
+ let account = request.params.account;
1467
+ let accountObject = new Account({ redis, account });
1468
+ try {
1469
+ request.logger.info({ msg: 'Request to flush logs', account });
1470
+
1471
+ await redis.del(accountObject.getLogKey());
1472
+
1473
+ return {
1474
+ success: true
1475
+ };
1476
+ } catch (err) {
1477
+ request.logger.error({ msg: 'Failed to flush logs', err, account });
1478
+ return { success: false, error: err.message };
1479
+ }
1480
+ },
1481
+ options: {
1482
+ validate: {
1483
+ options: {
1484
+ stripUnknown: true,
1485
+ abortEarly: false,
1486
+ convert: true
1487
+ },
1488
+
1489
+ failAction,
1490
+
1491
+ params: Joi.object({
1492
+ account: accountIdSchema.required()
1493
+ })
1494
+ }
1495
+ }
1496
+ });
1497
+
1498
+ // Get account logs as text
1499
+ server.route({
1500
+ method: 'GET',
1501
+ path: '/admin/accounts/{account}/logs.txt',
1502
+ async handler(request) {
1503
+ return getLogs(redis, request.params.account);
1504
+ },
1505
+ options: {
1506
+ validate: {
1507
+ options: {
1508
+ stripUnknown: true,
1509
+ abortEarly: false,
1510
+ convert: true
1511
+ },
1512
+
1513
+ failAction,
1514
+
1515
+ params: Joi.object({
1516
+ account: accountIdSchema.required()
1517
+ })
1518
+ }
1519
+ }
1520
+ });
1521
+
1522
+ // Browse account messages
1523
+ server.route({
1524
+ method: 'GET',
1525
+ path: '/admin/accounts/{account}/browse',
1526
+ async handler(request, h) {
1527
+ let authData = await settings.get('authData');
1528
+ let hasExistingPassword = !!(authData && authData.password);
1529
+ if (!hasExistingPassword) {
1530
+ await request.flash({ type: 'info', message: `Set a password to access messages` });
1531
+ return h.redirect('/admin/account/password');
1532
+ }
1533
+
1534
+ if (!request.state.ee || !request.state.ee.sid) {
1535
+ // force login to get the sid assigned
1536
+ if (request.cookieAuth) {
1537
+ request.cookieAuth.clear();
1538
+ }
1539
+ await request.flash({ type: 'info', message: `Sign in again to continue` });
1540
+ return h.redirect('/admin/login?next=' + encodeURIComponent('/admin/accounts/{account}/browse'));
1541
+ }
1542
+
1543
+ let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
1544
+ let accountData;
1545
+ try {
1546
+ // throws if account does not exist
1547
+ accountData = await accountObject.loadAccountData();
1548
+ } catch (err) {
1549
+ if (Boom.isBoom(err)) {
1550
+ throw err;
1551
+ }
1552
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
1553
+ if (err.code) {
1554
+ error.output.payload.code = err.code;
1555
+ }
1556
+ throw error;
1557
+ }
1558
+
1559
+ const canReadMail = (accountData.imap || accountData.oauth2) && !(accountData.imap && accountData.imap.disabled) && !DISABLE_MESSAGE_BROWSER;
1560
+ if (!canReadMail) {
1561
+ await request.flash({ type: 'danger', message: `Mail access is disabled for this account` });
1562
+ return h.redirect(`/admin/accounts/${request.params.account}`);
1563
+ }
1564
+
1565
+ return h.view(
1566
+ 'accounts/browse',
1567
+ {
1568
+ pageTitle: `Browse \u2013 ${accountData.email}`,
1569
+
1570
+ menuAccounts: true,
1571
+ account: request.params.account,
1572
+
1573
+ sessionToken: await tokens.getSessionToken(request.state.ee.sid, request.params.account, 900)
1574
+ },
1575
+ {
1576
+ layout: 'app'
1577
+ }
1578
+ );
1579
+ },
1580
+
1581
+ options: {
1582
+ validate: {
1583
+ options: {
1584
+ stripUnknown: true,
1585
+ abortEarly: false,
1586
+ convert: true
1587
+ },
1588
+
1589
+ async failAction(request, h, err) {
1590
+ await request.flash({ type: 'danger', message: `Invalid account request: ${err.message}` });
1591
+ return h.redirect('/admin/accounts').takeover();
1592
+ },
1593
+
1594
+ params: Joi.object({
1595
+ account: accountIdSchema.required()
1596
+ })
1597
+ }
1598
+ }
1599
+ });
1600
+
1601
+ // Edit account (GET)
1602
+ server.route({
1603
+ method: 'GET',
1604
+ path: '/admin/accounts/{account}/edit',
1605
+ async handler(request, h) {
1606
+ let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
1607
+ let accountData;
1608
+ try {
1609
+ // throws if account does not exist
1610
+ accountData = await accountObject.loadAccountData();
1611
+ } catch (err) {
1612
+ if (Boom.isBoom(err)) {
1613
+ throw err;
1614
+ }
1615
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
1616
+ if (err.code) {
1617
+ error.output.payload.code = err.code;
1618
+ }
1619
+ throw error;
1620
+ }
1621
+
1622
+ const values = Object.assign({}, flattenObjectKeys(accountData), {
1623
+ imap: true,
1624
+ imap_disabled: (!accountData.imap && !accountData.oauth2) || (accountData.imap && accountData.imap.disabled),
1625
+ smtp: !!accountData.smtp,
1626
+ oauth2: !!accountData.oauth2,
1627
+
1628
+ imap_auth_pass: '',
1629
+ smtp_auth_pass: '',
1630
+
1631
+ customHeaders: []
1632
+ .concat(accountData.webhooksCustomHeaders || [])
1633
+ .map(entry => `${entry.key}: ${entry.value}`.trim())
1634
+ .join('\n')
1635
+ });
1636
+
1637
+ let mailboxes = await getMailboxListing(accountObject);
1638
+
1639
+ return h.view(
1640
+ 'accounts/edit',
1641
+ {
1642
+ pageTitle: `Email Accounts \u2013 ${accountData.email}`,
1643
+
1644
+ menuAccounts: true,
1645
+ account: request.params.account,
1646
+ values,
1647
+ availablePaths: JSON.stringify(mailboxes.map(entry => entry.path)),
1648
+
1649
+ isApi: accountData.isApi,
1650
+
1651
+ hasIMAPPass: accountData.imap && accountData.imap.auth && !!accountData.imap.auth.pass,
1652
+ hasSMTPPass: accountData.smtp && accountData.smtp.auth && !!accountData.smtp.auth.pass,
1653
+ defaultSmtpEhloName: await getServiceHostname()
1654
+ },
1655
+ {
1656
+ layout: 'app'
1657
+ }
1658
+ );
1659
+ },
1660
+
1661
+ options: {
1662
+ validate: {
1663
+ options: {
1664
+ stripUnknown: true,
1665
+ abortEarly: false,
1666
+ convert: true
1667
+ },
1668
+
1669
+ async failAction(request, h, err) {
1670
+ await request.flash({ type: 'danger', message: `Invalid account request: ${err.message}` });
1671
+ return h.redirect('/admin/accounts').takeover();
1672
+ },
1673
+
1674
+ params: Joi.object({
1675
+ account: accountIdSchema.required()
1676
+ })
1677
+ }
1678
+ }
1679
+ });
1680
+
1681
+ // Edit account (POST)
1682
+ server.route({
1683
+ method: 'POST',
1684
+ path: '/admin/accounts/{account}/edit',
1685
+ async handler(request, h) {
1686
+ try {
1687
+ let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
1688
+
1689
+ let oldData = await accountObject.loadAccountData();
1690
+
1691
+ let updates = {
1692
+ account: request.params.account,
1693
+ name: request.payload.name || '',
1694
+ email: request.payload.email,
1695
+ proxy: request.payload.proxy,
1696
+ smtpEhloName: request.payload.smtpEhloName,
1697
+ webhooks: request.payload.webhooks
1698
+ };
1699
+
1700
+ updates.webhooksCustomHeaders = request.payload.customHeaders
1701
+ .split(/[\r\n]+/)
1702
+ .map(header => header.trim())
1703
+ .filter(header => header)
1704
+ .map(line => {
1705
+ let sep = line.indexOf(':');
1706
+ if (sep >= 0) {
1707
+ return {
1708
+ key: line.substring(0, sep).trim(),
1709
+ value: line.substring(sep + 1).trim()
1710
+ };
1711
+ }
1712
+ return {
1713
+ key: line,
1714
+ value: ''
1715
+ };
1716
+ });
1717
+
1718
+ if (request.payload.imap) {
1719
+ let imapTls = (oldData.imap && oldData.imap.tls) || {};
1720
+
1721
+ let updateKeys = {
1722
+ tls: imapTls
1723
+ };
1724
+
1725
+ for (let key of ['host', 'port', 'disabled', 'sentMailPath']) {
1726
+ if (`imap_${key}` in request.payload) {
1727
+ updateKeys[key] = request.payload[`imap_${key}`];
1728
+ }
1729
+ }
1730
+
1731
+ if ('imap_auth_user' in request.payload) {
1732
+ let imapAuth = Object.assign((oldData.imap && oldData.imap.auth) || {}, { user: request.payload.imap_auth_user });
1733
+ if (request.payload.imap_auth_pass) {
1734
+ imapAuth.pass = request.payload.imap_auth_pass;
1735
+ }
1736
+ updateKeys.auth = imapAuth;
1737
+ updateKeys.secure = request.payload.imap_secure;
1738
+ }
1739
+
1740
+ updates.imap = Object.assign(oldData.imap || {}, updateKeys);
1741
+
1742
+ if (request.payload.imap_resyncDelay) {
1743
+ updates.imap.resyncDelay = request.payload.imap_resyncDelay;
1744
+ }
1745
+ }
1746
+
1747
+ if (request.payload.smtp) {
1748
+ let smtpAuth = Object.assign((oldData.smtp && oldData.smtp.auth) || {}, { user: request.payload.smtp_auth_user });
1749
+ let smtpTls = (oldData.smtp && oldData.smtp.tls) || {};
1750
+
1751
+ if (request.payload.smtp_auth_pass) {
1752
+ smtpAuth.pass = request.payload.smtp_auth_pass;
1753
+ }
1754
+
1755
+ updates.smtp = Object.assign(oldData.smtp || {}, {
1756
+ host: request.payload.smtp_host,
1757
+ port: request.payload.smtp_port,
1758
+ secure: request.payload.smtp_secure,
1759
+ auth: smtpAuth,
1760
+ tls: smtpTls
1761
+ });
1762
+ }
1763
+
1764
+ await accountObject.update(updates);
1765
+
1766
+ return h.redirect(`/admin/accounts/${request.params.account}`);
1767
+ } catch (err) {
1768
+ await request.flash({ type: 'danger', message: `Couldn't save account settings. Try again.` });
1769
+ request.logger.error({ msg: 'Failed to update account settings', err, account: request.params.account });
1770
+
1771
+ let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
1772
+ let accountData = await accountObject.loadAccountData();
1773
+
1774
+ let mailboxes = await getMailboxListing(accountObject);
1775
+
1776
+ return h.view(
1777
+ 'accounts/edit',
1778
+ {
1779
+ pageTitle: `Email Accounts \u2013 ${accountData.email}`,
1780
+
1781
+ menuAccounts: true,
1782
+ account: request.params.account,
1783
+ availablePaths: JSON.stringify(mailboxes.map(entry => entry.path)),
1784
+
1785
+ isApi: accountData.isApi,
1786
+
1787
+ hasIMAPPass: accountData.imap && accountData.imap.auth && !!accountData.imap.auth.pass,
1788
+ hasSMTPPass: accountData.smtp && accountData.smtp.auth && !!accountData.smtp.auth.pass,
1789
+ defaultSmtpEhloName: await getServiceHostname()
1790
+ },
1791
+ {
1792
+ layout: 'app'
1793
+ }
1794
+ );
1795
+ }
1796
+ },
1797
+
1798
+ options: {
1799
+ validate: {
1800
+ options: {
1801
+ stripUnknown: true,
1802
+ abortEarly: false,
1803
+ convert: true
1804
+ },
1805
+
1806
+ async failAction(request, h, err) {
1807
+ let errors = {};
1808
+
1809
+ if (err.details) {
1810
+ err.details.forEach(detail => {
1811
+ if (!errors[detail.path]) {
1812
+ errors[detail.path] = detail.message;
1813
+ }
1814
+ });
1815
+ }
1816
+
1817
+ await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
1818
+ request.logger.error({ msg: 'Failed to update configuration', err });
1819
+
1820
+ let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
1821
+ let accountData = await accountObject.loadAccountData();
1822
+ let mailboxes = await getMailboxListing(accountObject);
1823
+
1824
+ return h
1825
+ .view(
1826
+ 'accounts/edit',
1827
+ {
1828
+ pageTitle: `Email Accounts \u2013 ${accountData.email}`,
1829
+
1830
+ menuAccounts: true,
1831
+ account: request.params.account,
1832
+ errors,
1833
+ availablePaths: JSON.stringify(mailboxes.map(entry => entry.path)),
1834
+
1835
+ isApi: accountData.isApi,
1836
+
1837
+ hasIMAPPass: accountData.imap && accountData.imap.auth && !!accountData.imap.auth.pass,
1838
+ hasSMTPPass: accountData.smtp && accountData.smtp.auth && !!accountData.smtp.auth.pass,
1839
+ defaultSmtpEhloName: await getServiceHostname()
1840
+ },
1841
+ {
1842
+ layout: 'app'
1843
+ }
1844
+ )
1845
+ .takeover();
1846
+ },
1847
+
1848
+ params: Joi.object({
1849
+ account: accountIdSchema.required()
1850
+ }),
1851
+
1852
+ payload: Joi.object({
1853
+ name: Joi.string().empty('').max(256).example('John Smith').description('Account Name'),
1854
+ email: Joi.string().email().required().example('user@example.com').label('Email').description('Your account email'),
1855
+
1856
+ proxy: settingsSchema.proxyUrl,
1857
+ smtpEhloName: settingsSchema.smtpEhloName,
1858
+
1859
+ imap: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),
1860
+
1861
+ imap_auth_user: Joi.string().empty('').trim().max(1024),
1862
+ imap_auth_pass: Joi.string().empty('').max(1024),
1863
+ imap_host: Joi.string().hostname().example('imap.gmail.com').description('Hostname to connect to'),
1864
+ imap_port: Joi.number()
1865
+ .integer()
1866
+ .min(1)
1867
+ .max(64 * 1024)
1868
+ .example(993)
1869
+ .description('Service port number'),
1870
+ imap_secure: Joi.boolean()
1871
+ .truthy('Y', 'true', '1', 'on')
1872
+ .falsy('N', 'false', 0, '')
1873
+ .default(false)
1874
+ .example(true)
1875
+ .description('Should connection use TLS. Usually true for port 993'),
1876
+ imap_disabled: Joi.boolean()
1877
+ .truthy('Y', 'true', '1', 'on')
1878
+ .falsy('N', 'false', 0, '')
1879
+ .default(false)
1880
+ .example(true)
1881
+ .description('Disable IMAP if you are using this email account to only send emails.'),
1882
+
1883
+ imap_resyncDelay: Joi.number().integer().empty(''),
1884
+
1885
+ imap_sentMailPath: Joi.string()
1886
+ .empty('')
1887
+ .default(null)
1888
+ .max(1024)
1889
+ .example('Sent Mail')
1890
+ .description("Upload sent message to this folder. By default the account's Sent Mail folder is used. Leave empty to unset."),
1891
+
1892
+ webhooks: Joi.string()
1893
+ .uri({
1894
+ scheme: ['http', 'https'],
1895
+ allowRelative: false
1896
+ })
1897
+ .allow('')
1898
+ .default('')
1899
+ .example('https://myservice.com/imap/webhooks')
1900
+ .description('Account-specific webhook URL'),
1901
+
1902
+ smtp: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),
1903
+
1904
+ smtp_auth_user: Joi.string().empty('').trim().max(1024),
1905
+ smtp_auth_pass: Joi.string().empty('').max(1024),
1906
+ smtp_host: Joi.string().hostname().example('smtp.gmail.com').description('Hostname to connect to'),
1907
+ smtp_port: Joi.number()
1908
+ .integer()
1909
+ .min(1)
1910
+ .max(64 * 1024)
1911
+ .example(465)
1912
+ .description('Service port number'),
1913
+ smtp_secure: Joi.boolean()
1914
+ .truthy('Y', 'true', '1', 'on')
1915
+ .falsy('N', 'false', 0, '')
1916
+ .default(false)
1917
+ .example(true)
1918
+ .description('Should connection use TLS. Usually true for port 465'),
1919
+
1920
+ customHeaders: Joi.string()
1921
+ .allow('')
1922
+ .trim()
1923
+ .max(10 * 1024)
1924
+ .description('Custom request headers')
1925
+ })
1926
+ }
1927
+ }
1928
+ });
1929
+ }
1930
+
1931
+ module.exports = init;