emailengine-app 2.68.0 → 2.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/codeql/codeql-config.yml +16 -0
- package/.github/workflows/codeql.yml +102 -0
- package/.github/workflows/deploy.yml +8 -0
- package/.github/workflows/release.yaml +4 -0
- package/.github/workflows/test.yml +3 -0
- package/CHANGELOG.md +49 -0
- package/SECURITY.md +80 -0
- package/SECURITY.txt +27 -0
- package/config/default.toml +2 -0
- package/data/google-crawlers.json +13 -1
- package/lib/account.js +62 -25
- package/lib/api-routes/account-routes.js +493 -75
- package/lib/api-routes/blocklist-routes.js +337 -0
- package/lib/api-routes/delivery-test-routes.js +321 -0
- package/lib/api-routes/export-routes.js +1 -12
- package/lib/api-routes/gateway-routes.js +376 -0
- package/lib/api-routes/license-routes.js +142 -0
- package/lib/api-routes/mailbox-routes.js +318 -0
- package/lib/api-routes/message-routes.js +21 -129
- package/lib/api-routes/oauth2-app-routes.js +631 -0
- package/lib/api-routes/outbox-routes.js +173 -0
- package/lib/api-routes/pubsub-routes.js +98 -0
- package/lib/api-routes/route-helpers.js +45 -0
- package/lib/api-routes/settings-routes.js +331 -0
- package/lib/api-routes/stats-routes.js +77 -0
- package/lib/api-routes/submit-routes.js +472 -0
- package/lib/api-routes/template-routes.js +7 -55
- package/lib/api-routes/token-routes.js +297 -0
- package/lib/api-routes/webhook-route-routes.js +152 -0
- package/lib/email-client/gmail-client.js +14 -0
- package/lib/email-client/imap/mailbox.js +34 -11
- package/lib/email-client/imap/subconnection.js +20 -12
- package/lib/email-client/imap/sync-operations.js +130 -2
- package/lib/email-client/imap-client.js +116 -58
- package/lib/email-client/outlook-client.js +85 -13
- package/lib/export.js +60 -19
- package/lib/imapproxy/imap-core/lib/commands/starttls.js +18 -0
- package/lib/imapproxy/imap-core/lib/imap-command.js +7 -2
- package/lib/imapproxy/imap-core/lib/imap-connection.js +113 -23
- package/lib/imapproxy/imap-core/lib/imap-server.js +25 -1
- package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
- package/lib/imapproxy/imap-server.js +92 -29
- package/lib/message-port-stream.js +113 -16
- package/lib/reject-worker-calls.js +42 -0
- package/lib/routes-ui.js +37 -8778
- package/lib/schemas.js +26 -1
- package/lib/tools.js +73 -0
- package/lib/ui-routes/account-routes.js +40 -210
- package/lib/ui-routes/admin-config-routes.js +913 -487
- package/lib/ui-routes/admin-entities-routes.js +1 -0
- package/lib/ui-routes/auth-routes.js +1339 -0
- package/lib/ui-routes/dashboard-routes.js +188 -0
- package/lib/ui-routes/document-store-routes.js +800 -0
- package/lib/ui-routes/export-routes.js +217 -0
- package/lib/ui-routes/internals-routes.js +354 -0
- package/lib/ui-routes/network-config-routes.js +759 -0
- package/lib/ui-routes/{oauth-routes.js → oauth-config-routes.js} +371 -91
- package/lib/ui-routes/route-helpers.js +316 -0
- package/lib/ui-routes/smtp-test-routes.js +236 -0
- package/lib/ui-routes/unsubscribe-routes.js +234 -0
- package/lib/webhook-request.js +36 -0
- package/package.json +17 -17
- package/sbom.json +1 -1
- package/server.js +217 -19
- package/static/licenses.html +52 -182
- package/translations/messages.pot +131 -151
- package/views/dashboard.hbs +7 -26
- package/views/internals/index.hbs +15 -0
- package/views/tokens/index.hbs +9 -0
- package/workers/api.js +198 -4401
- package/workers/export.js +87 -54
- package/workers/imap.js +29 -13
- package/workers/submit.js +20 -11
- package/workers/webhooks.js +6 -20
package/lib/schemas.js
CHANGED
|
@@ -1285,6 +1285,23 @@ const searchSchema = Joi.object({
|
|
|
1285
1285
|
|
|
1286
1286
|
gmailRaw: Joi.string().max(1024).example('has:attachment in:unread').description('Gmail search syntax (only works with Gmail accounts)'),
|
|
1287
1287
|
|
|
1288
|
+
labels: Joi.object({
|
|
1289
|
+
has: Joi.array()
|
|
1290
|
+
.items(Joi.string().max(128))
|
|
1291
|
+
.single()
|
|
1292
|
+
.description('Match messages that contain ALL of these labels/categories. Supported for Gmail and MS Graph (Outlook) accounts only.')
|
|
1293
|
+
.example(['Horizon'])
|
|
1294
|
+
.label('HasLabels'),
|
|
1295
|
+
not: Joi.array()
|
|
1296
|
+
.items(Joi.string().max(128))
|
|
1297
|
+
.single()
|
|
1298
|
+
.description('Match messages that contain NONE of these labels/categories. Supported for Gmail and MS Graph (Outlook) accounts only.')
|
|
1299
|
+
.example(['Horizon'])
|
|
1300
|
+
.label('NotLabels')
|
|
1301
|
+
})
|
|
1302
|
+
.description('Filter by Gmail labels or MS Graph (Outlook) categories. Not supported for generic IMAP accounts.')
|
|
1303
|
+
.label('LabelFilter'),
|
|
1304
|
+
|
|
1288
1305
|
emailIds: Joi.array()
|
|
1289
1306
|
.items(emailIdSchema)
|
|
1290
1307
|
.single()
|
|
@@ -1855,6 +1872,13 @@ const ipSchema = Joi.string()
|
|
|
1855
1872
|
})
|
|
1856
1873
|
.example('127.0.0.1');
|
|
1857
1874
|
|
|
1875
|
+
const tokenIdSchema = Joi.string()
|
|
1876
|
+
.length(64)
|
|
1877
|
+
.hex()
|
|
1878
|
+
.example('1bc12baf7f0d5e51fe0a4e0eda06e1be5b8d6cc2c66b95dc0fbe4d2e9f5d5e1a')
|
|
1879
|
+
.description('Token identifier (SHA-256 hash of the token)')
|
|
1880
|
+
.label('TokenId');
|
|
1881
|
+
|
|
1858
1882
|
const accountCountersSchema = Joi.object({
|
|
1859
1883
|
events: Joi.object()
|
|
1860
1884
|
.unknown()
|
|
@@ -2011,7 +2035,7 @@ const exportRequestSchema = Joi.object({
|
|
|
2011
2035
|
.valid('plain', 'html', '*')
|
|
2012
2036
|
.default('*')
|
|
2013
2037
|
.example('*')
|
|
2014
|
-
.description('Text content to include: "plain", "html", "*" (both)
|
|
2038
|
+
.description('Text content to include: "plain", "html", or "*" (both)')
|
|
2015
2039
|
.label('ExportTextType'),
|
|
2016
2040
|
maxBytes: Joi.number()
|
|
2017
2041
|
.integer()
|
|
@@ -2117,6 +2141,7 @@ module.exports = {
|
|
|
2117
2141
|
tokenRestrictionsSchema,
|
|
2118
2142
|
accountIdSchema,
|
|
2119
2143
|
ipSchema,
|
|
2144
|
+
tokenIdSchema,
|
|
2120
2145
|
accountCountersSchema,
|
|
2121
2146
|
accountPathSchema,
|
|
2122
2147
|
messageSpecialUseSchema,
|
package/lib/tools.js
CHANGED
|
@@ -145,6 +145,14 @@ async function reloadHttpProxyAgent() {
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
// Reload the shared HTTP proxy agent, but only when proxy-related settings actually changed.
|
|
149
|
+
// Centralizes the 'settings' message handling shared by the main thread and every worker.
|
|
150
|
+
function maybeReloadHttpProxyAgent(data) {
|
|
151
|
+
if (data && ('httpProxyEnabled' in data || 'httpProxyUrl' in data)) {
|
|
152
|
+
reloadHttpProxyAgent().catch(err => logger.error({ msg: 'Failed to reload HTTP proxy agent', err }));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
148
156
|
async function _doReloadHttpProxyAgent() {
|
|
149
157
|
let enabled, proxyUrl;
|
|
150
158
|
try {
|
|
@@ -1387,6 +1395,9 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV3QUiYsp13nD9suD1/ZkEXnuMoSg
|
|
|
1387
1395
|
let redisSoftware;
|
|
1388
1396
|
let redisCluster = false;
|
|
1389
1397
|
|
|
1398
|
+
// Consolidated list of Redis health issues, rendered as dashboard warning banners
|
|
1399
|
+
let redisWarnings = [];
|
|
1400
|
+
|
|
1390
1401
|
try {
|
|
1391
1402
|
let redisInfo = await module.exports.getRedisStats(redis);
|
|
1392
1403
|
|
|
@@ -1434,6 +1445,61 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV3QUiYsp13nD9suD1/ZkEXnuMoSg
|
|
|
1434
1445
|
softwareDetails = `Amazon MemoryDB`;
|
|
1435
1446
|
redisSoftware = 'memorydb';
|
|
1436
1447
|
}
|
|
1448
|
+
|
|
1449
|
+
// Incompatible backend: Amazon ElastiCache
|
|
1450
|
+
if (redisSoftware === 'elasticache') {
|
|
1451
|
+
redisWarnings.push({
|
|
1452
|
+
key: 'elasticache',
|
|
1453
|
+
color: 'danger',
|
|
1454
|
+
title: 'Redis compatibility warning',
|
|
1455
|
+
details: [
|
|
1456
|
+
'EmailEngine is incompatible with Amazon ElastiCache as the database backend.',
|
|
1457
|
+
'Please switch to a standard Redis instance. Using ElastiCache with EmailEngine can result in data loss and some features may not work correctly.'
|
|
1458
|
+
]
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// Incompatible backend: Redis Cluster
|
|
1463
|
+
if (redisCluster) {
|
|
1464
|
+
redisWarnings.push({
|
|
1465
|
+
key: 'cluster',
|
|
1466
|
+
color: 'danger',
|
|
1467
|
+
title: 'Redis compatibility warning',
|
|
1468
|
+
details: ['EmailEngine is incompatible with a Redis Cluster setup.', 'Please switch to a standard Redis primary instance.']
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// EmailEngine uses Redis as a database, not a cache. Any eviction silently drops sync state,
|
|
1473
|
+
// which makes already-synced messages look new and triggers duplicate webhooks.
|
|
1474
|
+
let maxmemoryPolicy = typeof redisInfo.maxmemory_policy === 'string' ? redisInfo.maxmemory_policy : null;
|
|
1475
|
+
let maxmemory = Number(redisInfo.maxmemory) || 0;
|
|
1476
|
+
let evictedKeys = Number(redisInfo.evicted_keys) || 0;
|
|
1477
|
+
|
|
1478
|
+
if (evictedKeys > 0) {
|
|
1479
|
+
// Definitive: Redis has already evicted keys
|
|
1480
|
+
redisWarnings.push({
|
|
1481
|
+
key: 'evicted-keys',
|
|
1482
|
+
color: 'danger',
|
|
1483
|
+
title: 'Redis eviction detected',
|
|
1484
|
+
details: [
|
|
1485
|
+
`Redis has evicted ${evictedKeys.toLocaleString('en-US')} key${evictedKeys === 1 ? '' : 's'} after reaching its memory limit.`,
|
|
1486
|
+
'EmailEngine uses Redis as its primary database, not a cache. Evicted keys cause already-synced messages to be reprocessed - generating duplicate webhooks - and can lead to data loss.',
|
|
1487
|
+
'Set the Redis "maxmemory-policy" to "noeviction" and provision enough memory so Redis never reaches its limit.'
|
|
1488
|
+
]
|
|
1489
|
+
});
|
|
1490
|
+
} else if (maxmemoryPolicy && maxmemoryPolicy !== 'noeviction') {
|
|
1491
|
+
// Latent risk: an eviction policy is configured but nothing has been evicted yet
|
|
1492
|
+
redisWarnings.push({
|
|
1493
|
+
key: 'maxmemory-policy',
|
|
1494
|
+
color: maxmemory > 0 ? 'danger' : 'warning',
|
|
1495
|
+
title: 'Unsafe Redis eviction policy',
|
|
1496
|
+
details: [
|
|
1497
|
+
`The Redis "maxmemory-policy" is set to "${maxmemoryPolicy}", but EmailEngine requires "noeviction".`,
|
|
1498
|
+
'With any other policy, Redis can delete EmailEngine sync state under memory pressure, causing already-synced messages to be reprocessed - generating duplicate webhooks - and possible data loss.',
|
|
1499
|
+
'Set "maxmemory-policy noeviction" and make sure Redis has enough memory for your workload.'
|
|
1500
|
+
]
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1437
1503
|
} catch (err) {
|
|
1438
1504
|
logger.error({ msg: 'Failed to get stats', err });
|
|
1439
1505
|
redisVersion = err.message;
|
|
@@ -1477,6 +1543,7 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV3QUiYsp13nD9suD1/ZkEXnuMoSg
|
|
|
1477
1543
|
redis: `${redisVersion}${softwareDetails ? ` (${softwareDetails})` : ''}`,
|
|
1478
1544
|
redisSoftware,
|
|
1479
1545
|
redisCluster,
|
|
1546
|
+
redisWarnings,
|
|
1480
1547
|
imapflow: ImapFlow.version || 'please upgrade',
|
|
1481
1548
|
bullmq: bullmqPackage.version,
|
|
1482
1549
|
arch: process.arch,
|
|
@@ -1571,6 +1638,11 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV3QUiYsp13nD9suD1/ZkEXnuMoSg
|
|
|
1571
1638
|
|
|
1572
1639
|
mergeObjects(destination, source) {
|
|
1573
1640
|
for (let propKey of Object.keys(source)) {
|
|
1641
|
+
// Guard against prototype pollution from crafted keys in parsed JSON
|
|
1642
|
+
if (propKey === '__proto__' || propKey === 'constructor' || propKey === 'prototype') {
|
|
1643
|
+
continue;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1574
1646
|
let sourceVal = source[propKey];
|
|
1575
1647
|
|
|
1576
1648
|
if (typeof destination[propKey] === 'undefined') {
|
|
@@ -1958,6 +2030,7 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV3QUiYsp13nD9suD1/ZkEXnuMoSg
|
|
|
1958
2030
|
|
|
1959
2031
|
httpAgent,
|
|
1960
2032
|
reloadHttpProxyAgent,
|
|
2033
|
+
maybeReloadHttpProxyAgent,
|
|
1961
2034
|
createSocksAgent,
|
|
1962
2035
|
|
|
1963
2036
|
get fetchAgent() {
|
|
@@ -1,205 +1,45 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// NB! This file is processed by the gettext parser (npm run gettext) and can not use newer syntax like ?.
|
|
4
|
+
|
|
5
|
+
// Admin UI routes for account management: /admin/accounts (listing), /admin/accounts/new and
|
|
6
|
+
// /accounts/new* (the add-account wizard incl. IMAP autoconfig/test/server steps), and
|
|
7
|
+
// /admin/accounts/{account}* (view, edit, delete, reconnect, sync, logs, browse). Extracted
|
|
8
|
+
// verbatim from lib/routes-ui.js. DISABLE_MESSAGE_BROWSER and the getMailboxListing helper move
|
|
9
|
+
// with the routes (only these account pages use them).
|
|
10
|
+
|
|
4
11
|
const Joi = require('joi');
|
|
5
12
|
const crypto = require('crypto');
|
|
6
|
-
const
|
|
7
|
-
const psl = require('psl');
|
|
13
|
+
const Boom = require('@hapi/boom');
|
|
8
14
|
|
|
9
15
|
const settings = require('../settings');
|
|
16
|
+
const consts = require('../consts');
|
|
10
17
|
const tokens = require('../tokens');
|
|
11
18
|
const { redis, documentsQueue } = require('../db');
|
|
19
|
+
const getSecret = require('../get-secret');
|
|
20
|
+
const capa = require('../capa');
|
|
21
|
+
const { Account } = require('../account');
|
|
22
|
+
const { Gateway } = require('../gateway');
|
|
23
|
+
const { autodetectImapSettings } = require('../autodetect-imap-settings');
|
|
24
|
+
const { oauth2Apps, oauth2ProviderData, SERVICE_ACCOUNT_PROVIDERS } = require('../oauth2-apps');
|
|
12
25
|
const {
|
|
13
|
-
failAction,
|
|
14
|
-
verifyAccountInfo,
|
|
15
|
-
getLogs,
|
|
16
|
-
flattenObjectKeys,
|
|
17
|
-
getSignedFormData,
|
|
18
26
|
getServiceHostname,
|
|
27
|
+
getSignedFormData,
|
|
19
28
|
parseSignedFormData,
|
|
29
|
+
getLogs,
|
|
30
|
+
verifyAccountInfo,
|
|
31
|
+
flattenObjectKeys,
|
|
20
32
|
getBoolean,
|
|
21
|
-
readEnvValue
|
|
33
|
+
readEnvValue,
|
|
34
|
+
failAction
|
|
22
35
|
} = require('../tools');
|
|
23
|
-
const { Account } = require('../account');
|
|
24
|
-
const { Gateway } = require('../gateway');
|
|
25
|
-
const { oauth2Apps, oauth2ProviderData, SERVICE_ACCOUNT_PROVIDERS } = 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
36
|
const { settingsSchema, accountIdSchema, defaultAccountTypeSchema } = require('../schemas');
|
|
31
|
-
const
|
|
32
|
-
const pathlib = require('path');
|
|
37
|
+
const { formatAccountData, cachedTemplates } = require('./route-helpers');
|
|
33
38
|
|
|
34
|
-
const {
|
|
39
|
+
const { REDIS_PREFIX, DEFAULT_PAGE_SIZE, NONCE_BYTES, MAX_FORM_TTL, DEFAULT_MAX_LOG_LINES } = consts;
|
|
35
40
|
|
|
36
41
|
const DISABLE_MESSAGE_BROWSER = getBoolean(readEnvValue('EENGINE_DISABLE_MESSAGE_BROWSER'));
|
|
37
42
|
|
|
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
|
-
// Check if IMAP was disabled due to errors - override state label to show error
|
|
163
|
-
if (account.imap && account.imap.disabled && account.lastErrorState) {
|
|
164
|
-
account.stateLabel = {
|
|
165
|
-
type: 'danger',
|
|
166
|
-
name: 'Failed',
|
|
167
|
-
error: account.lastErrorState.description || account.lastErrorState.response
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (account.oauth2) {
|
|
172
|
-
account.oauth2.scopes = []
|
|
173
|
-
.concat(account.oauth2.scope || [])
|
|
174
|
-
.concat(account.oauth2.scopes || [])
|
|
175
|
-
.flatMap(entry => entry.split(/\s+/))
|
|
176
|
-
.map(entry => entry.trim())
|
|
177
|
-
.filter(entry => entry);
|
|
178
|
-
|
|
179
|
-
account.oauth2.expiresStr = account.oauth2.expires ? account.oauth2.expires.toISOString() : false;
|
|
180
|
-
account.oauth2.generatedStr = account.oauth2.generated ? account.oauth2.generated.toISOString() : false;
|
|
181
|
-
|
|
182
|
-
if (account.outlookSubscription) {
|
|
183
|
-
account.outlookSubscription.subscriptionExpiresStr = account.outlookSubscription.expirationDateTime
|
|
184
|
-
? account.outlookSubscription.expirationDateTime.toISOString()
|
|
185
|
-
: false;
|
|
186
|
-
|
|
187
|
-
let state = account.outlookSubscription.state || {};
|
|
188
|
-
|
|
189
|
-
account.outlookSubscription.isValid =
|
|
190
|
-
state.state !== 'error' && account.outlookSubscription.expirationDateTime && account.outlookSubscription.expirationDateTime > new Date();
|
|
191
|
-
|
|
192
|
-
account.outlookSubscription.stateLabel = (state.state || '').replace(/^./, c => c.toUpperCase());
|
|
193
|
-
|
|
194
|
-
if ((state.state === 'created' && !account.outlookSubscription.expirationDateTime) || account.outlookSubscription.expirationDateTime < new Date()) {
|
|
195
|
-
account.outlookSubscription.stateLabel = 'Expired';
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return account;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
43
|
async function getMailboxListing(accountObject) {
|
|
204
44
|
let mailboxes = [
|
|
205
45
|
{
|
|
@@ -229,7 +69,7 @@ async function getMailboxListing(accountObject) {
|
|
|
229
69
|
return a.path.toLowerCase().localeCompare(b.path.toLowerCase());
|
|
230
70
|
});
|
|
231
71
|
} catch (err) {
|
|
232
|
-
//
|
|
72
|
+
// failed to get mailbox list
|
|
233
73
|
}
|
|
234
74
|
|
|
235
75
|
return mailboxes;
|
|
@@ -238,7 +78,6 @@ async function getMailboxListing(accountObject) {
|
|
|
238
78
|
function init(args) {
|
|
239
79
|
const { server, call } = args;
|
|
240
80
|
|
|
241
|
-
// Account listing route
|
|
242
81
|
server.route({
|
|
243
82
|
method: 'GET',
|
|
244
83
|
path: '/admin/accounts',
|
|
@@ -256,14 +95,21 @@ function init(args) {
|
|
|
256
95
|
}
|
|
257
96
|
|
|
258
97
|
for (let account of accounts.accounts) {
|
|
259
|
-
let
|
|
260
|
-
|
|
98
|
+
let accountObject = new Account({ redis, account: account.account });
|
|
99
|
+
try {
|
|
100
|
+
account.data = await accountObject.loadAccountData(null, null, runIndex);
|
|
261
101
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
102
|
+
if (account.data && account.data.oauth2 && account.data.oauth2.provider) {
|
|
103
|
+
let oauth2App = await oauth2Apps.get(account.data.oauth2.provider);
|
|
104
|
+
if (oauth2App) {
|
|
105
|
+
account.data.oauth2.app = oauth2App;
|
|
106
|
+
}
|
|
266
107
|
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
// Account has invalid config (e.g., broken delegation)
|
|
110
|
+
account.data = {
|
|
111
|
+
delegationError: err.message
|
|
112
|
+
};
|
|
267
113
|
}
|
|
268
114
|
}
|
|
269
115
|
|
|
@@ -365,7 +211,7 @@ function init(args) {
|
|
|
365
211
|
selectedState: stateOptions.find(entry => entry.state && entry.state === request.query.state),
|
|
366
212
|
|
|
367
213
|
searchTarget: '/admin/accounts',
|
|
368
|
-
searchPlaceholder: 'Search for accounts
|
|
214
|
+
searchPlaceholder: 'Search for accounts…',
|
|
369
215
|
|
|
370
216
|
showPaging: accounts.pages > 1,
|
|
371
217
|
nextPage,
|
|
@@ -415,7 +261,6 @@ function init(args) {
|
|
|
415
261
|
}
|
|
416
262
|
});
|
|
417
263
|
|
|
418
|
-
// New account POST handler
|
|
419
264
|
server.route({
|
|
420
265
|
method: 'POST',
|
|
421
266
|
path: '/admin/accounts/new',
|
|
@@ -566,7 +411,6 @@ function init(args) {
|
|
|
566
411
|
);
|
|
567
412
|
}
|
|
568
413
|
|
|
569
|
-
// Public GET new account form
|
|
570
414
|
server.route({
|
|
571
415
|
method: 'GET',
|
|
572
416
|
path: '/accounts/new',
|
|
@@ -628,7 +472,6 @@ function init(args) {
|
|
|
628
472
|
}
|
|
629
473
|
});
|
|
630
474
|
|
|
631
|
-
// Public POST new account form
|
|
632
475
|
server.route({
|
|
633
476
|
method: 'POST',
|
|
634
477
|
path: '/accounts/new',
|
|
@@ -669,7 +512,6 @@ function init(args) {
|
|
|
669
512
|
}
|
|
670
513
|
});
|
|
671
514
|
|
|
672
|
-
// IMAP account setup form
|
|
673
515
|
server.route({
|
|
674
516
|
method: 'POST',
|
|
675
517
|
path: '/accounts/new/imap',
|
|
@@ -772,7 +614,6 @@ function init(args) {
|
|
|
772
614
|
}
|
|
773
615
|
});
|
|
774
616
|
|
|
775
|
-
// Test IMAP settings
|
|
776
617
|
server.route({
|
|
777
618
|
method: 'POST',
|
|
778
619
|
path: '/accounts/new/imap/test',
|
|
@@ -924,7 +765,6 @@ function init(args) {
|
|
|
924
765
|
}
|
|
925
766
|
});
|
|
926
767
|
|
|
927
|
-
// Submit IMAP server settings
|
|
928
768
|
server.route({
|
|
929
769
|
method: 'POST',
|
|
930
770
|
path: '/accounts/new/imap/server',
|
|
@@ -1097,7 +937,6 @@ function init(args) {
|
|
|
1097
937
|
}
|
|
1098
938
|
});
|
|
1099
939
|
|
|
1100
|
-
// View account details
|
|
1101
940
|
server.route({
|
|
1102
941
|
method: 'GET',
|
|
1103
942
|
path: '/admin/accounts/{account}',
|
|
@@ -1327,7 +1166,6 @@ function init(args) {
|
|
|
1327
1166
|
}
|
|
1328
1167
|
});
|
|
1329
1168
|
|
|
1330
|
-
// Delete account
|
|
1331
1169
|
server.route({
|
|
1332
1170
|
method: 'POST',
|
|
1333
1171
|
path: '/admin/accounts/{account}/delete',
|
|
@@ -1369,7 +1207,6 @@ function init(args) {
|
|
|
1369
1207
|
}
|
|
1370
1208
|
});
|
|
1371
1209
|
|
|
1372
|
-
// Reconnect account
|
|
1373
1210
|
server.route({
|
|
1374
1211
|
method: 'POST',
|
|
1375
1212
|
path: '/admin/accounts/{account}/reconnect',
|
|
@@ -1408,7 +1245,6 @@ function init(args) {
|
|
|
1408
1245
|
}
|
|
1409
1246
|
});
|
|
1410
1247
|
|
|
1411
|
-
// Sync account
|
|
1412
1248
|
server.route({
|
|
1413
1249
|
method: 'POST',
|
|
1414
1250
|
path: '/admin/accounts/{account}/sync',
|
|
@@ -1447,7 +1283,6 @@ function init(args) {
|
|
|
1447
1283
|
}
|
|
1448
1284
|
});
|
|
1449
1285
|
|
|
1450
|
-
// Toggle account logs
|
|
1451
1286
|
server.route({
|
|
1452
1287
|
method: 'POST',
|
|
1453
1288
|
path: '/admin/accounts/{account}/logs',
|
|
@@ -1495,7 +1330,6 @@ function init(args) {
|
|
|
1495
1330
|
}
|
|
1496
1331
|
});
|
|
1497
1332
|
|
|
1498
|
-
// Flush account logs
|
|
1499
1333
|
server.route({
|
|
1500
1334
|
method: 'POST',
|
|
1501
1335
|
path: '/admin/accounts/{account}/logs-flush',
|
|
@@ -1532,7 +1366,6 @@ function init(args) {
|
|
|
1532
1366
|
}
|
|
1533
1367
|
});
|
|
1534
1368
|
|
|
1535
|
-
// Get account logs as text
|
|
1536
1369
|
server.route({
|
|
1537
1370
|
method: 'GET',
|
|
1538
1371
|
path: '/admin/accounts/{account}/logs.txt',
|
|
@@ -1556,7 +1389,6 @@ function init(args) {
|
|
|
1556
1389
|
}
|
|
1557
1390
|
});
|
|
1558
1391
|
|
|
1559
|
-
// Browse account messages
|
|
1560
1392
|
server.route({
|
|
1561
1393
|
method: 'GET',
|
|
1562
1394
|
path: '/admin/accounts/{account}/browse',
|
|
@@ -1573,7 +1405,7 @@ function init(args) {
|
|
|
1573
1405
|
if (request.cookieAuth) {
|
|
1574
1406
|
request.cookieAuth.clear();
|
|
1575
1407
|
}
|
|
1576
|
-
await request.flash({ type: 'info', message: `Sign in again to continue
|
|
1408
|
+
await request.flash({ type: 'info', message: `Sign in again to continue.` });
|
|
1577
1409
|
return h.redirect('/admin/login?next=' + encodeURIComponent('/admin/accounts/{account}/browse'));
|
|
1578
1410
|
}
|
|
1579
1411
|
|
|
@@ -1635,7 +1467,6 @@ function init(args) {
|
|
|
1635
1467
|
}
|
|
1636
1468
|
});
|
|
1637
1469
|
|
|
1638
|
-
// Edit account (GET)
|
|
1639
1470
|
server.route({
|
|
1640
1471
|
method: 'GET',
|
|
1641
1472
|
path: '/admin/accounts/{account}/edit',
|
|
@@ -1715,7 +1546,6 @@ function init(args) {
|
|
|
1715
1546
|
}
|
|
1716
1547
|
});
|
|
1717
1548
|
|
|
1718
|
-
// Edit account (POST)
|
|
1719
1549
|
server.route({
|
|
1720
1550
|
method: 'POST',
|
|
1721
1551
|
path: '/admin/accounts/{account}/edit',
|