emailengine-app 2.69.0 → 2.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/.github/workflows/deploy.yml +6 -3
  2. package/.github/workflows/release.yaml +2 -0
  3. package/CHANGELOG.md +19 -0
  4. package/Gruntfile.js +3 -1
  5. package/data/google-crawlers.json +1 -1
  6. package/getswagger.sh +40 -4
  7. package/gettext-extract.js +163 -0
  8. package/lib/account.js +73 -47
  9. package/lib/api-routes/account-routes.js +231 -71
  10. package/lib/api-routes/blocklist-routes.js +25 -18
  11. package/lib/api-routes/chat-routes.js +32 -14
  12. package/lib/api-routes/delivery-test-routes.js +30 -5
  13. package/lib/api-routes/export-routes.js +27 -2
  14. package/lib/api-routes/gateway-routes.js +63 -12
  15. package/lib/api-routes/license-routes.js +18 -4
  16. package/lib/api-routes/mailbox-routes.js +33 -7
  17. package/lib/api-routes/message-routes.js +200 -58
  18. package/lib/api-routes/oauth2-app-routes.js +90 -24
  19. package/lib/api-routes/outbox-routes.js +16 -4
  20. package/lib/api-routes/pubsub-routes.js +8 -4
  21. package/lib/api-routes/route-helpers.js +14 -1
  22. package/lib/api-routes/settings-routes.js +51 -25
  23. package/lib/api-routes/stats-routes.js +37 -3
  24. package/lib/api-routes/submit-routes.js +31 -42
  25. package/lib/api-routes/template-routes.js +54 -21
  26. package/lib/api-routes/token-routes.js +67 -67
  27. package/lib/api-routes/webhook-route-routes.js +37 -8
  28. package/lib/autodetect-imap-settings.js +0 -2
  29. package/lib/consts.js +5 -0
  30. package/lib/email-client/base-client.js +28 -6
  31. package/lib/email-client/gmail-client.js +119 -112
  32. package/lib/email-client/imap/subconnection.js +0 -1
  33. package/lib/email-client/imap/sync-operations.js +1 -1
  34. package/lib/email-client/imap-client.js +36 -17
  35. package/lib/email-client/notification-handler.js +1 -4
  36. package/lib/email-client/outlook-client.js +49 -62
  37. package/lib/export.js +37 -1
  38. package/lib/feature-flags.js +2 -2
  39. package/lib/gateway.js +4 -9
  40. package/lib/get-raw-email.js +5 -5
  41. package/lib/imapproxy/imap-core/lib/imap-connection.js +0 -1
  42. package/lib/logger.js +24 -21
  43. package/lib/metrics-collector.js +0 -2
  44. package/lib/oauth2-apps.js +13 -4
  45. package/lib/outbox.js +24 -40
  46. package/lib/redis-operations.js +1 -1
  47. package/lib/schemas.js +403 -83
  48. package/lib/sentry.js +139 -0
  49. package/lib/settings.js +9 -3
  50. package/lib/stream-encrypt.js +1 -1
  51. package/lib/templates.js +1 -1
  52. package/lib/tokens.js +5 -3
  53. package/lib/tools.js +2 -4
  54. package/lib/ui-routes/account-routes.js +7 -4
  55. package/lib/ui-routes/admin-config-routes.js +16 -3
  56. package/lib/ui-routes/oauth-config-routes.js +0 -2
  57. package/lib/ui-routes/route-helpers.js +0 -2
  58. package/lib/ui-routes/unsubscribe-routes.js +0 -2
  59. package/lib/webhooks.js +8 -4
  60. package/package.json +9 -8
  61. package/sbom.json +1 -1
  62. package/server.js +8 -23
  63. package/static/licenses.html +152 -292
  64. package/translations/messages.pot +122 -122
  65. package/update-info.sh +19 -1
  66. package/views/config/logging.hbs +48 -0
  67. package/workers/api.js +11 -32
  68. package/workers/documents.js +2 -22
  69. package/workers/export.js +16 -50
  70. package/workers/imap-proxy.js +3 -23
  71. package/workers/imap.js +2 -22
  72. package/workers/smtp.js +2 -22
  73. package/workers/submit.js +6 -24
  74. package/workers/webhooks.js +2 -22
@@ -41,7 +41,7 @@ jobs:
41
41
  id: deploy
42
42
  run: |
43
43
  echo $GITHUB_SHA > commit.txt
44
- ./update-info.sh
44
+ EE_COMMIT_HASH=$GITHUB_SHA ./update-info.sh
45
45
  npm install --omit=dev
46
46
  tar czf /tmp/${SERVICE_NAME}.tar.gz --exclude .git .
47
47
  scp -o "UserKnownHostsFile=/dev/null" -o "StrictHostKeyChecking=no" /tmp/${SERVICE_NAME}.tar.gz deploy@${TARGET_HOST_02}:
@@ -55,7 +55,7 @@ jobs:
55
55
  id: deploy-demo
56
56
  run: |
57
57
  echo $GITHUB_SHA > commit.txt
58
- ./update-info.sh
58
+ EE_COMMIT_HASH=$GITHUB_SHA ./update-info.sh
59
59
  npm install --omit=dev
60
60
  tar czf /tmp/${SERVICE_NAME}.tar.gz --exclude .git .
61
61
  scp -o "UserKnownHostsFile=/dev/null" -o "StrictHostKeyChecking=no" /tmp/${SERVICE_NAME}.tar.gz deploy@${TARGET_HOST_02}:
@@ -104,8 +104,11 @@ jobs:
104
104
  images: |
105
105
  ${{ github.repository }}
106
106
  ghcr.io/${{ github.repository }}
107
+ # :latest is owned by the release workflow (release.yaml) and
108
+ # always points at the newest release; per-push dev builds from
109
+ # master are published as :edge instead
107
110
  tags: |
108
- type=raw,value=latest,enable=true
111
+ type=raw,value=edge,enable=true
109
112
 
110
113
  - name: Build and push
111
114
  uses: docker/build-push-action@v7
@@ -103,9 +103,11 @@ jobs:
103
103
  platforms: ${{ steps.buildx.outputs.platforms }}
104
104
  push: true
105
105
  tags: |
106
+ ${{ github.repository }}:latest
106
107
  ${{ github.repository }}:v${{needs.release_please.outputs.major}}.${{needs.release_please.outputs.minor}}.${{needs.release_please.outputs.patch}}
107
108
  ${{ github.repository }}:v${{needs.release_please.outputs.major}}.${{needs.release_please.outputs.minor}}
108
109
  ${{ github.repository }}:v${{needs.release_please.outputs.major}}
110
+ ghcr.io/${{ github.repository }}:latest
109
111
  ghcr.io/${{ github.repository }}:v${{needs.release_please.outputs.major}}.${{needs.release_please.outputs.minor}}.${{needs.release_please.outputs.patch}}
110
112
  ghcr.io/${{ github.repository }}:v${{needs.release_please.outputs.major}}.${{needs.release_please.outputs.minor}}
111
113
  ghcr.io/${{ github.repository }}:v${{needs.release_please.outputs.major}}
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.70.0](https://github.com/postalsys/emailengine/compare/v2.69.0...v2.70.0) (2026-06-11)
4
+
5
+
6
+ ### Features
7
+
8
+ * allow enabling Sentry error reporting from the admin UI ([a4076a4](https://github.com/postalsys/emailengine/commit/a4076a4fa6658ccc998f9bfe35dddf015cd12b09))
9
+ * replace Bugsnag with self-hosted Sentry for error tracking ([62de831](https://github.com/postalsys/emailengine/commit/62de831a2911da9b649fe693547ba09d70e2e481))
10
+ * tag Sentry error reports with instance id and license key ([2e9683f](https://github.com/postalsys/emailengine/commit/2e9683f27d54ff8fa7ba3f6f4453866fd135f173))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * align API response schemas with actual responses and fix uncovered API bugs ([abacbf6](https://github.com/postalsys/emailengine/commit/abacbf66f048a77d00d180a831a97594380d8ed9))
16
+ * align remaining API response schemas with actual responses ([06136ab](https://github.com/postalsys/emailengine/commit/06136abd20a97c45b25aac7f93653387eb55928a))
17
+ * assign unassigned accounts after license activation ([5677977](https://github.com/postalsys/emailengine/commit/56779778eba27f2d1604ff06f17e5736a5ddee61))
18
+ * do not crash at startup when a feature flag env variable is set to a falsy value ([98ad979](https://github.com/postalsys/emailengine/commit/98ad97988e752fad9e2ae11eff63f35ffcf84976))
19
+ * report correct nextAttempt time for queued messages ([d5ebdfb](https://github.com/postalsys/emailengine/commit/d5ebdfb029b3ed331a8239ace7426f4f0704ae2e))
20
+ * resolve code review findings in Gmail bulk ops, exports, webhooks, and schemas ([f0ddb46](https://github.com/postalsys/emailengine/commit/f0ddb4631fb924ddb93e0a99d0e20bc9f0cc8ee2))
21
+
3
22
  ## [2.69.0](https://github.com/postalsys/emailengine/compare/v2.68.1...v2.69.0) (2026-06-09)
4
23
 
5
24
 
package/Gruntfile.js CHANGED
@@ -15,7 +15,9 @@ module.exports = function (grunt) {
15
15
 
16
16
  shell: {
17
17
  eslint: {
18
- command: 'npx eslint lib/**/*.js workers/**/*.js server.js Gruntfile.js',
18
+ // Globs are quoted so eslint expands them itself - unquoted, sh (no globstar)
19
+ // expands lib/**/*.js to depth-2 files only and skips most of lib/
20
+ command: "npx eslint 'lib/**/*.js' 'workers/**/*.js' server.js Gruntfile.js",
19
21
  options: {
20
22
  async: false
21
23
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "creationTime": "2026-06-09T14:45:50.000000",
2
+ "creationTime": "2026-06-10T14:45:48.000000",
3
3
  "prefixes": [
4
4
  {
5
5
  "ipv6Prefix": "2001:4860:4801:2008::/64"
package/getswagger.sh CHANGED
@@ -1,8 +1,44 @@
1
1
  #!/bin/bash
2
2
 
3
+ set -e
4
+
3
5
  export EENGINE_PORT=5678
4
6
 
5
- npm start > /dev/null 2>&1 &
6
- sleep 5
7
- curl -s "http://127.0.0.1:${EENGINE_PORT}/swagger.json" > swagger.json
8
- pkill emailengine
7
+ # refuse to run if something is already listening on the port, otherwise the
8
+ # polling loop below would silently fetch swagger.json from a stale instance
9
+ if (exec 3<>"/dev/tcp/127.0.0.1/${EENGINE_PORT}") 2>/dev/null; then
10
+ echo "Port ${EENGINE_PORT} is already in use" >&2
11
+ exit 1
12
+ fi
13
+
14
+ node server.js > /dev/null 2>&1 &
15
+ SERVER_PID=$!
16
+
17
+ cleanup() {
18
+ kill "$SERVER_PID" 2>/dev/null || true
19
+ wait "$SERVER_PID" 2>/dev/null || true
20
+ }
21
+ trap cleanup EXIT
22
+
23
+ # poll until the API is up instead of relying on a fixed sleep
24
+ rm -f swagger.json.tmp
25
+ for i in $(seq 1 60); do
26
+ if curl -fs --max-time 5 "http://127.0.0.1:${EENGINE_PORT}/swagger.json" -o swagger.json.tmp; then
27
+ break
28
+ fi
29
+ if ! kill -0 "$SERVER_PID" 2>/dev/null; then
30
+ echo "Server exited before swagger.json could be fetched" >&2
31
+ exit 1
32
+ fi
33
+ sleep 1
34
+ done
35
+
36
+ if [ ! -s swagger.json.tmp ]; then
37
+ echo "Timed out waiting for swagger.json" >&2
38
+ exit 1
39
+ fi
40
+
41
+ # only replace swagger.json if the download parses as JSON
42
+ node -e 'JSON.parse(require("fs").readFileSync("swagger.json.tmp", "utf-8"))'
43
+
44
+ mv swagger.json.tmp swagger.json
@@ -0,0 +1,163 @@
1
+ 'use strict';
2
+
3
+ // Extracts translatable strings from JS sources into translations/messages.pot.
4
+ // Run via `npm run gettext` after xgettext-template has generated the POT from the
5
+ // Handlebars views - this script joins the strings found in JS files into that file.
6
+ //
7
+ // Replaces jsxgettext, which bundled acorn 5 and crashed on post-ES2018 syntax
8
+ // (optional chaining, nullish coalescing, etc.). Files are discovered dynamically,
9
+ // so new modules with gettext()/ngettext() calls are picked up automatically.
10
+
11
+ const { parse } = require('acorn');
12
+ const { full: walkFull } = require('acorn-walk');
13
+ const { po } = require('gettext-parser');
14
+ const fs = require('fs');
15
+ const Path = require('path');
16
+
17
+ const ROOT_DIR = __dirname;
18
+ const POT_PATH = Path.join(ROOT_DIR, 'translations', 'messages.pot');
19
+
20
+ // All server-side code that may contain gettext()/ngettext() calls
21
+ const SCAN_DIRS = ['bin', 'lib', 'workers'];
22
+ const SCAN_FILES = ['server.js'];
23
+
24
+ function listJsFiles(dir) {
25
+ return fs
26
+ .readdirSync(dir, { recursive: true })
27
+ .filter(entryPath => entryPath.endsWith('.js'))
28
+ .map(entryPath => Path.join(dir, entryPath));
29
+ }
30
+
31
+ // The full sorted scan set (sorted to keep POT reference output deterministic)
32
+ function listScanFiles() {
33
+ let files = SCAN_FILES.map(file => Path.join(ROOT_DIR, file));
34
+ for (let dir of SCAN_DIRS) {
35
+ files = files.concat(listJsFiles(Path.join(ROOT_DIR, dir)));
36
+ }
37
+ return files.sort();
38
+ }
39
+
40
+ function parseFile(filePath) {
41
+ const source = fs.readFileSync(filePath, 'utf-8');
42
+ try {
43
+ return parse(source, { ecmaVersion: 'latest', sourceType: 'script', locations: true, allowHashBang: true });
44
+ } catch (err) {
45
+ err.message = `Failed to parse ${filePath}: ${err.message}`;
46
+ throw err;
47
+ }
48
+ }
49
+
50
+ // Resolves a call argument into a string value. Handles string literals and
51
+ // concatenation of string literals ('foo' + 'bar'). Returns false for anything
52
+ // dynamic (identifiers, template literals with expressions, etc.) - such calls
53
+ // forward runtime values and do not define new translatable strings.
54
+ function resolveString(node) {
55
+ switch (node.type) {
56
+ case 'Literal':
57
+ return typeof node.value === 'string' ? node.value : false;
58
+ case 'TemplateLiteral':
59
+ return node.expressions.length === 0 ? node.quasis[0].value.cooked : false;
60
+ case 'BinaryExpression': {
61
+ if (node.operator !== '+') {
62
+ return false;
63
+ }
64
+ let left = resolveString(node.left);
65
+ let right = resolveString(node.right);
66
+ return left !== false && right !== false ? left + right : false;
67
+ }
68
+ default:
69
+ return false;
70
+ }
71
+ }
72
+
73
+ function calleeName(callee) {
74
+ if (callee.type === 'Identifier') {
75
+ return callee.name;
76
+ }
77
+ if (callee.type === 'MemberExpression' && !callee.computed && callee.property.type === 'Identifier') {
78
+ return callee.property.name;
79
+ }
80
+ return false;
81
+ }
82
+
83
+ function extractFromFile(filePath) {
84
+ const ast = parseFile(filePath);
85
+
86
+ let entries = [];
87
+ let reference = node => `${Path.relative(ROOT_DIR, filePath)}:${node.loc.start.line}`;
88
+
89
+ walkFull(ast, node => {
90
+ if (node.type !== 'CallExpression') {
91
+ return;
92
+ }
93
+
94
+ let name = calleeName(node.callee);
95
+
96
+ // An empty msgid is skipped (like xgettext does): translations[''] is the POT header
97
+ // entry in gettext-parser, so an empty key would corrupt the header block
98
+ if (name === 'gettext' && node.arguments.length >= 1) {
99
+ let msgid = resolveString(node.arguments[0]);
100
+ if (msgid !== false && msgid !== '') {
101
+ entries.push({ msgid, reference: reference(node) });
102
+ }
103
+ }
104
+
105
+ if (name === 'ngettext' && node.arguments.length >= 2) {
106
+ let msgid = resolveString(node.arguments[0]);
107
+ let msgidPlural = resolveString(node.arguments[1]);
108
+ if (msgid !== false && msgid !== '' && msgidPlural !== false) {
109
+ entries.push({ msgid, msgidPlural, reference: reference(node) });
110
+ }
111
+ }
112
+ });
113
+
114
+ return entries;
115
+ }
116
+
117
+ function main() {
118
+ let files = listScanFiles();
119
+
120
+ let extracted = [];
121
+ for (let filePath of files) {
122
+ extracted = extracted.concat(extractFromFile(filePath));
123
+ }
124
+
125
+ let pot = po.parse(fs.readFileSync(POT_PATH));
126
+ let translations = (pot.translations[''] = pot.translations[''] || {});
127
+
128
+ // xgettext-template does not stamp a creation date, so set it here (jsxgettext used to)
129
+ pot.headers = pot.headers || {};
130
+ pot.headers['POT-Creation-Date'] = new Date()
131
+ .toISOString()
132
+ .replace(/T/, ' ')
133
+ .replace(/:\d+\.\d+Z$/, '+0000');
134
+
135
+ for (let { msgid, msgidPlural, reference } of extracted) {
136
+ let entry = translations[msgid];
137
+ if (!entry) {
138
+ entry = translations[msgid] = { msgid, msgstr: [''] };
139
+ }
140
+
141
+ if (msgidPlural && !entry.msgid_plural) {
142
+ entry.msgid_plural = msgidPlural;
143
+ entry.msgstr = ['', ''];
144
+ }
145
+
146
+ entry.comments = entry.comments || {};
147
+ let references = entry.comments.reference ? entry.comments.reference.split('\n') : [];
148
+ if (!references.includes(reference)) {
149
+ references.push(reference);
150
+ }
151
+ entry.comments.reference = references.join('\n');
152
+ }
153
+
154
+ fs.writeFileSync(POT_PATH, po.compile(pot));
155
+ console.log(`Extracted ${extracted.length} gettext strings from ${files.length} JS files into ${Path.relative(ROOT_DIR, POT_PATH)}`);
156
+ }
157
+
158
+ if (require.main === module) {
159
+ main();
160
+ }
161
+
162
+ // Exported for the gettext coverage test, which walks the same scan set with the same helpers
163
+ module.exports = { listScanFiles, parseFile, calleeName, extractFromFile };
package/lib/account.js CHANGED
@@ -945,7 +945,7 @@ class Account {
945
945
  throw error;
946
946
  }
947
947
 
948
- let state = false;
948
+ let state;
949
949
  if (result[0][1] && result[0][1].account) {
950
950
  // existing user
951
951
  state = 'existing';
@@ -1267,16 +1267,7 @@ class Account {
1267
1267
  } catch (err) {
1268
1268
  // should not happen
1269
1269
  if (logger.notifyError) {
1270
- logger.notifyError(err, event => {
1271
- if (this.account) {
1272
- event.setUser(this.account);
1273
- }
1274
-
1275
- event.addMetadata('ee', {
1276
- path,
1277
- mailboxListing: typeof mailboxListing
1278
- });
1279
- });
1270
+ logger.notifyError(err, { user: this.account, meta: { path, mailboxListing: typeof mailboxListing } });
1280
1271
  }
1281
1272
 
1282
1273
  let message = 'Failed to process stored mailbox listing';
@@ -1290,22 +1281,44 @@ class Account {
1290
1281
  return mailboxes;
1291
1282
  }
1292
1283
 
1284
+ // Worker backends return false for entities they can not find (e.g. an unknown mailbox path
1285
+ // or message ID on IMAP). Convert such results into a 404 error for the API.
1286
+ assertFound(result, message, code) {
1287
+ if (!result) {
1288
+ let error = Boom.boomify(new Error(message), { statusCode: 404 });
1289
+ error.output.payload.code = code;
1290
+ throw error;
1291
+ }
1292
+ return result;
1293
+ }
1294
+
1295
+ assertMessageFound(result) {
1296
+ return this.assertFound(result, 'Requested message was not found', 'MessageNotFound');
1297
+ }
1298
+
1299
+ assertFolderFound(result) {
1300
+ return this.assertFound(result, 'Requested mailbox folder was not found', 'FolderNotFound');
1301
+ }
1302
+
1293
1303
  async updateMessage(message, updates) {
1294
1304
  await this.loadAccountData(this.account, true);
1295
1305
 
1296
- return await this.call({
1306
+ let result = await this.call({
1297
1307
  cmd: 'updateMessage',
1298
1308
  account: this.account,
1299
1309
  message,
1300
1310
  updates,
1301
1311
  timeout: this.timeout
1302
1312
  });
1313
+
1314
+ // IMAP backend returns false for unknown messages and folders
1315
+ return this.assertMessageFound(result);
1303
1316
  }
1304
1317
 
1305
1318
  async updateMessages(path, search, updates) {
1306
1319
  await this.loadAccountData(this.account, true);
1307
1320
 
1308
- return await this.call({
1321
+ let result = await this.call({
1309
1322
  cmd: 'updateMessages',
1310
1323
  account: this.account,
1311
1324
  path,
@@ -1313,6 +1326,9 @@ class Account {
1313
1326
  updates,
1314
1327
  timeout: this.timeout
1315
1328
  });
1329
+
1330
+ // IMAP backend returns false for unknown folders
1331
+ return this.assertFolderFound(result);
1316
1332
  }
1317
1333
 
1318
1334
  async listMailboxes(query) {
@@ -1335,7 +1351,8 @@ class Account {
1335
1351
 
1336
1352
  async moveMessage(message, target, options) {
1337
1353
  await this.loadAccountData(this.account, true);
1338
- return await this.call({
1354
+
1355
+ let result = await this.call({
1339
1356
  cmd: 'moveMessage',
1340
1357
  account: this.account,
1341
1358
  message,
@@ -1343,11 +1360,15 @@ class Account {
1343
1360
  options,
1344
1361
  timeout: this.timeout
1345
1362
  });
1363
+
1364
+ // IMAP backend returns false for unknown messages and folders
1365
+ return this.assertMessageFound(result);
1346
1366
  }
1347
1367
 
1348
1368
  async moveMessages(source, search, target) {
1349
1369
  await this.loadAccountData(this.account, true);
1350
- return await this.call({
1370
+
1371
+ let result = await this.call({
1351
1372
  cmd: 'moveMessages',
1352
1373
  account: this.account,
1353
1374
  source,
@@ -1355,24 +1376,30 @@ class Account {
1355
1376
  target,
1356
1377
  timeout: this.timeout
1357
1378
  });
1379
+
1380
+ // IMAP backend returns false for unknown folders
1381
+ return this.assertFolderFound(result);
1358
1382
  }
1359
1383
 
1360
1384
  async deleteMessage(message, force) {
1361
1385
  await this.loadAccountData(this.account, true);
1362
1386
 
1363
- return await this.call({
1387
+ let result = await this.call({
1364
1388
  cmd: 'deleteMessage',
1365
1389
  account: this.account,
1366
1390
  message,
1367
1391
  force,
1368
1392
  timeout: this.timeout
1369
1393
  });
1394
+
1395
+ // IMAP backend returns false for unknown messages and folders
1396
+ return this.assertMessageFound(result);
1370
1397
  }
1371
1398
 
1372
1399
  async deleteMessages(path, search, force) {
1373
1400
  await this.loadAccountData(this.account, true);
1374
1401
 
1375
- return await this.call({
1402
+ let result = await this.call({
1376
1403
  cmd: 'deleteMessages',
1377
1404
  account: this.account,
1378
1405
  path,
@@ -1380,6 +1407,9 @@ class Account {
1380
1407
  force,
1381
1408
  timeout: this.timeout
1382
1409
  });
1410
+
1411
+ // IMAP backend returns false for unknown folders
1412
+ return this.assertFolderFound(result);
1383
1413
  }
1384
1414
 
1385
1415
  async getQuota() {
@@ -1447,13 +1477,7 @@ class Account {
1447
1477
  results: getResult && getResult._source ? 1 : 0
1448
1478
  });
1449
1479
 
1450
- if (!getResult || !getResult._source) {
1451
- let message = 'Requested message was not found';
1452
- let error = Boom.boomify(new Error(message), { statusCode: 404 });
1453
- throw error;
1454
- }
1455
-
1456
- let messageData = getResult._source;
1480
+ let messageData = this.assertMessageFound(getResult && getResult._source);
1457
1481
  let response = {};
1458
1482
 
1459
1483
  response.hasMore = false;
@@ -1473,13 +1497,16 @@ class Account {
1473
1497
 
1474
1498
  await this.loadAccountData(this.account, true);
1475
1499
 
1476
- return await this.call({
1500
+ let textData = await this.call({
1477
1501
  cmd: 'getText',
1478
1502
  account: this.account,
1479
1503
  text,
1480
1504
  options,
1481
1505
  timeout: this.timeout
1482
1506
  });
1507
+
1508
+ // IMAP backend returns false for unknown messages and folders
1509
+ return this.assertMessageFound(textData);
1483
1510
  }
1484
1511
 
1485
1512
  async getMessage(message, options) {
@@ -1521,13 +1548,7 @@ class Account {
1521
1548
  results: getResult && getResult._source ? 1 : 0
1522
1549
  });
1523
1550
 
1524
- if (!getResult || !getResult._source) {
1525
- let message = 'Requested message was not found';
1526
- let error = Boom.boomify(new Error(message), { statusCode: 404 });
1527
- throw error;
1528
- }
1529
-
1530
- let messageData = getResult._source;
1551
+ let messageData = this.assertMessageFound(getResult && getResult._source);
1531
1552
 
1532
1553
  // restore headers and text object as per the API response
1533
1554
  let headersObj = {};
@@ -1632,6 +1653,9 @@ class Account {
1632
1653
 
1633
1654
  if (options.markAsSeen && (!messageData.flags || !messageData.flags.includes('\\Seen'))) {
1634
1655
  // mark message as seen
1656
+ if (!messageData.flags) {
1657
+ messageData.flags = [];
1658
+ }
1635
1659
  messageData.flags.push('\\Seen');
1636
1660
  // do not wait until the update is completed, return immediately
1637
1661
  this.updateMessage(message, { flags: { add: ['\\Seen'] } }).catch(err => {
@@ -1661,13 +1685,7 @@ class Account {
1661
1685
  timeout: this.timeout
1662
1686
  });
1663
1687
 
1664
- if (!messageData) {
1665
- let message = 'Requested message was not found';
1666
- let error = Boom.boomify(new Error(message), { statusCode: 404 });
1667
- throw error;
1668
- }
1669
-
1670
- return messageData;
1688
+ return this.assertMessageFound(messageData);
1671
1689
  }
1672
1690
 
1673
1691
  async getMessages(messageIds, options) {
@@ -1801,7 +1819,7 @@ class Account {
1801
1819
 
1802
1820
  await this.loadAccountData(this.account, true);
1803
1821
 
1804
- return await this.call(
1822
+ let listing = await this.call(
1805
1823
  Object.assign(
1806
1824
  {
1807
1825
  cmd: 'listMessages',
@@ -1811,6 +1829,9 @@ class Account {
1811
1829
  { timeout: this.timeout }
1812
1830
  )
1813
1831
  );
1832
+
1833
+ // IMAP and Gmail backends return false for unknown folders
1834
+ return this.assertFolderFound(listing);
1814
1835
  }
1815
1836
 
1816
1837
  async searchMessages(query, searchOpts) {
@@ -2086,11 +2107,11 @@ class Account {
2086
2107
  let sizeMatch = {};
2087
2108
 
2088
2109
  if (query.search.larger) {
2089
- dateMatch.gte = query.search.larger;
2110
+ sizeMatch.gte = query.search.larger;
2090
2111
  }
2091
2112
 
2092
2113
  if (query.search.smaller) {
2093
- dateMatch.lte = query.search.smaller;
2114
+ sizeMatch.lte = query.search.smaller;
2094
2115
  }
2095
2116
 
2096
2117
  if (Object.keys(sizeMatch).length) {
@@ -2198,7 +2219,7 @@ class Account {
2198
2219
 
2199
2220
  await this.loadAccountData(this.account, true);
2200
2221
 
2201
- return await this.call(
2222
+ let listing = await this.call(
2202
2223
  Object.assign(
2203
2224
  {
2204
2225
  cmd: 'listMessages',
@@ -2208,6 +2229,9 @@ class Account {
2208
2229
  { timeout: this.timeout }
2209
2230
  )
2210
2231
  );
2232
+
2233
+ // IMAP and Gmail backends return false for unknown folders
2234
+ return this.assertFolderFound(listing);
2211
2235
  }
2212
2236
 
2213
2237
  async uploadMessage(data) {
@@ -2322,7 +2346,7 @@ class Account {
2322
2346
  }
2323
2347
  } catch (err) {
2324
2348
  this.logger.error({ msg: 'Failed to get lock', lockKey, err });
2325
- if (Boom.isBoom) {
2349
+ if (Boom.isBoom(err)) {
2326
2350
  throw err;
2327
2351
  }
2328
2352
  let error = Boom.boomify(new Error('Failed to get flush lock, try again later'), { statusCode: 500 });
@@ -2381,7 +2405,9 @@ class Account {
2381
2405
  // Flush ElasticSearch index for this account
2382
2406
  const { index, client } = this.esClient;
2383
2407
  if (!client) {
2384
- return;
2408
+ // Account data in Redis was already flushed, only the index cleanup was skipped
2409
+ this.logger.error({ msg: 'Document store is enabled but the ElasticSearch client is not available', action: 'flush' });
2410
+ return true;
2385
2411
  }
2386
2412
 
2387
2413
  let deleteResult = {};
@@ -2592,7 +2618,7 @@ class Account {
2592
2618
  account: accountData.account,
2593
2619
  user: authData.user,
2594
2620
  accessToken: authData.accessToken,
2595
- provider: accountData.oauth2.auth.provider,
2621
+ provider: accountData.oauth2.provider,
2596
2622
  registeredScopes: accountData.oauth2.scope,
2597
2623
  cached: false
2598
2624
  };
@@ -2626,7 +2652,7 @@ class Account {
2626
2652
  account: accountData.account,
2627
2653
  user: accountData.oauth2.auth.user,
2628
2654
  accessToken,
2629
- provider: accountData.oauth2.auth.provider,
2655
+ provider: accountData.oauth2.provider,
2630
2656
  registeredScopes: accountData.oauth2.scope,
2631
2657
  expires:
2632
2658
  accountData.oauth2.expires && typeof accountData.oauth2.expires.toISOString === 'function'