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
package/test/api-test.js
DELETED
|
@@ -1,899 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
require('dotenv').config({ quiet: true });
|
|
4
|
-
|
|
5
|
-
const config = require('@zone-eu/wild-config');
|
|
6
|
-
const testConfig = require('./test-config');
|
|
7
|
-
const supertest = require('supertest');
|
|
8
|
-
const test = require('node:test');
|
|
9
|
-
const assert = require('node:assert').strict;
|
|
10
|
-
const nodemailer = require('nodemailer');
|
|
11
|
-
const Redis = require('ioredis');
|
|
12
|
-
const redis = new Redis(config.dbs.redis);
|
|
13
|
-
const webhooksServer = require('./webhooks-server');
|
|
14
|
-
|
|
15
|
-
const accessToken = '2aa97ad0456d6624a55d30780aa2ff61bfb7edc6fa00935b40814b271e718660';
|
|
16
|
-
|
|
17
|
-
const server = supertest.agent(`http://127.0.0.1:${config.api.port}`).auth(accessToken, { type: 'bearer' });
|
|
18
|
-
|
|
19
|
-
let testAccount;
|
|
20
|
-
const defaultAccountId = 'main-account';
|
|
21
|
-
const gmailAccountId1 = 'gmail-account1';
|
|
22
|
-
const gmailAccountId2 = 'gmail-account2';
|
|
23
|
-
const gmailSendOnlyAccountId = 'gmail-sendonly-account';
|
|
24
|
-
|
|
25
|
-
// Helper function for polling with timeout
|
|
26
|
-
async function waitForCondition(checkFn, options = {}) {
|
|
27
|
-
const { interval = testConfig.POLL_INTERVAL, timeout = testConfig.DEFAULT_TIMEOUT, message = 'Condition not met within timeout' } = options;
|
|
28
|
-
|
|
29
|
-
const startTime = Date.now();
|
|
30
|
-
|
|
31
|
-
while (Date.now() - startTime < timeout) {
|
|
32
|
-
const result = await checkFn();
|
|
33
|
-
if (result) {
|
|
34
|
-
return result;
|
|
35
|
-
}
|
|
36
|
-
await new Promise(r => setTimeout(r, interval));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
throw new Error(`Timeout: ${message}`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
test('API tests', async t => {
|
|
43
|
-
let message2;
|
|
44
|
-
let oauth2PubsubId;
|
|
45
|
-
let oauth2AppId;
|
|
46
|
-
|
|
47
|
-
let gmailReceivedEmailId;
|
|
48
|
-
let gmailReceivedMessageId;
|
|
49
|
-
|
|
50
|
-
let oauth2SendOnlyAppId;
|
|
51
|
-
|
|
52
|
-
t.before(async () => {
|
|
53
|
-
testAccount = await nodemailer.createTestAccount();
|
|
54
|
-
await webhooksServer.init();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
t.after(async () => {
|
|
58
|
-
redis.quit();
|
|
59
|
-
await webhooksServer.quit();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
await t.test('list existing users (empty list)', async () => {
|
|
63
|
-
const response = await server.get(`/v1/accounts`).expect(200);
|
|
64
|
-
|
|
65
|
-
assert.strictEqual(response.body.accounts.length, 0);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
await t.test('Verify IMAP account', async () => {
|
|
69
|
-
const response = await server
|
|
70
|
-
.post(`/v1/verifyAccount`)
|
|
71
|
-
.send({
|
|
72
|
-
mailboxes: true,
|
|
73
|
-
imap: {
|
|
74
|
-
host: testAccount.imap.host,
|
|
75
|
-
port: testAccount.imap.port,
|
|
76
|
-
secure: testAccount.imap.secure,
|
|
77
|
-
auth: {
|
|
78
|
-
user: testAccount.user,
|
|
79
|
-
pass: testAccount.pass
|
|
80
|
-
}
|
|
81
|
-
},
|
|
82
|
-
smtp: {
|
|
83
|
-
host: testAccount.smtp.host,
|
|
84
|
-
port: testAccount.smtp.port,
|
|
85
|
-
secure: testAccount.smtp.secure,
|
|
86
|
-
auth: {
|
|
87
|
-
user: testAccount.user,
|
|
88
|
-
pass: testAccount.pass
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
})
|
|
92
|
-
.expect(200);
|
|
93
|
-
|
|
94
|
-
assert.strictEqual(response.body.imap.success, true);
|
|
95
|
-
assert.strictEqual(response.body.smtp.success, true);
|
|
96
|
-
// Check if Inbox folder exists
|
|
97
|
-
assert.ok(response.body.mailboxes.some(mb => mb.specialUse === '\\Inbox'));
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
await t.test('Register new IMAP account', async () => {
|
|
101
|
-
const response = await server
|
|
102
|
-
.post(`/v1/account`)
|
|
103
|
-
.send({
|
|
104
|
-
account: defaultAccountId,
|
|
105
|
-
name: 'Test User 🫥',
|
|
106
|
-
email: testAccount.user,
|
|
107
|
-
imap: {
|
|
108
|
-
host: testAccount.imap.host,
|
|
109
|
-
port: testAccount.imap.port,
|
|
110
|
-
secure: testAccount.imap.secure,
|
|
111
|
-
auth: {
|
|
112
|
-
user: testAccount.user,
|
|
113
|
-
pass: testAccount.pass
|
|
114
|
-
},
|
|
115
|
-
resyncDelay: 60 * 1000
|
|
116
|
-
},
|
|
117
|
-
smtp: {
|
|
118
|
-
host: testAccount.smtp.host,
|
|
119
|
-
port: testAccount.smtp.port,
|
|
120
|
-
secure: testAccount.smtp.secure,
|
|
121
|
-
auth: {
|
|
122
|
-
user: testAccount.user,
|
|
123
|
-
pass: testAccount.pass
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
})
|
|
127
|
-
.expect(200);
|
|
128
|
-
|
|
129
|
-
assert.strictEqual(response.body.state, 'new');
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
await t.test('wait until added account is available', { timeout: 60000 }, async () => {
|
|
133
|
-
// wait until connected with timeout
|
|
134
|
-
|
|
135
|
-
await waitForCondition(
|
|
136
|
-
async () => {
|
|
137
|
-
const response = await server.get(`/v1/account/${defaultAccountId}`).expect(200);
|
|
138
|
-
switch (response.body.state) {
|
|
139
|
-
case 'authenticationError':
|
|
140
|
-
case 'connectError':
|
|
141
|
-
throw new Error('Invalid account state ' + response.body.state);
|
|
142
|
-
case 'connected':
|
|
143
|
-
return true;
|
|
144
|
-
}
|
|
145
|
-
return false;
|
|
146
|
-
},
|
|
147
|
-
{ timeout: testConfig.CONNECTION_TIMEOUT, message: 'Account connection timeout' }
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
// check if we have all expected webhooks
|
|
151
|
-
let webhooks = webhooksServer.webhooks.get(defaultAccountId);
|
|
152
|
-
|
|
153
|
-
for (let event of ['accountAdded', 'authenticationSuccess', 'accountInitialized']) {
|
|
154
|
-
assert.ok(webhooks.some(wh => wh.event === event));
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
await t.test('list existing users (1 account)', async () => {
|
|
159
|
-
const response = await server.get(`/v1/accounts`).expect(200);
|
|
160
|
-
|
|
161
|
-
assert.strictEqual(response.body.accounts.length, 1);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
await t.test('check if account credentials are encrypted', async () => {
|
|
165
|
-
let accountData = await redis.hgetall(`iad:${defaultAccountId}`);
|
|
166
|
-
let imapData = JSON.parse(accountData.imap);
|
|
167
|
-
let smtpData = JSON.parse(accountData.smtp);
|
|
168
|
-
|
|
169
|
-
assert.ok(imapData.auth.pass.indexOf('$wd01$') === 0);
|
|
170
|
-
assert.ok(smtpData.auth.pass.indexOf('$wd01$') === 0);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
await t.test('list mailboxes for an account', async () => {
|
|
174
|
-
const response = await server.get(`/v1/account/${defaultAccountId}/mailboxes`).expect(200);
|
|
175
|
-
|
|
176
|
-
assert.ok(response.body.mailboxes.some(mb => mb.specialUse === '\\Inbox'));
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
await t.test('list inbox messages (empty)', async () => {
|
|
180
|
-
const response = await server.get(`/v1/account/${defaultAccountId}/messages?path=INBOX`).expect(200);
|
|
181
|
-
|
|
182
|
-
assert.strictEqual(response.body.total, 0);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
await t.test('upload email to Inbox and wait for a messageNew webhook', { timeout: 60000 }, async () => {
|
|
186
|
-
const response1 = await server
|
|
187
|
-
.post(`/v1/account/${defaultAccountId}/message`)
|
|
188
|
-
.send({
|
|
189
|
-
path: 'Inbox',
|
|
190
|
-
flags: ['\\Seen'],
|
|
191
|
-
from: {
|
|
192
|
-
name: 'Test Sender',
|
|
193
|
-
address: 'test.sender@example.com'
|
|
194
|
-
},
|
|
195
|
-
to: [
|
|
196
|
-
{
|
|
197
|
-
name: 'Test Received',
|
|
198
|
-
address: 'test.received@example.com'
|
|
199
|
-
}
|
|
200
|
-
],
|
|
201
|
-
subject: 'Test message 🤣',
|
|
202
|
-
text: 'Hello world! 🙃',
|
|
203
|
-
html: '<b>Hello world! 🙃</b>',
|
|
204
|
-
messageId: '<test1@example.com>'
|
|
205
|
-
})
|
|
206
|
-
.expect(200);
|
|
207
|
-
assert.ok(response1.body.id);
|
|
208
|
-
|
|
209
|
-
const response2 = await server
|
|
210
|
-
.post(`/v1/account/${defaultAccountId}/message`)
|
|
211
|
-
.send({
|
|
212
|
-
path: 'Inbox',
|
|
213
|
-
flags: [],
|
|
214
|
-
from: {
|
|
215
|
-
name: 'Test Sender',
|
|
216
|
-
address: 'test.sender@example.com'
|
|
217
|
-
},
|
|
218
|
-
to: [
|
|
219
|
-
{
|
|
220
|
-
name: 'Test Received',
|
|
221
|
-
address: 'test.received@example.com'
|
|
222
|
-
}
|
|
223
|
-
],
|
|
224
|
-
subject: 'Test message 🤣',
|
|
225
|
-
text: 'Hello world! 🙃',
|
|
226
|
-
html: '<b>Hello world! 🙃</b>',
|
|
227
|
-
messageId: '<test2@example.com>',
|
|
228
|
-
attachments: [
|
|
229
|
-
{
|
|
230
|
-
filename: 'transparent.gif',
|
|
231
|
-
content: 'R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=',
|
|
232
|
-
contentType: 'image/gif',
|
|
233
|
-
contentDisposition: 'inline',
|
|
234
|
-
encoding: 'base64'
|
|
235
|
-
}
|
|
236
|
-
]
|
|
237
|
-
})
|
|
238
|
-
.expect(200);
|
|
239
|
-
|
|
240
|
-
assert.ok(response2.body.id);
|
|
241
|
-
|
|
242
|
-
const { messageNewWebhook1, messageNewWebhook2 } = await waitForCondition(
|
|
243
|
-
async () => {
|
|
244
|
-
let webhooks = webhooksServer.webhooks.get(defaultAccountId);
|
|
245
|
-
const webhook1 = webhooks.find(wh => wh.path === 'INBOX' && wh.event === 'messageNew' && wh.data.messageId === '<test1@example.com>');
|
|
246
|
-
const webhook2 = webhooks.find(wh => wh.path === 'INBOX' && wh.event === 'messageNew' && wh.data.messageId === '<test2@example.com>');
|
|
247
|
-
if (webhook1 && webhook2) {
|
|
248
|
-
return { messageNewWebhook1: webhook1, messageNewWebhook2: webhook2 };
|
|
249
|
-
}
|
|
250
|
-
return false;
|
|
251
|
-
},
|
|
252
|
-
{ timeout: testConfig.WEBHOOK_TIMEOUT, message: 'Webhook notification timeout' }
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
message2 = messageNewWebhook2.data;
|
|
256
|
-
|
|
257
|
-
assert.equal(messageNewWebhook1.data.subject, 'Test message 🤣');
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
await t.test('list inbox messages (2 messages)', async () => {
|
|
261
|
-
const response = await server.get(`/v1/account/${defaultAccountId}/messages?path=INBOX`).expect(200);
|
|
262
|
-
|
|
263
|
-
assert.strictEqual(response.body.total, 2);
|
|
264
|
-
assert.equal(response.body.messages[0].messageId, '<test2@example.com>');
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
await t.test('list mailboxes with counters', async () => {
|
|
268
|
-
const response = await server.get(`/v1/account/${defaultAccountId}/mailboxes?counters=true`).expect(200);
|
|
269
|
-
assert.ok(response.body.mailboxes.some(mb => mb.specialUse === '\\Inbox' && mb.status.messages === 2 && mb.status.unseen === 1));
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
await t.test('retrieve message text', async () => {
|
|
273
|
-
const response = await server.get(`/v1/account/${defaultAccountId}/text/${message2.text.id}?textType=*`).expect(200);
|
|
274
|
-
assert.deepEqual(response.body, { plain: 'Hello world! 🙃', html: '<b>Hello world! 🙃</b>', hasMore: false });
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
await t.test('download attachment', async () => {
|
|
278
|
-
const response = await server.get(`/v1/account/${defaultAccountId}/attachment/${message2.attachments[0].id}`).expect(200);
|
|
279
|
-
|
|
280
|
-
assert.strictEqual(response.headers['content-type'], `image/gif`);
|
|
281
|
-
assert.strictEqual(response.headers['content-disposition'], `attachment; filename="transparent.gif"; filename*=utf-8''transparent.gif`);
|
|
282
|
-
|
|
283
|
-
let attachment = response._body.toString('base64');
|
|
284
|
-
|
|
285
|
-
assert.strictEqual(attachment, 'R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=');
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
await t.test('get message information', async () => {
|
|
289
|
-
const response = await server.get(`/v1/account/${defaultAccountId}/message/${message2.id}?textType=*`).expect(200);
|
|
290
|
-
|
|
291
|
-
let message = response.body;
|
|
292
|
-
|
|
293
|
-
assert.strictEqual(message.id, message2.id);
|
|
294
|
-
assert.strictEqual(message.subject, 'Test message 🤣');
|
|
295
|
-
assert.strictEqual(message.messageSpecialUse, '\\Inbox');
|
|
296
|
-
assert.strictEqual(message.text.plain, 'Hello world! 🙃');
|
|
297
|
-
assert.strictEqual(message.text.html, '<b>Hello world! 🙃</b>');
|
|
298
|
-
assert.ok(!message.text.webSafe);
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
await t.test('get message information, websafe', async () => {
|
|
302
|
-
const response = await server.get(`/v1/account/${defaultAccountId}/message/${message2.id}?webSafeHtml=true`).expect(200);
|
|
303
|
-
|
|
304
|
-
let message = response.body;
|
|
305
|
-
|
|
306
|
-
assert.strictEqual(message.id, message2.id);
|
|
307
|
-
assert.strictEqual(message.subject, 'Test message 🤣');
|
|
308
|
-
assert.strictEqual(message.messageSpecialUse, '\\Inbox');
|
|
309
|
-
assert.strictEqual(message.text.plain, 'Hello world! 🙃');
|
|
310
|
-
assert.strictEqual(message.text.html, '<div style="overflow: auto;"><b>Hello world! 🙃</b></div>');
|
|
311
|
-
assert.strictEqual(message.text.webSafe, true);
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
await t.test('download raw message', async () => {
|
|
315
|
-
const response = await server.get(`/v1/account/${defaultAccountId}/message/${message2.id}/source`).expect(200);
|
|
316
|
-
|
|
317
|
-
assert.strictEqual(response.headers['content-type'], `message/rfc822`);
|
|
318
|
-
assert.strictEqual(response.headers['content-disposition'], `attachment; filename=message.eml`);
|
|
319
|
-
|
|
320
|
-
let eml = response.text;
|
|
321
|
-
|
|
322
|
-
assert.ok(/^Message-ID:/im.test(eml));
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
await t.test('search unseen messages', async () => {
|
|
326
|
-
const response = await server
|
|
327
|
-
.post(`/v1/account/${defaultAccountId}/search?path=INBOX`)
|
|
328
|
-
.send({
|
|
329
|
-
search: {
|
|
330
|
-
unseen: true
|
|
331
|
-
}
|
|
332
|
-
})
|
|
333
|
-
.expect(200);
|
|
334
|
-
|
|
335
|
-
assert.strictEqual(response.body.total, 1);
|
|
336
|
-
assert.strictEqual(response.body.messages[0].messageId, '<test2@example.com>');
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
await t.test('mark message as seen', { timeout: 60000 }, async () => {
|
|
340
|
-
const response = await server
|
|
341
|
-
.put(`/v1/account/${defaultAccountId}/message/${message2.id}`)
|
|
342
|
-
.send({
|
|
343
|
-
flags: {
|
|
344
|
-
add: ['\\Seen']
|
|
345
|
-
}
|
|
346
|
-
})
|
|
347
|
-
.expect(200);
|
|
348
|
-
|
|
349
|
-
assert.ok(response.body.flags.add);
|
|
350
|
-
|
|
351
|
-
const messageUpdatedWebhook = await waitForCondition(
|
|
352
|
-
async () => {
|
|
353
|
-
let webhooks = webhooksServer.webhooks.get(defaultAccountId);
|
|
354
|
-
return webhooks.find(wh => wh.path === 'INBOX' && wh.event === 'messageUpdated' && wh.data.id === message2.id);
|
|
355
|
-
},
|
|
356
|
-
{ timeout: testConfig.WEBHOOK_TIMEOUT, message: 'Message update webhook timeout' }
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
assert.deepEqual(messageUpdatedWebhook.data.changes.flags.added, ['\\Seen']);
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
await t.test('upload by reference', { timeout: 60000 }, async () => {
|
|
363
|
-
await server
|
|
364
|
-
.post(`/v1/account/${defaultAccountId}/message`)
|
|
365
|
-
.send({
|
|
366
|
-
path: 'Inbox',
|
|
367
|
-
reference: {
|
|
368
|
-
message: message2.id,
|
|
369
|
-
action: 'forward',
|
|
370
|
-
inline: true,
|
|
371
|
-
forwardAttachments: true,
|
|
372
|
-
messageId: '<invalid@value>'
|
|
373
|
-
},
|
|
374
|
-
to: [
|
|
375
|
-
{
|
|
376
|
-
name: 'Test Received',
|
|
377
|
-
address: 'test.received@example.com'
|
|
378
|
-
}
|
|
379
|
-
],
|
|
380
|
-
text: 'Hallo hallo! 🙃',
|
|
381
|
-
html: '<b>Hallo hallo! 🙃</b>',
|
|
382
|
-
messageId: '<test3@example.com>'
|
|
383
|
-
})
|
|
384
|
-
// fails message-id test
|
|
385
|
-
.expect(404);
|
|
386
|
-
|
|
387
|
-
const response = await server
|
|
388
|
-
.post(`/v1/account/${defaultAccountId}/message`)
|
|
389
|
-
.send({
|
|
390
|
-
path: 'Inbox',
|
|
391
|
-
reference: {
|
|
392
|
-
message: message2.id,
|
|
393
|
-
action: 'forward',
|
|
394
|
-
inline: true,
|
|
395
|
-
forwardAttachments: true,
|
|
396
|
-
messageId: '<test2@example.com>'
|
|
397
|
-
},
|
|
398
|
-
to: [
|
|
399
|
-
{
|
|
400
|
-
name: 'Test Received',
|
|
401
|
-
address: 'test.received@example.com'
|
|
402
|
-
}
|
|
403
|
-
],
|
|
404
|
-
text: 'Hallo hallo! 🙃',
|
|
405
|
-
html: '<b>Hallo hallo! 🙃</b>',
|
|
406
|
-
messageId: '<test3@example.com>'
|
|
407
|
-
})
|
|
408
|
-
.expect(200);
|
|
409
|
-
|
|
410
|
-
assert.ok(response.body.id);
|
|
411
|
-
|
|
412
|
-
const messageNewWebhook = await waitForCondition(
|
|
413
|
-
async () => {
|
|
414
|
-
let webhooks = webhooksServer.webhooks.get(defaultAccountId);
|
|
415
|
-
return webhooks.find(wh => wh.path === 'INBOX' && wh.event === 'messageNew' && wh.data.messageId === '<test3@example.com>');
|
|
416
|
-
},
|
|
417
|
-
{ timeout: testConfig.WEBHOOK_TIMEOUT, message: 'Message upload webhook timeout' }
|
|
418
|
-
);
|
|
419
|
-
|
|
420
|
-
assert.ok(/Begin forwarded message/.test(messageNewWebhook.data.text.plain));
|
|
421
|
-
assert.strictEqual(messageNewWebhook.data.attachments[0].filename, 'transparent.gif');
|
|
422
|
-
assert.strictEqual(messageNewWebhook.data.subject, 'Fwd: Test message 🤣');
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
await t.test('submit by reference', { timeout: 60000 }, async () => {
|
|
426
|
-
const response = await server
|
|
427
|
-
.post(`/v1/account/${defaultAccountId}/submit`)
|
|
428
|
-
.send({
|
|
429
|
-
reference: {
|
|
430
|
-
message: message2.id,
|
|
431
|
-
action: 'forward',
|
|
432
|
-
inline: true,
|
|
433
|
-
forwardAttachments: true,
|
|
434
|
-
messageId: '<test2@example.com>'
|
|
435
|
-
},
|
|
436
|
-
to: [
|
|
437
|
-
{
|
|
438
|
-
name: 'Test Received',
|
|
439
|
-
address: 'test.received@example.com'
|
|
440
|
-
}
|
|
441
|
-
],
|
|
442
|
-
text: 'Hallo hallo! 🙃',
|
|
443
|
-
html: '<b>Hallo hallo! 🙃</b>',
|
|
444
|
-
messageId: '<test4@example.com>'
|
|
445
|
-
})
|
|
446
|
-
.expect(200);
|
|
447
|
-
|
|
448
|
-
assert.ok(response.body.messageId);
|
|
449
|
-
assert.ok(response.body.queueId);
|
|
450
|
-
|
|
451
|
-
const messageNewWebhook = await waitForCondition(
|
|
452
|
-
async () => {
|
|
453
|
-
let webhooks = webhooksServer.webhooks.get(defaultAccountId);
|
|
454
|
-
return webhooks.find(wh => wh.path === 'INBOX' && wh.event === 'messageNew' && wh.data.messageId === '<test4@example.com>');
|
|
455
|
-
},
|
|
456
|
-
{ timeout: testConfig.WEBHOOK_TIMEOUT, message: 'Submit webhook timeout' }
|
|
457
|
-
);
|
|
458
|
-
|
|
459
|
-
assert.ok(/Begin forwarded message/.test(messageNewWebhook.data.text.plain));
|
|
460
|
-
assert.strictEqual(messageNewWebhook.data.attachments[0].filename, 'transparent.gif');
|
|
461
|
-
assert.strictEqual(messageNewWebhook.data.subject, 'Fwd: Test message 🤣');
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
await t.test('create a mailbox', { timeout: 60000 }, async () => {
|
|
465
|
-
const response = await server
|
|
466
|
-
.post(`/v1/account/${defaultAccountId}/mailbox`)
|
|
467
|
-
.send({
|
|
468
|
-
path: ['My Target Folder 😇']
|
|
469
|
-
})
|
|
470
|
-
.expect(200);
|
|
471
|
-
|
|
472
|
-
assert.strictEqual(response.body.path, 'My Target Folder 😇');
|
|
473
|
-
assert.ok(response.body.created);
|
|
474
|
-
|
|
475
|
-
const mailboxNewWebhook = await waitForCondition(
|
|
476
|
-
async () => {
|
|
477
|
-
let webhooks = webhooksServer.webhooks.get(defaultAccountId);
|
|
478
|
-
return webhooks.find(wh => wh.path === 'My Target Folder 😇' && wh.event === 'mailboxNew');
|
|
479
|
-
},
|
|
480
|
-
{ timeout: testConfig.WEBHOOK_TIMEOUT, message: 'Mailbox creation webhook timeout' }
|
|
481
|
-
);
|
|
482
|
-
|
|
483
|
-
assert.ok(mailboxNewWebhook);
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
await t.test('modify mailbox - rename only', { timeout: 60000 }, async () => {
|
|
487
|
-
const response = await server
|
|
488
|
-
.put(`/v1/account/${defaultAccountId}/mailbox`)
|
|
489
|
-
.send({
|
|
490
|
-
path: 'My Target Folder 😇',
|
|
491
|
-
newPath: 'My Renamed Folder'
|
|
492
|
-
})
|
|
493
|
-
.expect(200);
|
|
494
|
-
|
|
495
|
-
assert.strictEqual(response.body.path, 'My Target Folder 😇');
|
|
496
|
-
assert.strictEqual(response.body.newPath, 'My Renamed Folder');
|
|
497
|
-
assert.strictEqual(response.body.renamed, true);
|
|
498
|
-
|
|
499
|
-
const mailboxListResponse = await server.get(`/v1/account/${defaultAccountId}/mailboxes`).expect(200);
|
|
500
|
-
const renamedMailbox = mailboxListResponse.body.mailboxes.find(mb => mb.path === 'My Renamed Folder');
|
|
501
|
-
assert.ok(renamedMailbox, 'Renamed mailbox should exist');
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
await t.test('modify mailbox - subscription only', { timeout: 60000 }, async () => {
|
|
505
|
-
const response = await server
|
|
506
|
-
.put(`/v1/account/${defaultAccountId}/mailbox`)
|
|
507
|
-
.send({
|
|
508
|
-
path: 'My Renamed Folder',
|
|
509
|
-
subscribed: false
|
|
510
|
-
})
|
|
511
|
-
.expect(200);
|
|
512
|
-
|
|
513
|
-
assert.strictEqual(response.body.path, 'My Renamed Folder');
|
|
514
|
-
assert.strictEqual(response.body.subscribed, false);
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
await t.test('modify mailbox - both rename and subscription', { timeout: 60000 }, async () => {
|
|
518
|
-
const response = await server
|
|
519
|
-
.put(`/v1/account/${defaultAccountId}/mailbox`)
|
|
520
|
-
.send({
|
|
521
|
-
path: 'My Renamed Folder',
|
|
522
|
-
newPath: 'My Final Folder',
|
|
523
|
-
subscribed: true
|
|
524
|
-
})
|
|
525
|
-
.expect(200);
|
|
526
|
-
|
|
527
|
-
assert.strictEqual(response.body.path, 'My Renamed Folder');
|
|
528
|
-
assert.strictEqual(response.body.newPath, 'My Final Folder');
|
|
529
|
-
assert.strictEqual(response.body.renamed, true);
|
|
530
|
-
assert.strictEqual(response.body.subscribed, true);
|
|
531
|
-
|
|
532
|
-
const mailboxListResponse = await server.get(`/v1/account/${defaultAccountId}/mailboxes`).expect(200);
|
|
533
|
-
const finalMailbox = mailboxListResponse.body.mailboxes.find(mb => mb.path === 'My Final Folder');
|
|
534
|
-
assert.ok(finalMailbox, 'Final mailbox should exist');
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
await t.test('move message to another folder', { timeout: 60000 }, async () => {
|
|
538
|
-
const response = await server
|
|
539
|
-
.put(`/v1/account/${defaultAccountId}/message/${message2.id}/move`)
|
|
540
|
-
.send({
|
|
541
|
-
path: 'My Final Folder'
|
|
542
|
-
})
|
|
543
|
-
.expect(200);
|
|
544
|
-
|
|
545
|
-
assert.strictEqual(response.body.path, 'My Final Folder');
|
|
546
|
-
|
|
547
|
-
assert.strictEqual(response.body.uid, 1);
|
|
548
|
-
|
|
549
|
-
const responseSearchTarget = await server
|
|
550
|
-
.post(`/v1/account/${defaultAccountId}/search?path=${encodeURIComponent('My Final Folder')}`)
|
|
551
|
-
.send({
|
|
552
|
-
search: {
|
|
553
|
-
uid: '1'
|
|
554
|
-
}
|
|
555
|
-
})
|
|
556
|
-
.expect(200);
|
|
557
|
-
|
|
558
|
-
assert.strictEqual(responseSearchTarget.body.total, 1);
|
|
559
|
-
assert.strictEqual(responseSearchTarget.body.messages[0].messageId, '<test2@example.com>');
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
await t.test('Create Gmail API OAuth2 service project', { timeout: 30000 }, async () => {
|
|
563
|
-
let gmailServiceData = {
|
|
564
|
-
name: 'Gmail API Pub/Sub',
|
|
565
|
-
provider: 'gmailService',
|
|
566
|
-
baseScopes: 'pubsub',
|
|
567
|
-
googleProjectId: process.env.GMAIL_API_PROJECT_ID,
|
|
568
|
-
serviceClient: process.env.GMAIL_API_SERVICE_CLIENT,
|
|
569
|
-
serviceClientEmail: process.env.GMAIL_API_SERVICE_EMAIL,
|
|
570
|
-
serviceKey: process.env.GMAIL_API_SERVICE_KEY
|
|
571
|
-
};
|
|
572
|
-
|
|
573
|
-
const response = await server.post(`/v1/oauth2`).send(gmailServiceData).expect(200);
|
|
574
|
-
|
|
575
|
-
oauth2PubsubId = response.body.id;
|
|
576
|
-
assert.ok(oauth2PubsubId);
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
await t.test('Create Gmail API OAuth2 client project', { timeout: 30000 }, async () => {
|
|
580
|
-
let gmailClientData = {
|
|
581
|
-
name: 'Gmail API Client',
|
|
582
|
-
provider: 'gmail',
|
|
583
|
-
baseScopes: 'api',
|
|
584
|
-
googleProjectId: process.env.GMAIL_API_PROJECT_ID,
|
|
585
|
-
clientId: process.env.GMAIL_API_CLIENT_ID,
|
|
586
|
-
clientSecret: process.env.GMAIL_API_CLIENT_SECRET,
|
|
587
|
-
pubSubApp: oauth2PubsubId,
|
|
588
|
-
redirectUrl: 'http://127.0.0.1:7003/oauth'
|
|
589
|
-
};
|
|
590
|
-
|
|
591
|
-
const response = await server.post(`/v1/oauth2`).send(gmailClientData).expect(200);
|
|
592
|
-
|
|
593
|
-
oauth2AppId = response.body.id;
|
|
594
|
-
assert.ok(oauth2AppId);
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
await t.test('Register Gmail account 1', { timeout: 30000 }, async () => {
|
|
598
|
-
const response = await server
|
|
599
|
-
.post(`/v1/account`)
|
|
600
|
-
.send({
|
|
601
|
-
account: gmailAccountId1,
|
|
602
|
-
name: 'Gmail User 1 🫥',
|
|
603
|
-
email: process.env.GMAIL_API_ACCOUNT_EMAIL_1,
|
|
604
|
-
oauth2: {
|
|
605
|
-
provider: oauth2AppId,
|
|
606
|
-
auth: {
|
|
607
|
-
user: process.env.GMAIL_API_ACCOUNT_EMAIL_1
|
|
608
|
-
},
|
|
609
|
-
refreshToken: process.env.GMAIL_API_ACCOUNT_REFRESH_1
|
|
610
|
-
}
|
|
611
|
-
})
|
|
612
|
-
.expect(200);
|
|
613
|
-
|
|
614
|
-
assert.strictEqual(response.body.state, 'new');
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
await t.test('Register Gmail account 2', { timeout: 30000 }, async () => {
|
|
618
|
-
const response = await server
|
|
619
|
-
.post(`/v1/account`)
|
|
620
|
-
.send({
|
|
621
|
-
account: gmailAccountId2,
|
|
622
|
-
name: 'Gmail User 2 🫥',
|
|
623
|
-
email: process.env.GMAIL_API_ACCOUNT_EMAIL_2,
|
|
624
|
-
oauth2: {
|
|
625
|
-
provider: oauth2AppId,
|
|
626
|
-
auth: {
|
|
627
|
-
user: process.env.GMAIL_API_ACCOUNT_EMAIL_2
|
|
628
|
-
},
|
|
629
|
-
refreshToken: process.env.GMAIL_API_ACCOUNT_REFRESH_2
|
|
630
|
-
}
|
|
631
|
-
})
|
|
632
|
-
.expect(200);
|
|
633
|
-
|
|
634
|
-
assert.strictEqual(response.body.state, 'new');
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
await t.test('wait until Gmail accounts are available', { timeout: 120000 }, async () => {
|
|
638
|
-
for (let account of [gmailAccountId1, gmailAccountId2]) {
|
|
639
|
-
// wait until connected with longer timeout for Gmail
|
|
640
|
-
await waitForCondition(
|
|
641
|
-
async () => {
|
|
642
|
-
const response = await server.get(`/v1/account/${account}`).expect(200);
|
|
643
|
-
switch (response.body.state) {
|
|
644
|
-
case 'authenticationError':
|
|
645
|
-
case 'connectError':
|
|
646
|
-
throw new Error('Invalid account state ' + response.body.state);
|
|
647
|
-
case 'connected':
|
|
648
|
-
return true;
|
|
649
|
-
}
|
|
650
|
-
return false;
|
|
651
|
-
},
|
|
652
|
-
{ timeout: testConfig.GMAIL_TIMEOUT, message: `Gmail account ${account} connection timeout` }
|
|
653
|
-
);
|
|
654
|
-
|
|
655
|
-
// check if we have all expected webhooks
|
|
656
|
-
let webhooks = webhooksServer.webhooks.get(account);
|
|
657
|
-
for (let event of ['accountAdded', 'authenticationSuccess', 'accountInitialized']) {
|
|
658
|
-
assert.ok(webhooks.some(wh => wh.event === event));
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
await t.test('list mailboxes for Gmail account 1', { timeout: 30000 }, async () => {
|
|
664
|
-
const response = await server.get(`/v1/account/${gmailAccountId1}/mailboxes`).expect(200);
|
|
665
|
-
|
|
666
|
-
assert.ok(response.body.mailboxes.some(mb => mb.specialUse === '\\Inbox'));
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
await t.test('list inbox messages for Gmail account 1 (greeting emails)', { timeout: 30000 }, async () => {
|
|
670
|
-
const response = await server.get(`/v1/account/${gmailAccountId1}/messages?path=INBOX`).expect(200);
|
|
671
|
-
|
|
672
|
-
assert.ok(response.body.total > 0);
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
await t.test('submit by API', { timeout: 120000 }, async () => {
|
|
676
|
-
let messageId = `<test-${Date.now()}@example.com>`;
|
|
677
|
-
|
|
678
|
-
const response = await server
|
|
679
|
-
.post(`/v1/account/${gmailAccountId2}/submit`)
|
|
680
|
-
.send({
|
|
681
|
-
to: [
|
|
682
|
-
{
|
|
683
|
-
name: 'Test Account 1',
|
|
684
|
-
address: process.env.GMAIL_API_ACCOUNT_EMAIL_1
|
|
685
|
-
}
|
|
686
|
-
],
|
|
687
|
-
subject: 'Hallo hallo 🤣',
|
|
688
|
-
text: 'Hallo hallo! 🙃',
|
|
689
|
-
html: '<b>Hallo hallo! 🙃</b>',
|
|
690
|
-
messageId
|
|
691
|
-
})
|
|
692
|
-
.expect(200);
|
|
693
|
-
|
|
694
|
-
assert.ok(response.body.messageId);
|
|
695
|
-
assert.ok(response.body.queueId);
|
|
696
|
-
|
|
697
|
-
const messageSentWebhook = await waitForCondition(
|
|
698
|
-
async () => {
|
|
699
|
-
let webhooks = webhooksServer.webhooks.get(gmailAccountId2);
|
|
700
|
-
return webhooks.find(wh => wh.event === 'messageSent' && wh.data.originalMessageId === messageId);
|
|
701
|
-
},
|
|
702
|
-
{ timeout: testConfig.GMAIL_TIMEOUT, message: 'Gmail message sent webhook timeout' }
|
|
703
|
-
);
|
|
704
|
-
|
|
705
|
-
gmailReceivedMessageId = messageSentWebhook.data.messageId;
|
|
706
|
-
|
|
707
|
-
const messageNewWebhook = await waitForCondition(
|
|
708
|
-
async () => {
|
|
709
|
-
let webhooks = webhooksServer.webhooks.get(gmailAccountId1);
|
|
710
|
-
return webhooks.find(wh => wh.event === 'messageNew' && wh.data.messageId === gmailReceivedMessageId);
|
|
711
|
-
},
|
|
712
|
-
{ timeout: testConfig.GMAIL_TIMEOUT, message: 'Gmail message receive webhook timeout' }
|
|
713
|
-
);
|
|
714
|
-
|
|
715
|
-
// * is added by gmail
|
|
716
|
-
assert.strictEqual(messageNewWebhook.data.text.plain.trim(), '*Hallo hallo! 🙃*');
|
|
717
|
-
assert.strictEqual(messageNewWebhook.data.messageId, gmailReceivedMessageId);
|
|
718
|
-
assert.strictEqual(messageNewWebhook.data.subject.trim(), 'Hallo hallo 🤣');
|
|
719
|
-
|
|
720
|
-
gmailReceivedEmailId = messageNewWebhook.data.id;
|
|
721
|
-
assert.ok(gmailReceivedEmailId);
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
await t.test('reply by reference by API', { timeout: 120000 }, async () => {
|
|
725
|
-
let messageId = `<test-${Date.now()}@example.com>`;
|
|
726
|
-
|
|
727
|
-
const response = await server
|
|
728
|
-
.post(`/v1/account/${gmailAccountId1}/submit`)
|
|
729
|
-
.send({
|
|
730
|
-
reference: {
|
|
731
|
-
message: gmailReceivedEmailId,
|
|
732
|
-
action: 'reply',
|
|
733
|
-
inline: true
|
|
734
|
-
},
|
|
735
|
-
text: 'Keedu kartul! 🍟',
|
|
736
|
-
html: '<b>Keedu kartul! 🍟</b>',
|
|
737
|
-
messageId
|
|
738
|
-
})
|
|
739
|
-
.expect(200);
|
|
740
|
-
|
|
741
|
-
assert.ok(response.body.messageId);
|
|
742
|
-
assert.ok(response.body.queueId);
|
|
743
|
-
|
|
744
|
-
let finalMessageId;
|
|
745
|
-
|
|
746
|
-
const messageSentWebhook = await waitForCondition(
|
|
747
|
-
async () => {
|
|
748
|
-
let webhooks = webhooksServer.webhooks.get(gmailAccountId1);
|
|
749
|
-
return webhooks.find(wh => wh.event === 'messageSent' && wh.data.originalMessageId === messageId);
|
|
750
|
-
},
|
|
751
|
-
{ timeout: testConfig.GMAIL_TIMEOUT, message: 'Gmail reply sent webhook timeout' }
|
|
752
|
-
);
|
|
753
|
-
|
|
754
|
-
finalMessageId = messageSentWebhook.data.messageId;
|
|
755
|
-
|
|
756
|
-
const messageNewWebhook = await waitForCondition(
|
|
757
|
-
async () => {
|
|
758
|
-
let webhooks = webhooksServer.webhooks.get(gmailAccountId2);
|
|
759
|
-
return webhooks.find(wh => wh.event === 'messageNew' && wh.data.messageId === finalMessageId);
|
|
760
|
-
},
|
|
761
|
-
{ timeout: testConfig.GMAIL_TIMEOUT, message: 'Gmail reply receive webhook timeout' }
|
|
762
|
-
);
|
|
763
|
-
|
|
764
|
-
assert.strictEqual(messageNewWebhook.data.subject.trim(), 'Re: Hallo hallo 🤣');
|
|
765
|
-
assert.strictEqual(messageNewWebhook.data.inReplyTo, gmailReceivedMessageId);
|
|
766
|
-
|
|
767
|
-
assert.ok(messageNewWebhook);
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
await t.test('Create Gmail send-only OAuth2 client project', { timeout: 30000 }, async () => {
|
|
771
|
-
let gmailSendOnlyClientData = {
|
|
772
|
-
name: 'Gmail API Send-Only Client',
|
|
773
|
-
provider: 'gmail',
|
|
774
|
-
baseScopes: 'api',
|
|
775
|
-
googleProjectId: process.env.GMAIL_SENDONLY_PROJECT_ID,
|
|
776
|
-
clientId: process.env.GMAIL_SENDONLY_CLIENT_ID,
|
|
777
|
-
clientSecret: process.env.GMAIL_SENDONLY_CLIENT_SECRET,
|
|
778
|
-
extraScopes: ['gmail.send'],
|
|
779
|
-
skipScopes: ['gmail.modify'],
|
|
780
|
-
redirectUrl: 'http://127.0.0.1:3000/oauth'
|
|
781
|
-
};
|
|
782
|
-
|
|
783
|
-
const response = await server.post(`/v1/oauth2`).send(gmailSendOnlyClientData).expect(200);
|
|
784
|
-
|
|
785
|
-
oauth2SendOnlyAppId = response.body.id;
|
|
786
|
-
assert.ok(oauth2SendOnlyAppId);
|
|
787
|
-
});
|
|
788
|
-
|
|
789
|
-
await t.test('Register Gmail send-only account', { timeout: 30000 }, async () => {
|
|
790
|
-
const response = await server
|
|
791
|
-
.post(`/v1/account`)
|
|
792
|
-
.send({
|
|
793
|
-
account: gmailSendOnlyAccountId,
|
|
794
|
-
name: 'Gmail Send-Only User',
|
|
795
|
-
email: process.env.GMAIL_SENDONLY_ACCOUNT_EMAIL,
|
|
796
|
-
oauth2: {
|
|
797
|
-
provider: oauth2SendOnlyAppId,
|
|
798
|
-
auth: {
|
|
799
|
-
user: process.env.GMAIL_SENDONLY_ACCOUNT_EMAIL
|
|
800
|
-
},
|
|
801
|
-
refreshToken: process.env.GMAIL_SENDONLY_ACCOUNT_REFRESH
|
|
802
|
-
}
|
|
803
|
-
})
|
|
804
|
-
.expect(200);
|
|
805
|
-
|
|
806
|
-
assert.strictEqual(response.body.state, 'new');
|
|
807
|
-
});
|
|
808
|
-
|
|
809
|
-
await t.test('wait until Gmail send-only account is available', { timeout: 180000 }, async () => {
|
|
810
|
-
// wait until connected with longer timeout for Gmail
|
|
811
|
-
await waitForCondition(
|
|
812
|
-
async () => {
|
|
813
|
-
const response = await server.get(`/v1/account/${gmailSendOnlyAccountId}`).expect(200);
|
|
814
|
-
switch (response.body.state) {
|
|
815
|
-
case 'authenticationError':
|
|
816
|
-
case 'connectError':
|
|
817
|
-
throw new Error('Invalid account state ' + response.body.state);
|
|
818
|
-
case 'connected':
|
|
819
|
-
return true;
|
|
820
|
-
}
|
|
821
|
-
return false;
|
|
822
|
-
},
|
|
823
|
-
{ timeout: testConfig.GMAIL_TIMEOUT, message: `Gmail send-only account connection timeout` }
|
|
824
|
-
);
|
|
825
|
-
|
|
826
|
-
// check account type
|
|
827
|
-
const response = await server.get(`/v1/account/${gmailSendOnlyAccountId}`).expect(200);
|
|
828
|
-
assert.strictEqual(response.body.sendOnly, true, 'Account should be detected as send-only');
|
|
829
|
-
|
|
830
|
-
// check if we have expected webhooks
|
|
831
|
-
let webhooks = webhooksServer.webhooks.get(gmailSendOnlyAccountId);
|
|
832
|
-
for (let event of ['accountAdded', 'authenticationSuccess', 'accountInitialized']) {
|
|
833
|
-
assert.ok(webhooks.some(wh => wh.event === event));
|
|
834
|
-
}
|
|
835
|
-
});
|
|
836
|
-
|
|
837
|
-
await t.test('send-only account - list mailboxes should fail', { timeout: 30000 }, async () => {
|
|
838
|
-
const response = await server.get(`/v1/account/${gmailSendOnlyAccountId}/mailboxes`).expect(403);
|
|
839
|
-
|
|
840
|
-
// Gmail API will reject the request due to insufficient scopes
|
|
841
|
-
assert.ok(response.body.error);
|
|
842
|
-
});
|
|
843
|
-
|
|
844
|
-
await t.test('send-only account - list messages should fail', { timeout: 30000 }, async () => {
|
|
845
|
-
const response = await server.get(`/v1/account/${gmailSendOnlyAccountId}/messages?path=INBOX`).expect(403);
|
|
846
|
-
|
|
847
|
-
// Gmail API will reject the request due to insufficient scopes
|
|
848
|
-
assert.ok(response.body.error);
|
|
849
|
-
});
|
|
850
|
-
|
|
851
|
-
await t.test('send-only account - get message should fail', { timeout: 30000 }, async () => {
|
|
852
|
-
// Use a message ID from gmailAccountId2 to try to access it
|
|
853
|
-
if (!gmailReceivedEmailId) {
|
|
854
|
-
throw new Error('No message ID available for testing');
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
const response = await server.get(`/v1/account/${gmailSendOnlyAccountId}/message/${gmailReceivedEmailId}`).expect(403);
|
|
858
|
-
|
|
859
|
-
// Gmail API will reject the request due to insufficient scopes
|
|
860
|
-
assert.ok(response.body.error);
|
|
861
|
-
});
|
|
862
|
-
|
|
863
|
-
await t.test('send-only account - submit email successfully', { timeout: 180000 }, async () => {
|
|
864
|
-
let messageId = `<sendonly-test-${Date.now()}@example.com>`;
|
|
865
|
-
|
|
866
|
-
const response = await server
|
|
867
|
-
.post(`/v1/account/${gmailSendOnlyAccountId}/submit`)
|
|
868
|
-
.send({
|
|
869
|
-
to: [
|
|
870
|
-
{
|
|
871
|
-
name: 'Test Account 2',
|
|
872
|
-
address: process.env.GMAIL_API_ACCOUNT_EMAIL_2
|
|
873
|
-
}
|
|
874
|
-
],
|
|
875
|
-
subject: 'Send-only test message',
|
|
876
|
-
text: 'This message was sent from a send-only account',
|
|
877
|
-
html: '<p>This message was sent from a <strong>send-only</strong> account</p>',
|
|
878
|
-
messageId
|
|
879
|
-
})
|
|
880
|
-
.expect(200);
|
|
881
|
-
|
|
882
|
-
assert.ok(response.body.messageId);
|
|
883
|
-
assert.ok(response.body.queueId);
|
|
884
|
-
|
|
885
|
-
// Wait for messageSent webhook on send-only account
|
|
886
|
-
const messageSentWebhook = await waitForCondition(
|
|
887
|
-
async () => {
|
|
888
|
-
let webhooks = webhooksServer.webhooks.get(gmailSendOnlyAccountId);
|
|
889
|
-
return webhooks.find(wh => wh.event === 'messageSent' && wh.data.originalMessageId === messageId);
|
|
890
|
-
},
|
|
891
|
-
{ timeout: testConfig.GMAIL_TIMEOUT, message: 'Gmail send-only message sent webhook timeout' }
|
|
892
|
-
);
|
|
893
|
-
assert.ok(messageSentWebhook);
|
|
894
|
-
|
|
895
|
-
// Cannot verify the final Gmail-assigned message ID because send-only accounts
|
|
896
|
-
// lack read permissions for the Sent Mail folder. Gmail assigns a new message ID
|
|
897
|
-
// that differs from the original messageId sent in the request.
|
|
898
|
-
});
|
|
899
|
-
});
|