emailengine-app 2.61.0 → 2.61.2

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 (137) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/data/google-crawlers.json +1 -1
  3. package/lib/account/account-state.js +248 -0
  4. package/lib/account.js +17 -178
  5. package/lib/api-routes/account-routes.js +1006 -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 +3 -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 +5 -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 +12 -12
  35. package/sbom.json +1 -1
  36. package/static/js/app.js +5 -5
  37. package/static/licenses.html +91 -21
  38. package/translations/de.mo +0 -0
  39. package/translations/de.po +85 -82
  40. package/translations/en.mo +0 -0
  41. package/translations/en.po +63 -71
  42. package/translations/et.mo +0 -0
  43. package/translations/et.po +84 -82
  44. package/translations/fr.mo +0 -0
  45. package/translations/fr.po +85 -82
  46. package/translations/ja.mo +0 -0
  47. package/translations/ja.po +84 -82
  48. package/translations/messages.pot +67 -80
  49. package/translations/nl.mo +0 -0
  50. package/translations/nl.po +86 -82
  51. package/translations/pl.mo +0 -0
  52. package/translations/pl.po +84 -82
  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/workers/webhooks.js +6 -0
  78. package/lib/imapproxy/imap-core/test/client.js +0 -46
  79. package/lib/imapproxy/imap-core/test/fixtures/append.eml +0 -1196
  80. package/lib/imapproxy/imap-core/test/fixtures/chunks.js +0 -44
  81. package/lib/imapproxy/imap-core/test/fixtures/fix1.eml +0 -6
  82. package/lib/imapproxy/imap-core/test/fixtures/fix2.eml +0 -599
  83. package/lib/imapproxy/imap-core/test/fixtures/fix3.eml +0 -32
  84. package/lib/imapproxy/imap-core/test/fixtures/fix4.eml +0 -6
  85. package/lib/imapproxy/imap-core/test/fixtures/mimetorture.eml +0 -599
  86. package/lib/imapproxy/imap-core/test/fixtures/mimetorture.js +0 -2740
  87. package/lib/imapproxy/imap-core/test/fixtures/mimetorture.json +0 -1411
  88. package/lib/imapproxy/imap-core/test/fixtures/mimetree.js +0 -85
  89. package/lib/imapproxy/imap-core/test/fixtures/nodemailer.eml +0 -582
  90. package/lib/imapproxy/imap-core/test/fixtures/ryan_finnie_mime_torture.eml +0 -599
  91. package/lib/imapproxy/imap-core/test/fixtures/simple.eml +0 -42
  92. package/lib/imapproxy/imap-core/test/fixtures/simple.json +0 -164
  93. package/lib/imapproxy/imap-core/test/imap-compile-stream-test.js +0 -671
  94. package/lib/imapproxy/imap-core/test/imap-compiler-test.js +0 -272
  95. package/lib/imapproxy/imap-core/test/imap-indexer-test.js +0 -236
  96. package/lib/imapproxy/imap-core/test/imap-parser-test.js +0 -922
  97. package/lib/imapproxy/imap-core/test/memory-notifier.js +0 -129
  98. package/lib/imapproxy/imap-core/test/prepare.sh +0 -74
  99. package/lib/imapproxy/imap-core/test/protocol-test.js +0 -1756
  100. package/lib/imapproxy/imap-core/test/search-test.js +0 -1356
  101. package/lib/imapproxy/imap-core/test/test-client.js +0 -152
  102. package/lib/imapproxy/imap-core/test/test-server.js +0 -623
  103. package/lib/imapproxy/imap-core/test/tools-test.js +0 -22
  104. package/test/api-test.js +0 -899
  105. package/test/autoreply-test.js +0 -327
  106. package/test/bounce-test.js +0 -151
  107. package/test/complaint-test.js +0 -256
  108. package/test/fixtures/autoreply/LICENSE +0 -27
  109. package/test/fixtures/autoreply/rfc3834-01.eml +0 -23
  110. package/test/fixtures/autoreply/rfc3834-02.eml +0 -24
  111. package/test/fixtures/autoreply/rfc3834-03.eml +0 -26
  112. package/test/fixtures/autoreply/rfc3834-04.eml +0 -48
  113. package/test/fixtures/autoreply/rfc3834-05.eml +0 -19
  114. package/test/fixtures/autoreply/rfc3834-06.eml +0 -59
  115. package/test/fixtures/bounces/163.eml +0 -2521
  116. package/test/fixtures/bounces/fastmail.eml +0 -242
  117. package/test/fixtures/bounces/gmail.eml +0 -252
  118. package/test/fixtures/bounces/hotmail.eml +0 -655
  119. package/test/fixtures/bounces/mailru.eml +0 -121
  120. package/test/fixtures/bounces/outlook.eml +0 -1107
  121. package/test/fixtures/bounces/postfix.eml +0 -101
  122. package/test/fixtures/bounces/rambler.eml +0 -116
  123. package/test/fixtures/bounces/workmail.eml +0 -142
  124. package/test/fixtures/bounces/yahoo.eml +0 -139
  125. package/test/fixtures/bounces/zoho.eml +0 -83
  126. package/test/fixtures/bounces/zonemta.eml +0 -100
  127. package/test/fixtures/complaints/LICENSE +0 -27
  128. package/test/fixtures/complaints/amazonses.eml +0 -72
  129. package/test/fixtures/complaints/dmarc.eml +0 -59
  130. package/test/fixtures/complaints/hotmail.eml +0 -49
  131. package/test/fixtures/complaints/optout.eml +0 -40
  132. package/test/fixtures/complaints/standard-arf.eml +0 -68
  133. package/test/fixtures/complaints/yahoo.eml +0 -68
  134. package/test/oauth2-apps-test.js +0 -301
  135. package/test/sendonly-test.js +0 -160
  136. package/test/test-config.js +0 -34
  137. package/test/webhooks-server.js +0 -39
@@ -1,301 +0,0 @@
1
- 'use strict';
2
-
3
- require('dotenv').config({ quiet: true });
4
-
5
- const test = require('node:test');
6
- const assert = require('node:assert').strict;
7
-
8
- const { formatExtraScopes } = require('../lib/oauth2-apps');
9
- const { Account } = require('../lib/account');
10
-
11
- test('formatExtraScopes', async t => {
12
- t.after(async () => {
13
- // force close because we loaded ../lib/oauth2-apps that spawns the db connection and queues
14
- setTimeout(() => process.exit(), 5000).unref();
15
- });
16
-
17
- await t.test('should filter out Gmail scopes using short form', async () => {
18
- const defaultScopes = [
19
- 'https://www.googleapis.com/auth/gmail.modify',
20
- 'https://www.googleapis.com/auth/gmail.send',
21
- 'https://www.googleapis.com/auth/gmail.labels'
22
- ];
23
-
24
- const result = formatExtraScopes([], null, defaultScopes, ['gmail.modify'], null);
25
-
26
- assert.deepStrictEqual(result, ['https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/gmail.labels']);
27
- });
28
-
29
- await t.test('should filter out Gmail scopes using full URL', async () => {
30
- const defaultScopes = [
31
- 'https://www.googleapis.com/auth/gmail.modify',
32
- 'https://www.googleapis.com/auth/gmail.send',
33
- 'https://www.googleapis.com/auth/gmail.labels'
34
- ];
35
-
36
- const result = formatExtraScopes([], null, defaultScopes, ['https://www.googleapis.com/auth/gmail.modify'], null);
37
-
38
- assert.deepStrictEqual(result, ['https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/gmail.labels']);
39
- });
40
-
41
- await t.test('should filter multiple Gmail scopes', async () => {
42
- const defaultScopes = [
43
- 'https://www.googleapis.com/auth/gmail.modify',
44
- 'https://www.googleapis.com/auth/gmail.readonly',
45
- 'https://www.googleapis.com/auth/gmail.send',
46
- 'https://www.googleapis.com/auth/gmail.labels'
47
- ];
48
-
49
- const result = formatExtraScopes([], null, defaultScopes, ['gmail.modify', 'gmail.readonly', 'gmail.labels'], null);
50
-
51
- assert.deepStrictEqual(result, ['https://www.googleapis.com/auth/gmail.send']);
52
- });
53
-
54
- await t.test('should add extra Gmail scopes', async () => {
55
- const defaultScopes = ['https://www.googleapis.com/auth/gmail.modify'];
56
-
57
- const result = formatExtraScopes(['https://www.googleapis.com/auth/gmail.send'], null, defaultScopes, [], null);
58
-
59
- assert.deepStrictEqual(result, ['https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/gmail.modify']);
60
- });
61
-
62
- await t.test('should add extra scopes and filter out unwanted ones', async () => {
63
- const defaultScopes = ['https://www.googleapis.com/auth/gmail.modify', 'https://www.googleapis.com/auth/gmail.labels'];
64
-
65
- const result = formatExtraScopes(['https://www.googleapis.com/auth/gmail.send'], null, defaultScopes, ['gmail.modify'], null);
66
-
67
- assert.deepStrictEqual(result, ['https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/gmail.labels']);
68
- });
69
-
70
- await t.test('should handle send-only configuration', async () => {
71
- const defaultScopes = ['https://www.googleapis.com/auth/gmail.modify'];
72
-
73
- const result = formatExtraScopes(['https://www.googleapis.com/auth/gmail.send'], null, defaultScopes, ['gmail.modify'], null);
74
-
75
- assert.deepStrictEqual(result, ['https://www.googleapis.com/auth/gmail.send']);
76
- });
77
-
78
- await t.test('should filter out Outlook scopes using short form', async () => {
79
- const defaultScopes = [
80
- 'https://outlook.office.com/IMAP.AccessAsUser.All',
81
- 'https://outlook.office.com/SMTP.Send',
82
- 'https://outlook.office.com/User.Read'
83
- ];
84
-
85
- const result = formatExtraScopes([], null, defaultScopes, ['IMAP.AccessAsUser.All'], null);
86
-
87
- assert.deepStrictEqual(result, ['https://outlook.office.com/SMTP.Send', 'https://outlook.office.com/User.Read']);
88
- });
89
-
90
- await t.test('should filter out Microsoft Graph scopes', async () => {
91
- const defaultScopes = ['https://graph.microsoft.com/Mail.ReadWrite', 'https://graph.microsoft.com/Mail.Send', 'https://graph.microsoft.com/User.Read'];
92
-
93
- const result = formatExtraScopes([], null, defaultScopes, ['Mail.ReadWrite'], null);
94
-
95
- assert.deepStrictEqual(result, ['https://graph.microsoft.com/Mail.Send', 'https://graph.microsoft.com/User.Read']);
96
- });
97
-
98
- await t.test('should filter out full URL Outlook scopes', async () => {
99
- const defaultScopes = ['https://outlook.office.com/IMAP.AccessAsUser.All', 'https://outlook.office.com/SMTP.Send'];
100
-
101
- const result = formatExtraScopes([], null, defaultScopes, ['https://outlook.office.com/IMAP.AccessAsUser.All'], null);
102
-
103
- assert.deepStrictEqual(result, ['https://outlook.office.com/SMTP.Send']);
104
- });
105
-
106
- await t.test('should return default scopes when no extras or skips', async () => {
107
- const defaultScopes = ['scope1', 'scope2'];
108
-
109
- const result = formatExtraScopes(null, null, defaultScopes, [], null);
110
-
111
- assert.deepStrictEqual(result, ['scope1', 'scope2']);
112
- });
113
-
114
- await t.test('should handle empty skipScopes array', async () => {
115
- const defaultScopes = ['scope1', 'scope2'];
116
-
117
- const result = formatExtraScopes(['scope3'], null, defaultScopes, [], null);
118
-
119
- assert.deepStrictEqual(result, ['scope3', 'scope1', 'scope2']);
120
- });
121
-
122
- await t.test('should not duplicate scopes already in defaults', async () => {
123
- const defaultScopes = ['scope1', 'scope2'];
124
-
125
- const result = formatExtraScopes(['scope1', 'scope3'], null, defaultScopes, [], null);
126
-
127
- assert.deepStrictEqual(result, ['scope3', 'scope1', 'scope2']);
128
- });
129
-
130
- await t.test('should handle scopePrefix correctly', async () => {
131
- const defaultScopes = ['prefix/scope1', 'prefix/scope2'];
132
-
133
- const result = formatExtraScopes(['scope1', 'scope3'], null, defaultScopes, [], 'prefix');
134
-
135
- assert.deepStrictEqual(result, ['scope3', 'prefix/scope1', 'prefix/scope2']);
136
- });
137
- });
138
-
139
- test('checkAccountScopes - Outlook', async t => {
140
- let account;
141
-
142
- t.beforeEach(() => {
143
- // Create a mock account instance with minimal required properties
144
- account = new Account({
145
- redis: {},
146
- account: 'test-account',
147
- secret: 'test-secret',
148
- logger: {
149
- warn: () => {},
150
- error: () => {},
151
- info: () => {},
152
- debug: () => {}
153
- }
154
- });
155
- });
156
-
157
- await t.test('should detect send-only Outlook account (global cloud)', async () => {
158
- const scopes = ['https://graph.microsoft.com/Mail.Send', 'offline_access'];
159
- const result = account.checkAccountScopes('outlook', scopes);
160
-
161
- assert.deepStrictEqual(result, { hasSendScope: true, hasReadScope: false });
162
- });
163
-
164
- await t.test('should detect full-access Outlook account (global cloud)', async () => {
165
- const scopes = ['https://graph.microsoft.com/Mail.ReadWrite', 'https://graph.microsoft.com/Mail.Send', 'offline_access'];
166
- const result = account.checkAccountScopes('outlook', scopes);
167
-
168
- assert.deepStrictEqual(result, { hasSendScope: true, hasReadScope: true });
169
- });
170
-
171
- await t.test('should detect send-only Outlook account (GCC-High cloud)', async () => {
172
- const scopes = ['https://graph.microsoft.us/Mail.Send', 'offline_access'];
173
- const result = account.checkAccountScopes('outlook', scopes);
174
-
175
- assert.deepStrictEqual(result, { hasSendScope: true, hasReadScope: false });
176
- });
177
-
178
- await t.test('should detect full-access Outlook account (GCC-High cloud)', async () => {
179
- const scopes = ['https://graph.microsoft.us/Mail.ReadWrite', 'https://graph.microsoft.us/Mail.Send', 'offline_access'];
180
- const result = account.checkAccountScopes('outlook', scopes);
181
-
182
- assert.deepStrictEqual(result, { hasSendScope: true, hasReadScope: true });
183
- });
184
-
185
- await t.test('should detect send-only Outlook account (DoD cloud)', async () => {
186
- const scopes = ['https://dod-graph.microsoft.us/Mail.Send', 'offline_access'];
187
- const result = account.checkAccountScopes('outlook', scopes);
188
-
189
- assert.deepStrictEqual(result, { hasSendScope: true, hasReadScope: false });
190
- });
191
-
192
- await t.test('should detect full-access Outlook account (DoD cloud)', async () => {
193
- const scopes = ['https://dod-graph.microsoft.us/Mail.ReadWrite', 'https://dod-graph.microsoft.us/Mail.Send', 'offline_access'];
194
- const result = account.checkAccountScopes('outlook', scopes);
195
-
196
- assert.deepStrictEqual(result, { hasSendScope: true, hasReadScope: true });
197
- });
198
-
199
- await t.test('should detect send-only Outlook account (China cloud)', async () => {
200
- const scopes = ['https://microsoftgraph.chinacloudapi.cn/Mail.Send', 'offline_access'];
201
- const result = account.checkAccountScopes('outlook', scopes);
202
-
203
- assert.deepStrictEqual(result, { hasSendScope: true, hasReadScope: false });
204
- });
205
-
206
- await t.test('should detect full-access Outlook account (China cloud)', async () => {
207
- const scopes = ['https://microsoftgraph.chinacloudapi.cn/Mail.ReadWrite', 'https://microsoftgraph.chinacloudapi.cn/Mail.Send', 'offline_access'];
208
- const result = account.checkAccountScopes('outlook', scopes);
209
-
210
- assert.deepStrictEqual(result, { hasSendScope: true, hasReadScope: true });
211
- });
212
-
213
- await t.test('should detect read-only Outlook account with Mail.Read', async () => {
214
- const scopes = ['https://graph.microsoft.com/Mail.Read', 'offline_access'];
215
- const result = account.checkAccountScopes('outlook', scopes);
216
-
217
- assert.deepStrictEqual(result, { hasSendScope: false, hasReadScope: true });
218
- });
219
-
220
- await t.test('should handle scopes with trailing slashes', async () => {
221
- const scopes = ['https://graph.microsoft.com/Mail.Send/', 'offline_access'];
222
- const result = account.checkAccountScopes('outlook', scopes);
223
-
224
- assert.deepStrictEqual(result, { hasSendScope: true, hasReadScope: false });
225
- });
226
-
227
- await t.test('should handle scopes with query parameters', async () => {
228
- const scopes = ['https://graph.microsoft.com/Mail.Send?foo=bar', 'offline_access'];
229
- const result = account.checkAccountScopes('outlook', scopes);
230
-
231
- assert.deepStrictEqual(result, { hasSendScope: true, hasReadScope: false });
232
- });
233
-
234
- await t.test('should handle scopes with fragments', async () => {
235
- const scopes = ['https://graph.microsoft.com/Mail.Send#section', 'offline_access'];
236
- const result = account.checkAccountScopes('outlook', scopes);
237
-
238
- assert.deepStrictEqual(result, { hasSendScope: true, hasReadScope: false });
239
- });
240
-
241
- await t.test('should handle plain scope names', async () => {
242
- const scopes = ['offline_access', 'openid', 'profile'];
243
- const result = account.checkAccountScopes('outlook', scopes);
244
-
245
- assert.deepStrictEqual(result, { hasSendScope: false, hasReadScope: false });
246
- });
247
-
248
- await t.test('should handle invalid protocol (http instead of https)', async () => {
249
- const scopes = ['http://graph.microsoft.com/Mail.Send', 'offline_access'];
250
- const result = account.checkAccountScopes('outlook', scopes);
251
-
252
- assert.deepStrictEqual(result, { hasSendScope: false, hasReadScope: false });
253
- });
254
-
255
- await t.test('should handle non-Microsoft Graph domains', async () => {
256
- const scopes = ['https://evil.com/Mail.Send', 'offline_access'];
257
- const result = account.checkAccountScopes('outlook', scopes);
258
-
259
- assert.deepStrictEqual(result, { hasSendScope: false, hasReadScope: false });
260
- });
261
-
262
- await t.test('should handle malformed URLs', async () => {
263
- const scopes = ['not-a-url', 'offline_access'];
264
- const result = account.checkAccountScopes('outlook', scopes);
265
-
266
- assert.deepStrictEqual(result, { hasSendScope: false, hasReadScope: false });
267
- });
268
-
269
- await t.test('should handle empty scopes array', async () => {
270
- const scopes = [];
271
- const result = account.checkAccountScopes('outlook', scopes);
272
-
273
- assert.deepStrictEqual(result, { hasSendScope: false, hasReadScope: false });
274
- });
275
-
276
- await t.test('should handle null scopes', async () => {
277
- const result = account.checkAccountScopes('outlook', null);
278
-
279
- assert.deepStrictEqual(result, { hasSendScope: false, hasReadScope: false });
280
- });
281
-
282
- await t.test('should handle undefined scopes', async () => {
283
- const result = account.checkAccountScopes('outlook', undefined);
284
-
285
- assert.deepStrictEqual(result, { hasSendScope: false, hasReadScope: false });
286
- });
287
-
288
- await t.test('should handle mixed cloud scopes', async () => {
289
- const scopes = ['https://graph.microsoft.com/Mail.Send', 'https://graph.microsoft.us/Mail.ReadWrite', 'offline_access'];
290
- const result = account.checkAccountScopes('outlook', scopes);
291
-
292
- assert.deepStrictEqual(result, { hasSendScope: true, hasReadScope: true });
293
- });
294
-
295
- await t.test('should handle User.Read scope (not mail-related)', async () => {
296
- const scopes = ['https://graph.microsoft.com/User.Read', 'offline_access'];
297
- const result = account.checkAccountScopes('outlook', scopes);
298
-
299
- assert.deepStrictEqual(result, { hasSendScope: false, hasReadScope: false });
300
- });
301
- });
@@ -1,160 +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 webhooksServer = require('./webhooks-server');
11
-
12
- const accessToken = '2aa97ad0456d6624a55d30780aa2ff61bfb7edc6fa00935b40814b271e718660';
13
- const server = supertest.agent(`http://127.0.0.1:${config.api.port}`).auth(accessToken, { type: 'bearer' });
14
-
15
- const gmailSendOnlyAccountId = 'gmail-sendonly-test';
16
-
17
- // Helper function for polling with timeout
18
- async function waitForCondition(checkFn, options = {}) {
19
- const { interval = testConfig.POLL_INTERVAL, timeout = testConfig.DEFAULT_TIMEOUT, message = 'Condition not met within timeout' } = options;
20
-
21
- const startTime = Date.now();
22
-
23
- while (Date.now() - startTime < timeout) {
24
- const result = await checkFn();
25
- if (result) {
26
- return result;
27
- }
28
- await new Promise(r => setTimeout(r, interval));
29
- }
30
-
31
- throw new Error(`Timeout: ${message}`);
32
- }
33
-
34
- test('Gmail send-only account - isolated send test', async t => {
35
- let oauth2PubsubId;
36
- let oauth2SendOnlyAppId;
37
-
38
- t.before(async () => {
39
- await webhooksServer.init();
40
- });
41
-
42
- t.after(async () => {
43
- await webhooksServer.quit();
44
- });
45
-
46
- await t.test('Create Gmail API OAuth2 service project', { timeout: 30000 }, async () => {
47
- let gmailServiceData = {
48
- name: 'Gmail API Pub/Sub',
49
- provider: 'gmailService',
50
- baseScopes: 'pubsub',
51
- googleProjectId: process.env.GMAIL_API_PROJECT_ID,
52
- serviceClient: process.env.GMAIL_API_SERVICE_CLIENT,
53
- serviceClientEmail: process.env.GMAIL_API_SERVICE_EMAIL,
54
- serviceKey: process.env.GMAIL_API_SERVICE_KEY
55
- };
56
-
57
- const response = await server.post(`/v1/oauth2`).send(gmailServiceData).expect(200);
58
-
59
- oauth2PubsubId = response.body.id;
60
- assert.ok(oauth2PubsubId);
61
- });
62
-
63
- await t.test('Create Gmail send-only OAuth2 client project', { timeout: 30000 }, async () => {
64
- let gmailSendOnlyClientData = {
65
- name: 'Gmail API Send-Only Client',
66
- provider: 'gmail',
67
- baseScopes: 'api',
68
- googleProjectId: process.env.GMAIL_SENDONLY_PROJECT_ID,
69
- clientId: process.env.GMAIL_SENDONLY_CLIENT_ID,
70
- clientSecret: process.env.GMAIL_SENDONLY_CLIENT_SECRET,
71
- extraScopes: ['gmail.send'],
72
- skipScopes: ['gmail.modify'],
73
- redirectUrl: 'http://127.0.0.1:3000/oauth'
74
- };
75
-
76
- const response = await server.post(`/v1/oauth2`).send(gmailSendOnlyClientData).expect(200);
77
-
78
- oauth2SendOnlyAppId = response.body.id;
79
- assert.ok(oauth2SendOnlyAppId);
80
- });
81
-
82
- await t.test('Register Gmail send-only account', { timeout: 30000 }, async () => {
83
- const response = await server
84
- .post(`/v1/account`)
85
- .send({
86
- account: gmailSendOnlyAccountId,
87
- name: 'Gmail Send-Only Test User',
88
- email: process.env.GMAIL_SENDONLY_ACCOUNT_EMAIL,
89
- oauth2: {
90
- provider: oauth2SendOnlyAppId,
91
- auth: {
92
- user: process.env.GMAIL_SENDONLY_ACCOUNT_EMAIL
93
- },
94
- refreshToken: process.env.GMAIL_SENDONLY_ACCOUNT_REFRESH
95
- }
96
- })
97
- .expect(200);
98
-
99
- assert.strictEqual(response.body.state, 'new');
100
- });
101
-
102
- await t.test('Wait until Gmail send-only account is connected', { timeout: 180000 }, async () => {
103
- await waitForCondition(
104
- async () => {
105
- const response = await server.get(`/v1/account/${gmailSendOnlyAccountId}`).expect(200);
106
- switch (response.body.state) {
107
- case 'authenticationError':
108
- case 'connectError':
109
- throw new Error('Invalid account state ' + response.body.state);
110
- case 'connected':
111
- return true;
112
- }
113
- return false;
114
- },
115
- { timeout: testConfig.GMAIL_TIMEOUT, message: `Gmail send-only account connection timeout` }
116
- );
117
-
118
- // Verify account type is 'sending'
119
- const response = await server.get(`/v1/account/${gmailSendOnlyAccountId}`).expect(200);
120
- assert.strictEqual(response.body.sendOnly, true, 'Account should be detected as send-only');
121
- });
122
-
123
- await t.test('Submit email from send-only account (no receive check)', { timeout: 60000 }, async () => {
124
- let messageId = `<sendonly-isolated-test-${Date.now()}@example.com>`;
125
-
126
- const response = await server
127
- .post(`/v1/account/${gmailSendOnlyAccountId}/submit`)
128
- .send({
129
- to: [
130
- {
131
- name: 'Test Recipient',
132
- address: process.env.GMAIL_API_ACCOUNT_EMAIL_2 || 'test@example.com'
133
- }
134
- ],
135
- subject: 'Isolated send-only test',
136
- text: 'This is a test message from send-only account - isolated test',
137
- html: '<p>This is a test message from <strong>send-only</strong> account - isolated test</p>',
138
- messageId
139
- })
140
- .expect(200);
141
-
142
- assert.ok(response.body.messageId, 'Should have messageId in response');
143
- assert.ok(response.body.queueId, 'Should have queueId in response');
144
-
145
- // Wait for messageSent webhook only - don't check for receive
146
- const messageSentWebhook = await waitForCondition(
147
- async () => {
148
- let webhooks = webhooksServer.webhooks.get(gmailSendOnlyAccountId);
149
- return webhooks?.find(wh => wh.event === 'messageSent' && wh.data.originalMessageId === messageId);
150
- },
151
- { timeout: 30000, message: 'Gmail send-only message sent webhook timeout' }
152
- );
153
-
154
- assert.ok(messageSentWebhook, 'Should receive messageSent webhook');
155
- });
156
-
157
- await t.test('Cleanup - delete send-only account', { timeout: 10000 }, async () => {
158
- await server.delete(`/v1/account/${gmailSendOnlyAccountId}`).expect(200);
159
- });
160
- });
@@ -1,34 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * Test configuration for EmailEngine test suite
5
- * Timeout values are configured to handle Gmail API operations
6
- * which may take longer due to network latency and API processing
7
- */
8
-
9
- module.exports = {
10
- // Default timeout for standard operations
11
- DEFAULT_TIMEOUT: 30000, // 30 seconds
12
-
13
- // Timeout for Gmail API operations
14
- GMAIL_TIMEOUT: 90000, // 90 seconds
15
-
16
- // Timeout for waiting for account connections
17
- CONNECTION_TIMEOUT: 60000, // 60 seconds
18
-
19
- // Timeout for webhook notifications
20
- WEBHOOK_TIMEOUT: 30000, // 30 seconds
21
-
22
- // Polling interval for checking conditions
23
- POLL_INTERVAL: 1000, // 1 second
24
-
25
- // Environment-specific overrides
26
- ...(process.env.CI
27
- ? {
28
- // Increase timeouts in CI environment
29
- GMAIL_TIMEOUT: 120000, // 2 minutes in CI
30
- CONNECTION_TIMEOUT: 90000, // 90 seconds in CI
31
- WEBHOOK_TIMEOUT: 60000 // 60 seconds in CI
32
- }
33
- : {})
34
- };
@@ -1,39 +0,0 @@
1
- 'use strict';
2
-
3
- const config = require('@zone-eu/wild-config');
4
- const Hapi = require('@hapi/hapi');
5
-
6
- const webhooks = new Map();
7
-
8
- const server = Hapi.server({
9
- port: config.webhooksServer.port,
10
- host: '0.0.0.0'
11
- });
12
-
13
- server.route({
14
- method: 'POST',
15
- path: '/webhooks',
16
- handler: async (request, h) => {
17
- let account = request.payload.account || '';
18
- if (!webhooks.has(account)) {
19
- webhooks.set(account, []);
20
- }
21
- console.log('WEBHOOK', JSON.stringify(request.payload));
22
- webhooks.get(account).push(request.payload);
23
- return h.response('OK').code(200);
24
- }
25
- });
26
-
27
- const init = async () => {
28
- await server.start();
29
- console.log('Webhooks Server running on %s', server.info.uri);
30
- };
31
-
32
- module.exports = {
33
- init,
34
- webhooks,
35
- async quit() {
36
- await server.stop({ timeout: 5 * 1000 });
37
- console.log('Webhooks Server closed');
38
- }
39
- };