emailengine-app 2.63.4 → 2.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/test.yml +4 -0
- package/CHANGELOG.md +70 -0
- package/copy-static-files.sh +1 -1
- package/data/google-crawlers.json +1 -1
- package/eslint.config.js +2 -0
- package/lib/account.js +13 -9
- package/lib/api-routes/account-routes.js +7 -1
- package/lib/consts.js +17 -1
- package/lib/email-client/gmail/gmail-api.js +1 -12
- package/lib/email-client/imap-client.js +5 -3
- package/lib/email-client/outlook/graph-api.js +9 -15
- package/lib/email-client/outlook-client.js +406 -177
- package/lib/export.js +17 -0
- package/lib/imapproxy/imap-server.js +3 -2
- package/lib/oauth/gmail.js +12 -1
- package/lib/oauth/outlook.js +99 -1
- package/lib/oauth/pubsub/google.js +253 -85
- package/lib/oauth2-apps.js +620 -389
- package/lib/outbox.js +1 -1
- package/lib/routes-ui.js +193 -238
- package/lib/schemas.js +189 -12
- package/lib/ui-routes/account-routes.js +7 -2
- package/lib/ui-routes/admin-entities-routes.js +3 -3
- package/lib/ui-routes/oauth-routes.js +27 -175
- package/package.json +21 -21
- package/sbom.json +1 -1
- package/server.js +54 -22
- package/static/licenses.html +30 -90
- package/translations/de.mo +0 -0
- package/translations/de.po +54 -42
- package/translations/en.mo +0 -0
- package/translations/en.po +55 -43
- package/translations/et.mo +0 -0
- package/translations/et.po +54 -42
- package/translations/fr.mo +0 -0
- package/translations/fr.po +54 -42
- package/translations/ja.mo +0 -0
- package/translations/ja.po +54 -42
- package/translations/messages.pot +93 -71
- package/translations/nl.mo +0 -0
- package/translations/nl.po +54 -42
- package/translations/pl.mo +0 -0
- package/translations/pl.po +54 -42
- package/views/config/oauth/app.hbs +12 -0
- package/views/config/oauth/edit.hbs +2 -0
- package/views/config/oauth/index.hbs +4 -1
- package/views/config/oauth/new.hbs +2 -0
- package/views/config/oauth/subscriptions.hbs +175 -0
- package/views/error.hbs +4 -4
- package/views/partials/oauth_form.hbs +179 -4
- package/views/partials/oauth_tabs.hbs +8 -0
- package/views/partials/scope_info.hbs +10 -0
- package/workers/api.js +174 -96
- package/workers/documents.js +1 -0
- package/workers/export.js +6 -2
- package/workers/imap.js +33 -49
- package/workers/smtp.js +1 -0
- package/workers/submit.js +1 -0
- package/workers/webhooks.js +42 -30
|
@@ -1,3 +1,77 @@
|
|
|
1
|
+
{{#if activeOutlook}}
|
|
2
|
+
<div class="card border-left-info shadow mb-4">
|
|
3
|
+
<div class="card-body">
|
|
4
|
+
<div class="row no-gutters align-items-center">
|
|
5
|
+
<div class="col mr-2">
|
|
6
|
+
<strong>Delegated</strong> access means EmailEngine acts on behalf of a signed-in user. Each
|
|
7
|
+
mailbox owner must complete an interactive OAuth2 login. Use this when users manage their own
|
|
8
|
+
accounts or when you have access to the mailbox credentials.
|
|
9
|
+
{{#if providerData.tutorialUrl}}
|
|
10
|
+
Read about setting up {{providerData.comment}} OAuth2 project from <a
|
|
11
|
+
href="{{providerData.tutorialUrl}}" target="_blank" rel="noopener noreferrer"
|
|
12
|
+
referrerpolicy="no-referrer">here</a>.
|
|
13
|
+
{{/if}}
|
|
14
|
+
</div>
|
|
15
|
+
<div class="col-auto">
|
|
16
|
+
<i class="fas fa-info-circle fa-2x text-gray-300"></i>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
{{/if}}
|
|
22
|
+
|
|
23
|
+
{{#if activeGmailService}}
|
|
24
|
+
<div class="card border-left-info shadow mb-4">
|
|
25
|
+
<div class="card-body">
|
|
26
|
+
<div class="row no-gutters align-items-center">
|
|
27
|
+
<div class="col mr-2">
|
|
28
|
+
<strong>Service accounts</strong> allow EmailEngine to access Gmail mailboxes using a Google Cloud
|
|
29
|
+
service key, with no interactive user login required. The service account must have domain-wide
|
|
30
|
+
delegation enabled in Google Workspace. Use this for automated integrations where you cannot
|
|
31
|
+
perform interactive logins.
|
|
32
|
+
Accounts using service access can only be added via the
|
|
33
|
+
<a href="/admin/swagger#/Account/postV1Account">REST API</a>,
|
|
34
|
+
not through the hosted authentication form.
|
|
35
|
+
{{#if providerData.tutorialUrl}}
|
|
36
|
+
Read about setting up {{providerData.comment}} from <a
|
|
37
|
+
href="{{providerData.tutorialUrl}}" target="_blank" rel="noopener noreferrer"
|
|
38
|
+
referrerpolicy="no-referrer">here</a>.
|
|
39
|
+
{{/if}}
|
|
40
|
+
</div>
|
|
41
|
+
<div class="col-auto">
|
|
42
|
+
<i class="fas fa-info-circle fa-2x text-gray-300"></i>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
{{/if}}
|
|
48
|
+
|
|
49
|
+
{{#if activeOutlookService}}
|
|
50
|
+
<div class="card border-left-info shadow mb-4">
|
|
51
|
+
<div class="card-body">
|
|
52
|
+
<div class="row no-gutters align-items-center">
|
|
53
|
+
<div class="col mr-2">
|
|
54
|
+
<strong>Application</strong> access means EmailEngine authenticates as the app itself using
|
|
55
|
+
client credentials, with no interactive user login required. The Entra app registration must have
|
|
56
|
+
<strong>application permissions</strong> (not delegated) with admin consent. Use this for
|
|
57
|
+
service integrations, shared mailboxes, or when you cannot perform interactive logins.
|
|
58
|
+
Accounts using application access can only be added via the
|
|
59
|
+
<a href="/admin/swagger#/Account/postV1Account">REST API</a>,
|
|
60
|
+
not through the hosted authentication form.
|
|
61
|
+
{{#if providerData.tutorialUrl}}
|
|
62
|
+
Read about setting up {{providerData.comment}} from <a
|
|
63
|
+
href="{{providerData.tutorialUrl}}" target="_blank" rel="noopener noreferrer"
|
|
64
|
+
referrerpolicy="no-referrer">here</a>.
|
|
65
|
+
{{/if}}
|
|
66
|
+
</div>
|
|
67
|
+
<div class="col-auto">
|
|
68
|
+
<i class="fas fa-info-circle fa-2x text-gray-300"></i>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
{{/if}}
|
|
74
|
+
|
|
1
75
|
<div class="card mb-4">
|
|
2
76
|
|
|
3
77
|
<div class="card-body">
|
|
@@ -42,7 +116,7 @@
|
|
|
42
116
|
<small class="form-text text-muted">Optional application description or a comment.</small>
|
|
43
117
|
</div>
|
|
44
118
|
|
|
45
|
-
{{#unless activeGmailService}}
|
|
119
|
+
{{#unless activeGmailService}}{{#unless activeOutlookService}}
|
|
46
120
|
<div class="form-group">
|
|
47
121
|
|
|
48
122
|
<label for="title">
|
|
@@ -63,7 +137,7 @@
|
|
|
63
137
|
<small class="form-text text-muted">Optional display title next to the application button on the account
|
|
64
138
|
type selection page.</small>
|
|
65
139
|
</div>
|
|
66
|
-
{{/unless}}
|
|
140
|
+
{{/unless}}{{/unless}}
|
|
67
141
|
|
|
68
142
|
|
|
69
143
|
{{#if activeGmail}}
|
|
@@ -82,7 +156,7 @@
|
|
|
82
156
|
</div>
|
|
83
157
|
{{/if}}
|
|
84
158
|
|
|
85
|
-
{{#unless activeGmailService}}
|
|
159
|
+
{{#unless activeGmailService}}{{#unless activeOutlookService}}
|
|
86
160
|
<div class="form-group form-check">
|
|
87
161
|
|
|
88
162
|
<input type="checkbox" class="form-check-input {{#if errors.enabled}}is-invalid{{/if}}" id="enabled"
|
|
@@ -94,7 +168,7 @@
|
|
|
94
168
|
<small class="form-text text-muted">If enabled, then this OAuth2 app is shown as an
|
|
95
169
|
account type option in the hosted authentication form.</small>
|
|
96
170
|
</div>
|
|
97
|
-
{{/unless}}
|
|
171
|
+
{{/unless}}{{/unless}}
|
|
98
172
|
|
|
99
173
|
</div>
|
|
100
174
|
</div>
|
|
@@ -175,6 +249,66 @@
|
|
|
175
249
|
{{/if}}
|
|
176
250
|
</div>
|
|
177
251
|
|
|
252
|
+
{{else if activeOutlookService}}
|
|
253
|
+
|
|
254
|
+
<div class="form-group">
|
|
255
|
+
|
|
256
|
+
<label for="clientId">
|
|
257
|
+
Azure Application Id
|
|
258
|
+
</label>
|
|
259
|
+
<input type="text" class="form-control {{#if errors.clientId}}is-invalid{{/if}}" id="clientId"
|
|
260
|
+
name="clientId" value="{{values.clientId}}" placeholder="Enter application (client) ID…" required />
|
|
261
|
+
{{#if errors.clientId}}
|
|
262
|
+
<span class="invalid-feedback">{{errors.clientId}}</span>
|
|
263
|
+
{{/if}}
|
|
264
|
+
<small class="form-text text-muted">The Application (client) ID from your Entra app registration.</small>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
<div class="form-group">
|
|
268
|
+
|
|
269
|
+
<label for="clientSecret">Client Secret</label>
|
|
270
|
+
|
|
271
|
+
<input type="text" class="form-control {{#if errors.clientSecret}}is-invalid{{/if}}" id="clientSecret"
|
|
272
|
+
name="clientSecret" value="{{values.clientSecret}}" {{#if hasClientSecret}}
|
|
273
|
+
placeholder="Client secret is set but not shown…" {{else}} placeholder="Enter client secret…"
|
|
274
|
+
{{/if}} {{#unless hasClientSecret}}required{{/unless}} />
|
|
275
|
+
{{#if errors.clientSecret}}
|
|
276
|
+
<span class="invalid-feedback">{{errors.clientSecret}}</span>
|
|
277
|
+
{{/if}}
|
|
278
|
+
<small class="form-text text-muted">Client secret value from Certificates & secrets.</small>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<div class="form-group">
|
|
282
|
+
<label for="cloud">Azure cloud environment</label>
|
|
283
|
+
<select class="custom-select custom-select-sm {{#if errors.cloud}}is-invalid{{/if}}" id="cloud" name="cloud"
|
|
284
|
+
required>
|
|
285
|
+
|
|
286
|
+
{{#each azureClouds}}
|
|
287
|
+
<option value="{{id}}" {{#if selected}}selected{{/if}}>{{name}}{{#if
|
|
288
|
+
description}} —
|
|
289
|
+
{{description}}{{/if}}</option>
|
|
290
|
+
{{/each}}
|
|
291
|
+
</select>
|
|
292
|
+
{{#if errors.cloud}}
|
|
293
|
+
<span class="invalid-feedback">{{errors.cloud}}</span>
|
|
294
|
+
{{/if}}
|
|
295
|
+
<small class="form-text text-muted">Accounts hosted in different cloud environments use different OAuth2
|
|
296
|
+
endpoints.</small>
|
|
297
|
+
</div>
|
|
298
|
+
|
|
299
|
+
<div class="form-group">
|
|
300
|
+
<label for="authority">Directory (tenant) ID</label>
|
|
301
|
+
|
|
302
|
+
<input type="text" class="form-control {{#if errors.authority}}is-invalid{{/if}}" id="authority"
|
|
303
|
+
name="authority" value="{{values.authority}}"
|
|
304
|
+
placeholder="Enter tenant ID, eg. "f8cdef31-a31e-4b4a-93e4-5f571e91255a"" required />
|
|
305
|
+
{{#if errors.authority}}
|
|
306
|
+
<span class="invalid-feedback">{{errors.authority}}</span>
|
|
307
|
+
{{/if}}
|
|
308
|
+
<small class="form-text text-muted">The Directory (tenant) ID from your Entra app registration. Client
|
|
309
|
+
credentials flow requires a specific tenant ID.</small>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
178
312
|
{{else}}
|
|
179
313
|
|
|
180
314
|
{{#if activeGmail}}
|
|
@@ -702,6 +836,47 @@
|
|
|
702
836
|
|
|
703
837
|
{{/if}}
|
|
704
838
|
|
|
839
|
+
{{#if activeOutlookService}}
|
|
840
|
+
|
|
841
|
+
<div class="card mb-4">
|
|
842
|
+
|
|
843
|
+
<div class="card-header py-3">
|
|
844
|
+
<h6 class="m-0 font-weight-bold text-primary">Connection type</h6>
|
|
845
|
+
</div>
|
|
846
|
+
|
|
847
|
+
<div class="card-body">
|
|
848
|
+
<input type="hidden" name="baseScopes" value="api" />
|
|
849
|
+
<p><strong>MS Graph API</strong> — This app uses Microsoft Graph API with application permissions
|
|
850
|
+
(client credentials). Requires <code>Mail.ReadWrite</code>, <code>Mail.Send</code>, and
|
|
851
|
+
<code>User.Read.All</code> application permissions with admin consent in Entra.</p>
|
|
852
|
+
</div>
|
|
853
|
+
|
|
854
|
+
<div class="card-footer">
|
|
855
|
+
|
|
856
|
+
<p>Microsoft Graph delivers change notifications to EmailEngine at these two endpoints:</p>
|
|
857
|
+
|
|
858
|
+
<pre><code>{{mainServiceUrl}}/oauth/msg/lifecycle
|
|
859
|
+
{{mainServiceUrl}}/oauth/msg/notification</code></pre>
|
|
860
|
+
|
|
861
|
+
<p>
|
|
862
|
+
Both endpoints must be publicly reachable for anonymous HTTPS
|
|
863
|
+
<strong>POST</strong> requests. When a notification arrives, EmailEngine
|
|
864
|
+
immediately forwards the event to your application through its own webhooks.
|
|
865
|
+
</p>
|
|
866
|
+
|
|
867
|
+
<p>
|
|
868
|
+
If your EmailEngine instance cannot be exposed directly, configure a public
|
|
869
|
+
proxy domain in <span class="text-muted code-link"><a href="/admin/swagger#/Settings/postV1Settings"
|
|
870
|
+
target="_blank" rel="noopener noreferrer">notificationBaseUrl</a></span>. Microsoft Graph will then
|
|
871
|
+
post to <code>https://<proxy-domain>/oauth/msg/...</code> instead of
|
|
872
|
+
<code>{{mainServiceUrl}}</code>.
|
|
873
|
+
</p>
|
|
874
|
+
|
|
875
|
+
</div>
|
|
876
|
+
</div>
|
|
877
|
+
|
|
878
|
+
{{/if}}
|
|
879
|
+
|
|
705
880
|
{{#if activeGmail}}
|
|
706
881
|
<div class="card mb-4 {{#unless baseScopesApi}}d-none{{/unless}}" id="account-type-card-gmail">
|
|
707
882
|
<div class="card-header py-3">
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<ul class="nav nav-pills mb-4">
|
|
2
|
+
<li class="nav-item">
|
|
3
|
+
<a class="nav-link {{#if activeApplications}}active{{/if}}" href="/admin/config/oauth">Applications</a>
|
|
4
|
+
</li>
|
|
5
|
+
<li class="nav-item">
|
|
6
|
+
<a class="nav-link {{#if activeSubscriptions}}active{{/if}}" href="/admin/config/oauth/subscriptions">Gmail Subscriptions</a>
|
|
7
|
+
</li>
|
|
8
|
+
</ul>
|
|
@@ -119,6 +119,16 @@
|
|
|
119
119
|
</div>
|
|
120
120
|
{{/if}}
|
|
121
121
|
|
|
122
|
+
{{#if activeOutlookService}}
|
|
123
|
+
<div>Your Entra app registration <strong>must</strong> have the following <strong>application permissions</strong> (not
|
|
124
|
+
delegated) with admin consent:</div>
|
|
125
|
+
<ul>
|
|
126
|
+
<li><code>"Mail.ReadWrite"</code></li>
|
|
127
|
+
<li><code>"Mail.Send"</code></li>
|
|
128
|
+
<li><code>"User.Read.All"</code></li>
|
|
129
|
+
</ul>
|
|
130
|
+
{{/if}}
|
|
131
|
+
|
|
122
132
|
{{#if activeMailRu}}
|
|
123
133
|
<div>Your OAuth2 project <strong>must</strong> have the following scopes enabled:</div>
|
|
124
134
|
<ul>
|
package/workers/api.js
CHANGED
|
@@ -163,7 +163,8 @@ const {
|
|
|
163
163
|
googleSubscriptionNameSchema,
|
|
164
164
|
messageReferenceSchema,
|
|
165
165
|
idempotencyKeySchema,
|
|
166
|
-
headerTimeoutSchema
|
|
166
|
+
headerTimeoutSchema,
|
|
167
|
+
pubSubErrorSchema
|
|
167
168
|
} = require('../lib/schemas');
|
|
168
169
|
|
|
169
170
|
const OAuth2ProviderSchema = Joi.string()
|
|
@@ -180,6 +181,21 @@ const AccountTypeSchema = Joi.string()
|
|
|
180
181
|
.required()
|
|
181
182
|
.label('AccountType');
|
|
182
183
|
|
|
184
|
+
function flattenOAuthAppMeta(app) {
|
|
185
|
+
if (!app.meta) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
let authFlag = app.meta.authFlag;
|
|
189
|
+
let pubSubFlag = app.meta.pubSubFlag;
|
|
190
|
+
delete app.meta;
|
|
191
|
+
if (authFlag && authFlag.message) {
|
|
192
|
+
app.lastError = { response: authFlag.message };
|
|
193
|
+
}
|
|
194
|
+
if (pubSubFlag && pubSubFlag.message) {
|
|
195
|
+
app.pubSubError = { message: pubSubFlag.message, description: pubSubFlag.description || null };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
183
199
|
const SUPPORTED_LOCALES = locales.map(locale => locale.locale);
|
|
184
200
|
|
|
185
201
|
const FLAG_SORT_ORDER = ['\\Inbox', '\\Flagged', '\\Sent', '\\Drafts', '\\All', '\\Archive', '\\Junk', '\\Trash'];
|
|
@@ -362,6 +378,7 @@ async function call(message, transferList) {
|
|
|
362
378
|
err.statusCode = 504;
|
|
363
379
|
err.code = 'Timeout';
|
|
364
380
|
err.ttl = ttl;
|
|
381
|
+
callQueue.delete(mid);
|
|
365
382
|
reject(err);
|
|
366
383
|
}, ttl);
|
|
367
384
|
|
|
@@ -1557,11 +1574,12 @@ Include your token in requests using one of these methods:
|
|
|
1557
1574
|
let checkKey = `${REDIS_PREFIX}test:${Date.now()}`;
|
|
1558
1575
|
let expected = crypto.randomBytes(8).toString('hex');
|
|
1559
1576
|
let res = await redis.multi().set(checkKey, expected).get(checkKey).del(checkKey).exec();
|
|
1560
|
-
if (res[1] && res[1][1] === expected && res[2] && res[2][1] === 1) {
|
|
1561
|
-
|
|
1577
|
+
if (!(res[1] && res[1][1] === expected && res[2] && res[2][1] === 1)) {
|
|
1578
|
+
let error = Boom.boomify(new Error('Database check failed'), { statusCode: 500 });
|
|
1579
|
+
throw error;
|
|
1562
1580
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1581
|
+
|
|
1582
|
+
return { success: true };
|
|
1565
1583
|
},
|
|
1566
1584
|
options: {
|
|
1567
1585
|
description: 'Health check',
|
|
@@ -1570,6 +1588,99 @@ Include your token in requests using one of these methods:
|
|
|
1570
1588
|
}
|
|
1571
1589
|
});
|
|
1572
1590
|
|
|
1591
|
+
server.route({
|
|
1592
|
+
method: 'GET',
|
|
1593
|
+
path: '/v1/pubsub/status',
|
|
1594
|
+
|
|
1595
|
+
async handler(request) {
|
|
1596
|
+
try {
|
|
1597
|
+
let response = await oauth2Apps.list(request.query.page, request.query.pageSize, { pubsub: true });
|
|
1598
|
+
|
|
1599
|
+
let apps = response.apps.map(app => {
|
|
1600
|
+
flattenOAuthAppMeta(app);
|
|
1601
|
+
return { id: app.id, name: app.name || null, lastError: app.lastError || null, pubSubError: app.pubSubError || null };
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
return {
|
|
1605
|
+
total: response.total,
|
|
1606
|
+
page: response.page,
|
|
1607
|
+
pages: response.pages,
|
|
1608
|
+
apps
|
|
1609
|
+
};
|
|
1610
|
+
} catch (err) {
|
|
1611
|
+
request.logger.error({ msg: 'API request failed', err });
|
|
1612
|
+
if (Boom.isBoom(err)) {
|
|
1613
|
+
throw err;
|
|
1614
|
+
}
|
|
1615
|
+
let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
|
|
1616
|
+
if (err.code) {
|
|
1617
|
+
error.output.payload.code = err.code;
|
|
1618
|
+
}
|
|
1619
|
+
throw error;
|
|
1620
|
+
}
|
|
1621
|
+
},
|
|
1622
|
+
|
|
1623
|
+
options: {
|
|
1624
|
+
description: 'List Pub/Sub status',
|
|
1625
|
+
notes: 'Lists Pub/Sub enabled OAuth2 applications and their subscription status',
|
|
1626
|
+
tags: ['api', 'OAuth2 Applications'],
|
|
1627
|
+
|
|
1628
|
+
plugins: {},
|
|
1629
|
+
|
|
1630
|
+
auth: {
|
|
1631
|
+
strategy: 'api-token',
|
|
1632
|
+
mode: 'required'
|
|
1633
|
+
},
|
|
1634
|
+
cors: CORS_CONFIG,
|
|
1635
|
+
|
|
1636
|
+
validate: {
|
|
1637
|
+
options: {
|
|
1638
|
+
stripUnknown: false,
|
|
1639
|
+
abortEarly: false,
|
|
1640
|
+
convert: true
|
|
1641
|
+
},
|
|
1642
|
+
failAction,
|
|
1643
|
+
|
|
1644
|
+
query: Joi.object({
|
|
1645
|
+
page: Joi.number()
|
|
1646
|
+
.integer()
|
|
1647
|
+
.min(0)
|
|
1648
|
+
.max(1024 * 1024)
|
|
1649
|
+
.default(0)
|
|
1650
|
+
.example(0)
|
|
1651
|
+
.description('Page number (zero indexed, so use 0 for first page)')
|
|
1652
|
+
.label('PageNumber'),
|
|
1653
|
+
pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
|
|
1654
|
+
}).label('PubSubStatusFilter')
|
|
1655
|
+
},
|
|
1656
|
+
|
|
1657
|
+
response: {
|
|
1658
|
+
schema: Joi.object({
|
|
1659
|
+
total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
|
|
1660
|
+
page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
|
|
1661
|
+
pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
|
|
1662
|
+
|
|
1663
|
+
apps: Joi.array()
|
|
1664
|
+
.items(
|
|
1665
|
+
Joi.object({
|
|
1666
|
+
id: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
|
|
1667
|
+
name: Joi.string().allow(null).max(256).example('My Gmail App').description('Display name for the app'),
|
|
1668
|
+
lastError: Joi.object({
|
|
1669
|
+
response: Joi.string().example('Enable the Cloud Pub/Sub API').description('Setup error message')
|
|
1670
|
+
})
|
|
1671
|
+
.allow(null)
|
|
1672
|
+
.description('Setup error from ensurePubsub, if any')
|
|
1673
|
+
.label('PubSubSetupError'),
|
|
1674
|
+
pubSubError: pubSubErrorSchema.allow(null)
|
|
1675
|
+
}).label('PubSubAppStatus')
|
|
1676
|
+
)
|
|
1677
|
+
.label('PubSubAppStatusList')
|
|
1678
|
+
}).label('PubSubStatusResponse'),
|
|
1679
|
+
failAction: 'log'
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
});
|
|
1683
|
+
|
|
1573
1684
|
server.route({
|
|
1574
1685
|
method: 'GET',
|
|
1575
1686
|
path: '/redirect',
|
|
@@ -1936,6 +2047,10 @@ Include your token in requests using one of these methods:
|
|
|
1936
2047
|
|
|
1937
2048
|
const outlookSubscription = accountData.outlookSubscription;
|
|
1938
2049
|
|
|
2050
|
+
// Deduplicate lifecycle events within the same batch to prevent
|
|
2051
|
+
// concurrent handlers racing (e.g., two subscriptionRemoved entries)
|
|
2052
|
+
const seenLifecycleEvents = new Set();
|
|
2053
|
+
|
|
1939
2054
|
for (let entry of (request.payload && request.payload.value) || []) {
|
|
1940
2055
|
request.logger.debug({
|
|
1941
2056
|
msg: 'MS Graph subscription event',
|
|
@@ -1960,82 +2075,43 @@ Include your token in requests using one of these methods:
|
|
|
1960
2075
|
continue;
|
|
1961
2076
|
}
|
|
1962
2077
|
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
2078
|
+
// Route recognized lifecycle events to the IMAP worker
|
|
2079
|
+
// so the live client with its OAuth state handles them
|
|
2080
|
+
if (entry.lifecycleEvent === 'reauthorizationRequired' || entry.lifecycleEvent === 'subscriptionRemoved') {
|
|
2081
|
+
const dedupeKey = `${entry.lifecycleEvent}:${entry.subscriptionId}`;
|
|
2082
|
+
if (seenLifecycleEvents.has(dedupeKey)) {
|
|
2083
|
+
request.logger.debug({
|
|
2084
|
+
msg: 'Skipping duplicate lifecycle event in batch',
|
|
2085
|
+
lifecycleEvent: entry.lifecycleEvent,
|
|
2086
|
+
subscriptionId: entry.subscriptionId,
|
|
1969
2087
|
account: request.query.account
|
|
1970
2088
|
});
|
|
1971
|
-
|
|
1972
|
-
// Use the unified renewal method from OutlookClient
|
|
1973
|
-
// We need to create a client instance to call the renewal method
|
|
1974
|
-
const { OutlookClient } = require('../lib/email-client/outlook-client');
|
|
1975
|
-
const client = new OutlookClient(accountData, {
|
|
1976
|
-
redis,
|
|
1977
|
-
secret: await getSecret(),
|
|
1978
|
-
logger: request.logger
|
|
1979
|
-
});
|
|
1980
|
-
|
|
1981
|
-
try {
|
|
1982
|
-
// Force renewal when we get reauthorizationRequired
|
|
1983
|
-
const renewalResult = await client.renewSubscription(true);
|
|
1984
|
-
|
|
1985
|
-
if (renewalResult.success) {
|
|
1986
|
-
request.logger.info({
|
|
1987
|
-
msg: 'Successfully renewed subscription from lifecycle event',
|
|
1988
|
-
subscriptionId: outlookSubscription.id,
|
|
1989
|
-
account: request.query.account,
|
|
1990
|
-
newExpirationDateTime: renewalResult.expirationDateTime
|
|
1991
|
-
});
|
|
1992
|
-
} else {
|
|
1993
|
-
request.logger.error({
|
|
1994
|
-
msg: 'Failed to renew subscription from lifecycle event',
|
|
1995
|
-
subscriptionId: outlookSubscription.id,
|
|
1996
|
-
account: request.query.account,
|
|
1997
|
-
reason: renewalResult.reason,
|
|
1998
|
-
error: renewalResult.error
|
|
1999
|
-
});
|
|
2000
|
-
}
|
|
2001
|
-
} catch (err) {
|
|
2002
|
-
request.logger.error({
|
|
2003
|
-
msg: 'Exception while renewing subscription from lifecycle event',
|
|
2004
|
-
subscriptionId: outlookSubscription.id,
|
|
2005
|
-
account: request.query.account,
|
|
2006
|
-
err
|
|
2007
|
-
});
|
|
2008
|
-
} finally {
|
|
2009
|
-
// Clean up client instance
|
|
2010
|
-
if (client && typeof client.close === 'function') {
|
|
2011
|
-
try {
|
|
2012
|
-
await client.close();
|
|
2013
|
-
} catch (cleanupErr) {
|
|
2014
|
-
request.logger.debug({
|
|
2015
|
-
msg: 'Error closing client after lifecycle renewal',
|
|
2016
|
-
account: request.query.account,
|
|
2017
|
-
err: cleanupErr
|
|
2018
|
-
});
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2021
|
-
}
|
|
2022
|
-
|
|
2023
|
-
break;
|
|
2089
|
+
continue;
|
|
2024
2090
|
}
|
|
2091
|
+
seenLifecycleEvents.add(dedupeKey);
|
|
2025
2092
|
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2093
|
+
request.logger.info({
|
|
2094
|
+
msg: 'Received lifecycle event',
|
|
2095
|
+
lifecycleEvent: entry.lifecycleEvent,
|
|
2096
|
+
subscriptionId: outlookSubscription.id,
|
|
2097
|
+
account: request.query.account
|
|
2098
|
+
});
|
|
2099
|
+
|
|
2100
|
+
// Fire-and-forget: return HTTP 202 immediately so Microsoft
|
|
2101
|
+
// does not time out the lifecycle webhook delivery
|
|
2102
|
+
call({
|
|
2103
|
+
cmd: 'subscriptionLifecycle',
|
|
2104
|
+
account: request.query.account,
|
|
2105
|
+
event: entry.lifecycleEvent,
|
|
2106
|
+
timeout: consts.OUTLOOK_SUBSCRIPTION_LOCK_TTL
|
|
2107
|
+
}).catch(err => {
|
|
2108
|
+
request.logger.error({
|
|
2109
|
+
msg: 'Failed to handle lifecycle event via worker',
|
|
2110
|
+
account: request.query.account,
|
|
2111
|
+
lifecycleEvent: entry.lifecycleEvent,
|
|
2112
|
+
err
|
|
2036
2113
|
});
|
|
2037
|
-
|
|
2038
|
-
}
|
|
2114
|
+
});
|
|
2039
2115
|
}
|
|
2040
2116
|
}
|
|
2041
2117
|
|
|
@@ -4876,13 +4952,7 @@ Include your token in requests using one of these methods:
|
|
|
4876
4952
|
delete app.app;
|
|
4877
4953
|
}
|
|
4878
4954
|
|
|
4879
|
-
|
|
4880
|
-
let authFlag = app.meta.authFlag;
|
|
4881
|
-
delete app.meta;
|
|
4882
|
-
if (authFlag && authFlag.message) {
|
|
4883
|
-
app.lastError = { response: authFlag.message };
|
|
4884
|
-
}
|
|
4885
|
-
}
|
|
4955
|
+
flattenOAuthAppMeta(app);
|
|
4886
4956
|
}
|
|
4887
4957
|
|
|
4888
4958
|
return response;
|
|
@@ -5002,7 +5072,8 @@ Include your token in requests using one of these methods:
|
|
|
5002
5072
|
.example('******')
|
|
5003
5073
|
.description('PEM formatted service secret for 2-legged OAuth2 applications. Actual value is not revealed.'),
|
|
5004
5074
|
|
|
5005
|
-
lastError: lastErrorSchema.allow(null)
|
|
5075
|
+
lastError: lastErrorSchema.allow(null),
|
|
5076
|
+
pubSubError: pubSubErrorSchema.allow(null)
|
|
5006
5077
|
}).label('OAuth2ResponseItem')
|
|
5007
5078
|
)
|
|
5008
5079
|
.label('OAuth2Entries')
|
|
@@ -5035,13 +5106,7 @@ Include your token in requests using one of these methods:
|
|
|
5035
5106
|
delete app.app;
|
|
5036
5107
|
}
|
|
5037
5108
|
|
|
5038
|
-
|
|
5039
|
-
let authFlag = app.meta.authFlag;
|
|
5040
|
-
delete app.meta;
|
|
5041
|
-
if (authFlag && authFlag.message) {
|
|
5042
|
-
app.lastError = { response: authFlag.message };
|
|
5043
|
-
}
|
|
5044
|
-
}
|
|
5109
|
+
flattenOAuthAppMeta(app);
|
|
5045
5110
|
|
|
5046
5111
|
return app;
|
|
5047
5112
|
} catch (err) {
|
|
@@ -5145,7 +5210,8 @@ Include your token in requests using one of these methods:
|
|
|
5145
5210
|
.example(12)
|
|
5146
5211
|
.description('The number of accounts registered with this application. Not available for legacy apps.'),
|
|
5147
5212
|
|
|
5148
|
-
lastError: lastErrorSchema.allow(null)
|
|
5213
|
+
lastError: lastErrorSchema.allow(null),
|
|
5214
|
+
pubSubError: pubSubErrorSchema.allow(null)
|
|
5149
5215
|
}).label('ApplicationResponse'),
|
|
5150
5216
|
failAction: 'log'
|
|
5151
5217
|
}
|
|
@@ -5160,8 +5226,9 @@ Include your token in requests using one of these methods:
|
|
|
5160
5226
|
try {
|
|
5161
5227
|
let result = await oauth2Apps.create(request.payload);
|
|
5162
5228
|
|
|
5163
|
-
if (result && result.pubsubUpdates && result.pubsubUpdates.
|
|
5229
|
+
if (result && result.pubsubUpdates && Object.keys(result.pubsubUpdates).length > 0) {
|
|
5164
5230
|
await call({ cmd: 'googlePubSub', app: result.id });
|
|
5231
|
+
delete result.pubsubUpdates;
|
|
5165
5232
|
}
|
|
5166
5233
|
|
|
5167
5234
|
return result;
|
|
@@ -5220,8 +5287,9 @@ Include your token in requests using one of these methods:
|
|
|
5220
5287
|
try {
|
|
5221
5288
|
let result = await oauth2Apps.update(request.params.app, request.payload);
|
|
5222
5289
|
|
|
5223
|
-
if (result && result.pubsubUpdates && result.pubsubUpdates.
|
|
5290
|
+
if (result && result.pubsubUpdates && Object.keys(result.pubsubUpdates).length > 0) {
|
|
5224
5291
|
await call({ cmd: 'googlePubSub', app: result.id });
|
|
5292
|
+
delete result.pubsubUpdates;
|
|
5225
5293
|
}
|
|
5226
5294
|
|
|
5227
5295
|
return result;
|
|
@@ -5369,7 +5437,15 @@ Include your token in requests using one of these methods:
|
|
|
5369
5437
|
|
|
5370
5438
|
async handler(request) {
|
|
5371
5439
|
try {
|
|
5372
|
-
|
|
5440
|
+
let result = await oauth2Apps.del(request.params.app);
|
|
5441
|
+
|
|
5442
|
+
try {
|
|
5443
|
+
await call({ cmd: 'googlePubSubRemove', app: request.params.app });
|
|
5444
|
+
} catch (err) {
|
|
5445
|
+
request.logger.error({ msg: 'Failed to notify workers about OAuth2 app deletion', err, app: request.params.app });
|
|
5446
|
+
}
|
|
5447
|
+
|
|
5448
|
+
return result;
|
|
5373
5449
|
} catch (err) {
|
|
5374
5450
|
request.logger.error({ msg: 'API request failed', err });
|
|
5375
5451
|
if (Boom.isBoom(err)) {
|
|
@@ -6942,11 +7018,13 @@ ${now}`,
|
|
|
6942
7018
|
// Replace error with friendly HTML
|
|
6943
7019
|
const error = response;
|
|
6944
7020
|
const ctx = {
|
|
7021
|
+
statusCode: error.output.statusCode,
|
|
6945
7022
|
message:
|
|
6946
7023
|
error.output.statusCode === 404
|
|
6947
7024
|
? request.app.gt.gettext('Requested page not found')
|
|
6948
7025
|
: (error.output && error.output.payload && error.output.payload.message) || request.app.gt.gettext('Something went wrong'),
|
|
6949
|
-
details: error.output && error.output.payload && error.output.payload.details
|
|
7026
|
+
details: error.output && error.output.payload && error.output.payload.details,
|
|
7027
|
+
templateLocale: request.app.locale
|
|
6950
7028
|
};
|
|
6951
7029
|
|
|
6952
7030
|
if (error.output && error.output.payload) {
|
package/workers/documents.js
CHANGED
package/workers/export.js
CHANGED
|
@@ -755,7 +755,7 @@ const exportWorker = new Worker(
|
|
|
755
755
|
throw new Error('Export not found');
|
|
756
756
|
}
|
|
757
757
|
|
|
758
|
-
await Export.
|
|
758
|
+
await Export.startProcessing(account, exportId);
|
|
759
759
|
await indexMessages(job, exportData);
|
|
760
760
|
await Export.update(account, exportId, { phase: 'exporting' });
|
|
761
761
|
|
|
@@ -788,7 +788,11 @@ const exportWorker = new Worker(
|
|
|
788
788
|
await fs.promises.unlink(exportData.filePath).catch(() => {});
|
|
789
789
|
}
|
|
790
790
|
|
|
791
|
-
|
|
791
|
+
if (err.code === 'ExportCancelled') {
|
|
792
|
+
await Export.deleteFully(account, exportId);
|
|
793
|
+
} else {
|
|
794
|
+
await Export.fail(account, exportId, err.message);
|
|
795
|
+
}
|
|
792
796
|
|
|
793
797
|
if (err.code !== 'AccountDeleted' && err.code !== 'AccountNotFound' && err.code !== 'ExportCancelled') {
|
|
794
798
|
await notify(account, EXPORT_FAILED_NOTIFY, {
|