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.
- package/CHANGELOG.md +17 -0
- package/data/google-crawlers.json +1 -1
- package/lib/account/account-state.js +248 -0
- package/lib/account.js +45 -193
- package/lib/api-routes/account-routes.js +1023 -0
- package/lib/api-routes/message-routes.js +1377 -0
- package/lib/consts.js +12 -2
- package/lib/email-client/base-client.js +282 -771
- package/lib/email-client/gmail/gmail-api.js +243 -0
- package/lib/email-client/gmail-client.js +145 -53
- package/lib/email-client/imap/mailbox.js +24 -698
- package/lib/email-client/imap/sync-operations.js +812 -0
- package/lib/email-client/imap-client.js +1 -1
- package/lib/email-client/message-builder.js +566 -0
- package/lib/email-client/notification-handler.js +314 -0
- package/lib/email-client/outlook/graph-api.js +326 -0
- package/lib/email-client/outlook-client.js +159 -113
- package/lib/email-client/smtp-pool-manager.js +196 -0
- package/lib/imapproxy/imap-server.js +3 -12
- package/lib/oauth/gmail.js +4 -4
- package/lib/oauth/mail-ru.js +30 -5
- package/lib/oauth/outlook.js +57 -3
- package/lib/oauth/pubsub/google.js +30 -11
- package/lib/oauth/scope-checker.js +202 -0
- package/lib/oauth2-apps.js +8 -4
- package/lib/redis-operations.js +484 -0
- package/lib/routes-ui.js +283 -2582
- package/lib/tools.js +4 -196
- package/lib/ui-routes/account-routes.js +1931 -0
- package/lib/ui-routes/admin-config-routes.js +1233 -0
- package/lib/ui-routes/admin-entities-routes.js +2367 -0
- package/lib/ui-routes/oauth-routes.js +992 -0
- package/lib/utils/network.js +237 -0
- package/package.json +10 -10
- package/sbom.json +1 -1
- package/static/js/app.js +5 -5
- package/static/licenses.html +79 -19
- package/translations/de.mo +0 -0
- package/translations/de.po +97 -86
- package/translations/en.mo +0 -0
- package/translations/en.po +80 -75
- package/translations/et.mo +0 -0
- package/translations/et.po +96 -86
- package/translations/fr.mo +0 -0
- package/translations/fr.po +97 -86
- package/translations/ja.mo +0 -0
- package/translations/ja.po +96 -86
- package/translations/messages.pot +105 -91
- package/translations/nl.mo +0 -0
- package/translations/nl.po +98 -86
- package/translations/pl.mo +0 -0
- package/translations/pl.po +96 -86
- package/views/account/security.hbs +4 -4
- package/views/accounts/account.hbs +13 -13
- package/views/accounts/register/imap-server.hbs +12 -12
- package/views/config/document-store/pre-processing/index.hbs +4 -2
- package/views/config/oauth/app.hbs +6 -7
- package/views/config/oauth/index.hbs +2 -2
- package/views/config/service.hbs +3 -4
- package/views/dashboard.hbs +5 -7
- package/views/error.hbs +22 -7
- package/views/gateways/gateway.hbs +2 -2
- package/views/partials/add_account_modal.hbs +7 -10
- package/views/partials/document_store_header.hbs +1 -1
- package/views/partials/editor_scope_info.hbs +0 -1
- package/views/partials/oauth_config_header.hbs +1 -1
- package/views/partials/side_menu.hbs +3 -3
- package/views/partials/webhook_form.hbs +2 -2
- package/views/templates/index.hbs +1 -1
- package/views/templates/template.hbs +8 -8
- package/views/tokens/index.hbs +6 -6
- package/views/tokens/new.hbs +1 -1
- package/views/webhooks/index.hbs +4 -4
- package/views/webhooks/webhook.hbs +7 -7
- package/workers/api.js +148 -2436
- package/workers/smtp.js +2 -1
- package/lib/imapproxy/imap-core/test/client.js +0 -46
- package/lib/imapproxy/imap-core/test/fixtures/append.eml +0 -1196
- package/lib/imapproxy/imap-core/test/fixtures/chunks.js +0 -44
- package/lib/imapproxy/imap-core/test/fixtures/fix1.eml +0 -6
- package/lib/imapproxy/imap-core/test/fixtures/fix2.eml +0 -599
- package/lib/imapproxy/imap-core/test/fixtures/fix3.eml +0 -32
- package/lib/imapproxy/imap-core/test/fixtures/fix4.eml +0 -6
- package/lib/imapproxy/imap-core/test/fixtures/mimetorture.eml +0 -599
- package/lib/imapproxy/imap-core/test/fixtures/mimetorture.js +0 -2740
- package/lib/imapproxy/imap-core/test/fixtures/mimetorture.json +0 -1411
- package/lib/imapproxy/imap-core/test/fixtures/mimetree.js +0 -85
- package/lib/imapproxy/imap-core/test/fixtures/nodemailer.eml +0 -582
- package/lib/imapproxy/imap-core/test/fixtures/ryan_finnie_mime_torture.eml +0 -599
- package/lib/imapproxy/imap-core/test/fixtures/simple.eml +0 -42
- package/lib/imapproxy/imap-core/test/fixtures/simple.json +0 -164
- package/lib/imapproxy/imap-core/test/imap-compile-stream-test.js +0 -671
- package/lib/imapproxy/imap-core/test/imap-compiler-test.js +0 -272
- package/lib/imapproxy/imap-core/test/imap-indexer-test.js +0 -236
- package/lib/imapproxy/imap-core/test/imap-parser-test.js +0 -922
- package/lib/imapproxy/imap-core/test/memory-notifier.js +0 -129
- package/lib/imapproxy/imap-core/test/prepare.sh +0 -74
- package/lib/imapproxy/imap-core/test/protocol-test.js +0 -1756
- package/lib/imapproxy/imap-core/test/search-test.js +0 -1356
- package/lib/imapproxy/imap-core/test/test-client.js +0 -152
- package/lib/imapproxy/imap-core/test/test-server.js +0 -623
- package/lib/imapproxy/imap-core/test/tools-test.js +0 -22
- package/test/api-test.js +0 -899
- package/test/autoreply-test.js +0 -327
- package/test/bounce-test.js +0 -151
- package/test/complaint-test.js +0 -256
- package/test/fixtures/autoreply/LICENSE +0 -27
- package/test/fixtures/autoreply/rfc3834-01.eml +0 -23
- package/test/fixtures/autoreply/rfc3834-02.eml +0 -24
- package/test/fixtures/autoreply/rfc3834-03.eml +0 -26
- package/test/fixtures/autoreply/rfc3834-04.eml +0 -48
- package/test/fixtures/autoreply/rfc3834-05.eml +0 -19
- package/test/fixtures/autoreply/rfc3834-06.eml +0 -59
- package/test/fixtures/bounces/163.eml +0 -2521
- package/test/fixtures/bounces/fastmail.eml +0 -242
- package/test/fixtures/bounces/gmail.eml +0 -252
- package/test/fixtures/bounces/hotmail.eml +0 -655
- package/test/fixtures/bounces/mailru.eml +0 -121
- package/test/fixtures/bounces/outlook.eml +0 -1107
- package/test/fixtures/bounces/postfix.eml +0 -101
- package/test/fixtures/bounces/rambler.eml +0 -116
- package/test/fixtures/bounces/workmail.eml +0 -142
- package/test/fixtures/bounces/yahoo.eml +0 -139
- package/test/fixtures/bounces/zoho.eml +0 -83
- package/test/fixtures/bounces/zonemta.eml +0 -100
- package/test/fixtures/complaints/LICENSE +0 -27
- package/test/fixtures/complaints/amazonses.eml +0 -72
- package/test/fixtures/complaints/dmarc.eml +0 -59
- package/test/fixtures/complaints/hotmail.eml +0 -49
- package/test/fixtures/complaints/optout.eml +0 -40
- package/test/fixtures/complaints/standard-arf.eml +0 -68
- package/test/fixtures/complaints/yahoo.eml +0 -68
- package/test/oauth2-apps-test.js +0 -301
- package/test/sendonly-test.js +0 -160
- package/test/test-config.js +0 -34
- 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;
|