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.
Files changed (136) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/data/google-crawlers.json +1 -1
  3. package/lib/account/account-state.js +248 -0
  4. package/lib/account.js +45 -193
  5. package/lib/api-routes/account-routes.js +1023 -0
  6. package/lib/api-routes/message-routes.js +1377 -0
  7. package/lib/consts.js +12 -2
  8. package/lib/email-client/base-client.js +282 -771
  9. package/lib/email-client/gmail/gmail-api.js +243 -0
  10. package/lib/email-client/gmail-client.js +145 -53
  11. package/lib/email-client/imap/mailbox.js +24 -698
  12. package/lib/email-client/imap/sync-operations.js +812 -0
  13. package/lib/email-client/imap-client.js +1 -1
  14. package/lib/email-client/message-builder.js +566 -0
  15. package/lib/email-client/notification-handler.js +314 -0
  16. package/lib/email-client/outlook/graph-api.js +326 -0
  17. package/lib/email-client/outlook-client.js +159 -113
  18. package/lib/email-client/smtp-pool-manager.js +196 -0
  19. package/lib/imapproxy/imap-server.js +3 -12
  20. package/lib/oauth/gmail.js +4 -4
  21. package/lib/oauth/mail-ru.js +30 -5
  22. package/lib/oauth/outlook.js +57 -3
  23. package/lib/oauth/pubsub/google.js +30 -11
  24. package/lib/oauth/scope-checker.js +202 -0
  25. package/lib/oauth2-apps.js +8 -4
  26. package/lib/redis-operations.js +484 -0
  27. package/lib/routes-ui.js +283 -2582
  28. package/lib/tools.js +4 -196
  29. package/lib/ui-routes/account-routes.js +1931 -0
  30. package/lib/ui-routes/admin-config-routes.js +1233 -0
  31. package/lib/ui-routes/admin-entities-routes.js +2367 -0
  32. package/lib/ui-routes/oauth-routes.js +992 -0
  33. package/lib/utils/network.js +237 -0
  34. package/package.json +10 -10
  35. package/sbom.json +1 -1
  36. package/static/js/app.js +5 -5
  37. package/static/licenses.html +79 -19
  38. package/translations/de.mo +0 -0
  39. package/translations/de.po +97 -86
  40. package/translations/en.mo +0 -0
  41. package/translations/en.po +80 -75
  42. package/translations/et.mo +0 -0
  43. package/translations/et.po +96 -86
  44. package/translations/fr.mo +0 -0
  45. package/translations/fr.po +97 -86
  46. package/translations/ja.mo +0 -0
  47. package/translations/ja.po +96 -86
  48. package/translations/messages.pot +105 -91
  49. package/translations/nl.mo +0 -0
  50. package/translations/nl.po +98 -86
  51. package/translations/pl.mo +0 -0
  52. package/translations/pl.po +96 -86
  53. package/views/account/security.hbs +4 -4
  54. package/views/accounts/account.hbs +13 -13
  55. package/views/accounts/register/imap-server.hbs +12 -12
  56. package/views/config/document-store/pre-processing/index.hbs +4 -2
  57. package/views/config/oauth/app.hbs +6 -7
  58. package/views/config/oauth/index.hbs +2 -2
  59. package/views/config/service.hbs +3 -4
  60. package/views/dashboard.hbs +5 -7
  61. package/views/error.hbs +22 -7
  62. package/views/gateways/gateway.hbs +2 -2
  63. package/views/partials/add_account_modal.hbs +7 -10
  64. package/views/partials/document_store_header.hbs +1 -1
  65. package/views/partials/editor_scope_info.hbs +0 -1
  66. package/views/partials/oauth_config_header.hbs +1 -1
  67. package/views/partials/side_menu.hbs +3 -3
  68. package/views/partials/webhook_form.hbs +2 -2
  69. package/views/templates/index.hbs +1 -1
  70. package/views/templates/template.hbs +8 -8
  71. package/views/tokens/index.hbs +6 -6
  72. package/views/tokens/new.hbs +1 -1
  73. package/views/webhooks/index.hbs +4 -4
  74. package/views/webhooks/webhook.hbs +7 -7
  75. package/workers/api.js +148 -2436
  76. package/workers/smtp.js +2 -1
  77. package/lib/imapproxy/imap-core/test/client.js +0 -46
  78. package/lib/imapproxy/imap-core/test/fixtures/append.eml +0 -1196
  79. package/lib/imapproxy/imap-core/test/fixtures/chunks.js +0 -44
  80. package/lib/imapproxy/imap-core/test/fixtures/fix1.eml +0 -6
  81. package/lib/imapproxy/imap-core/test/fixtures/fix2.eml +0 -599
  82. package/lib/imapproxy/imap-core/test/fixtures/fix3.eml +0 -32
  83. package/lib/imapproxy/imap-core/test/fixtures/fix4.eml +0 -6
  84. package/lib/imapproxy/imap-core/test/fixtures/mimetorture.eml +0 -599
  85. package/lib/imapproxy/imap-core/test/fixtures/mimetorture.js +0 -2740
  86. package/lib/imapproxy/imap-core/test/fixtures/mimetorture.json +0 -1411
  87. package/lib/imapproxy/imap-core/test/fixtures/mimetree.js +0 -85
  88. package/lib/imapproxy/imap-core/test/fixtures/nodemailer.eml +0 -582
  89. package/lib/imapproxy/imap-core/test/fixtures/ryan_finnie_mime_torture.eml +0 -599
  90. package/lib/imapproxy/imap-core/test/fixtures/simple.eml +0 -42
  91. package/lib/imapproxy/imap-core/test/fixtures/simple.json +0 -164
  92. package/lib/imapproxy/imap-core/test/imap-compile-stream-test.js +0 -671
  93. package/lib/imapproxy/imap-core/test/imap-compiler-test.js +0 -272
  94. package/lib/imapproxy/imap-core/test/imap-indexer-test.js +0 -236
  95. package/lib/imapproxy/imap-core/test/imap-parser-test.js +0 -922
  96. package/lib/imapproxy/imap-core/test/memory-notifier.js +0 -129
  97. package/lib/imapproxy/imap-core/test/prepare.sh +0 -74
  98. package/lib/imapproxy/imap-core/test/protocol-test.js +0 -1756
  99. package/lib/imapproxy/imap-core/test/search-test.js +0 -1356
  100. package/lib/imapproxy/imap-core/test/test-client.js +0 -152
  101. package/lib/imapproxy/imap-core/test/test-server.js +0 -623
  102. package/lib/imapproxy/imap-core/test/tools-test.js +0 -22
  103. package/test/api-test.js +0 -899
  104. package/test/autoreply-test.js +0 -327
  105. package/test/bounce-test.js +0 -151
  106. package/test/complaint-test.js +0 -256
  107. package/test/fixtures/autoreply/LICENSE +0 -27
  108. package/test/fixtures/autoreply/rfc3834-01.eml +0 -23
  109. package/test/fixtures/autoreply/rfc3834-02.eml +0 -24
  110. package/test/fixtures/autoreply/rfc3834-03.eml +0 -26
  111. package/test/fixtures/autoreply/rfc3834-04.eml +0 -48
  112. package/test/fixtures/autoreply/rfc3834-05.eml +0 -19
  113. package/test/fixtures/autoreply/rfc3834-06.eml +0 -59
  114. package/test/fixtures/bounces/163.eml +0 -2521
  115. package/test/fixtures/bounces/fastmail.eml +0 -242
  116. package/test/fixtures/bounces/gmail.eml +0 -252
  117. package/test/fixtures/bounces/hotmail.eml +0 -655
  118. package/test/fixtures/bounces/mailru.eml +0 -121
  119. package/test/fixtures/bounces/outlook.eml +0 -1107
  120. package/test/fixtures/bounces/postfix.eml +0 -101
  121. package/test/fixtures/bounces/rambler.eml +0 -116
  122. package/test/fixtures/bounces/workmail.eml +0 -142
  123. package/test/fixtures/bounces/yahoo.eml +0 -139
  124. package/test/fixtures/bounces/zoho.eml +0 -83
  125. package/test/fixtures/bounces/zonemta.eml +0 -100
  126. package/test/fixtures/complaints/LICENSE +0 -27
  127. package/test/fixtures/complaints/amazonses.eml +0 -72
  128. package/test/fixtures/complaints/dmarc.eml +0 -59
  129. package/test/fixtures/complaints/hotmail.eml +0 -49
  130. package/test/fixtures/complaints/optout.eml +0 -40
  131. package/test/fixtures/complaints/standard-arf.eml +0 -68
  132. package/test/fixtures/complaints/yahoo.eml +0 -68
  133. package/test/oauth2-apps-test.js +0 -301
  134. package/test/sendonly-test.js +0 -160
  135. package/test/test-config.js +0 -34
  136. 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
- });