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.
- package/.github/workflows/deploy.yml +6 -3
- package/.github/workflows/release.yaml +2 -0
- package/CHANGELOG.md +19 -0
- package/Gruntfile.js +3 -1
- package/data/google-crawlers.json +1 -1
- package/getswagger.sh +40 -4
- package/gettext-extract.js +163 -0
- package/lib/account.js +73 -47
- package/lib/api-routes/account-routes.js +231 -71
- package/lib/api-routes/blocklist-routes.js +25 -18
- package/lib/api-routes/chat-routes.js +32 -14
- package/lib/api-routes/delivery-test-routes.js +30 -5
- package/lib/api-routes/export-routes.js +27 -2
- package/lib/api-routes/gateway-routes.js +63 -12
- package/lib/api-routes/license-routes.js +18 -4
- package/lib/api-routes/mailbox-routes.js +33 -7
- package/lib/api-routes/message-routes.js +200 -58
- package/lib/api-routes/oauth2-app-routes.js +90 -24
- package/lib/api-routes/outbox-routes.js +16 -4
- package/lib/api-routes/pubsub-routes.js +8 -4
- package/lib/api-routes/route-helpers.js +14 -1
- package/lib/api-routes/settings-routes.js +51 -25
- package/lib/api-routes/stats-routes.js +37 -3
- package/lib/api-routes/submit-routes.js +31 -42
- package/lib/api-routes/template-routes.js +54 -21
- package/lib/api-routes/token-routes.js +67 -67
- package/lib/api-routes/webhook-route-routes.js +37 -8
- package/lib/autodetect-imap-settings.js +0 -2
- package/lib/consts.js +5 -0
- package/lib/email-client/base-client.js +28 -6
- package/lib/email-client/gmail-client.js +119 -112
- package/lib/email-client/imap/subconnection.js +0 -1
- package/lib/email-client/imap/sync-operations.js +1 -1
- package/lib/email-client/imap-client.js +36 -17
- package/lib/email-client/notification-handler.js +1 -4
- package/lib/email-client/outlook-client.js +49 -62
- package/lib/export.js +37 -1
- package/lib/feature-flags.js +2 -2
- package/lib/gateway.js +4 -9
- package/lib/get-raw-email.js +5 -5
- package/lib/imapproxy/imap-core/lib/imap-connection.js +0 -1
- package/lib/logger.js +24 -21
- package/lib/metrics-collector.js +0 -2
- package/lib/oauth2-apps.js +13 -4
- package/lib/outbox.js +24 -40
- package/lib/redis-operations.js +1 -1
- package/lib/schemas.js +403 -83
- package/lib/sentry.js +139 -0
- package/lib/settings.js +9 -3
- package/lib/stream-encrypt.js +1 -1
- package/lib/templates.js +1 -1
- package/lib/tokens.js +5 -3
- package/lib/tools.js +2 -4
- package/lib/ui-routes/account-routes.js +7 -4
- package/lib/ui-routes/admin-config-routes.js +16 -3
- package/lib/ui-routes/oauth-config-routes.js +0 -2
- package/lib/ui-routes/route-helpers.js +0 -2
- package/lib/ui-routes/unsubscribe-routes.js +0 -2
- package/lib/webhooks.js +8 -4
- package/package.json +9 -8
- package/sbom.json +1 -1
- package/server.js +8 -23
- package/static/licenses.html +152 -292
- package/translations/messages.pot +122 -122
- package/update-info.sh +19 -1
- package/views/config/logging.hbs +48 -0
- package/workers/api.js +11 -32
- package/workers/documents.js +2 -22
- package/workers/export.js +16 -50
- package/workers/imap-proxy.js +3 -23
- package/workers/imap.js +2 -22
- package/workers/smtp.js +2 -22
- package/workers/submit.js +6 -24
- 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=
|
|
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
|
-
|
|
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
|
}
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2110
|
+
sizeMatch.gte = query.search.larger;
|
|
2090
2111
|
}
|
|
2091
2112
|
|
|
2092
2113
|
if (query.search.smaller) {
|
|
2093
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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'
|