emailengine-app 2.67.2 → 2.68.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.
@@ -1,7 +1,7 @@
1
1
  msgid ""
2
2
  msgstr ""
3
3
  "Content-Type: text/plain; charset=ascii\n"
4
- "POT-Creation-Date: 2026-04-17 08:51+0000\n"
4
+ "POT-Creation-Date: 2026-04-29 10:14+0000\n"
5
5
 
6
6
  #: views/error.hbs:4
7
7
  #: workers/api.js:7103
@@ -27,10 +27,6 @@ msgid_plural "%d days"
27
27
  msgstr[0] ""
28
28
  msgstr[1] ""
29
29
 
30
- #: views/redirect.hbs:1
31
- msgid "Click <a href=\"%s\">here</a> to continue&mldr;"
32
- msgstr ""
33
-
34
30
  #: views/oauth-scope-error.hbs:2
35
31
  msgid "Insufficient Permissions"
36
32
  msgstr ""
@@ -55,6 +51,10 @@ msgstr ""
55
51
  msgid "Try Again"
56
52
  msgstr ""
57
53
 
54
+ #: views/redirect.hbs:1
55
+ msgid "Click <a href=\"%s\">here</a> to continue&mldr;"
56
+ msgstr ""
57
+
58
58
  #: views/unsubscribe.hbs:3
59
59
  #: views/unsubscribe.hbs:62
60
60
  #: views/unsubscribe.hbs:85
@@ -104,14 +104,6 @@ msgstr ""
104
104
  msgid "Enter your email address"
105
105
  msgstr ""
106
106
 
107
- #: views/accounts/register/index.hbs:2
108
- msgid "Choose your email account provider"
109
- msgstr ""
110
-
111
- #: views/accounts/register/index.hbs:15
112
- msgid "Standard IMAP"
113
- msgstr ""
114
-
115
107
  #: views/accounts/register/imap.hbs:11
116
108
  msgid "Your name"
117
109
  msgstr ""
@@ -134,6 +126,14 @@ msgstr ""
134
126
  msgid "Continue"
135
127
  msgstr ""
136
128
 
129
+ #: views/accounts/register/index.hbs:2
130
+ msgid "Choose your email account provider"
131
+ msgstr ""
132
+
133
+ #: views/accounts/register/index.hbs:15
134
+ msgid "Standard IMAP"
135
+ msgstr ""
136
+
137
137
  #: views/accounts/register/imap-server.hbs:19
138
138
  msgid "IMAP"
139
139
  msgstr ""
@@ -315,26 +315,26 @@
315
315
  errorsListElm.innerHTML = '';
316
316
 
317
317
  if (data.error && !data.fields) {
318
- addErrorRow('{{_ "Error" templateLocale }}', data.error.message || data.error)
318
+ addErrorRow(`{{_ "Error" templateLocale }}`, data.error.message || data.error)
319
319
  } else if (data.fields) {
320
- addErrorRow('{{_ "Invalid settings" templateLocale }}', data.message);
320
+ addErrorRow(`{{_ "Invalid settings" templateLocale }}`, data.message);
321
321
  for (let field of data.fields) {
322
322
  addErrorRow('-', field.message, 'aaaa', 'bbbbbb');
323
323
  }
324
324
  } else {
325
325
  if (!data.imap || !data.imap.success) {
326
- let error = data.imap && data.imap.error || '{{_ "Couldn't connect to IMAP server" templateLocale }}'
326
+ let error = data.imap && data.imap.error || `{{_ "Couldn't connect to IMAP server" templateLocale }}`
327
327
  if (data.imap && data.imap.responseText) {
328
- addErrorRow('IMAP', error, '{{_ "Server response:" templateLocale }}', data.imap.responseText);
328
+ addErrorRow('IMAP', error, `{{_ "Server response:" templateLocale }}`, data.imap.responseText);
329
329
  } else {
330
330
  addErrorRow('IMAP', error);
331
331
  }
332
332
  }
333
333
 
334
334
  if (!data.smtp || !data.smtp.success) {
335
- let error = data.smtp && data.smtp.error || '{{_ "Couldn't connect to SMTP server" templateLocale }}'
335
+ let error = data.smtp && data.smtp.error || `{{_ "Couldn't connect to SMTP server" templateLocale }}`
336
336
  if (data.smtp && data.smtp.responseText) {
337
- addErrorRow('SMTP', error, '{{_ "Server response:" templateLocale }}', data.smtp.responseText);
337
+ addErrorRow('SMTP', error, `{{_ "Server response:" templateLocale }}`, data.smtp.responseText);
338
338
  } else {
339
339
  addErrorRow('SMTP', error);
340
340
  }
@@ -33,6 +33,11 @@
33
33
  <i class="fas fa-user-edit fa-fw"></i> Edit app
34
34
  </a>
35
35
 
36
+ <button type="button" class="btn btn-light" data-toggle="modal" data-target="#verifySetupModal"
37
+ title="Run live checks against the provider" id="verify-btn" data-placement="top">
38
+ <i class="fas fa-clipboard-check fa-fw"></i> Verify setup
39
+ </button>
40
+
36
41
  <button type="button" class="btn btn-light" data-toggle="modal" data-target="#deleteModal"
37
42
  title="Delete this application" id="delete-btn" data-placement="top">
38
43
  <i class="fas fa-trash-alt fa-fw"></i> Delete app
@@ -40,6 +45,16 @@
40
45
 
41
46
  </div>
42
47
 
48
+ {{#if canAddServiceAccount}}
49
+ <div class="btn-group ml-auto mb-1" role="group" aria-label="Account actions">
50
+ <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addServiceAccountModal"
51
+ title="Register an email account that authenticates through this service application"
52
+ id="add-service-account-btn" data-placement="top">
53
+ <i class="fas fa-user-plus fa-fw"></i> Add account
54
+ </button>
55
+ </div>
56
+ {{/if}}
57
+
43
58
  </div>
44
59
 
45
60
  {{#if app.description}}
@@ -164,6 +179,22 @@
164
179
  <dd class="col-sm-9 text-monospace"><small>*****</small></dd>
165
180
  {{/if}}
166
181
 
182
+ {{#if appShowAuthMethod}}
183
+ <dt class="col-sm-3">Authentication method</dt>
184
+ <dd class="col-sm-9">
185
+ {{#if authMethodIsExternalAccount}}
186
+ Workload Identity Federation
187
+ {{else}}
188
+ Service account key
189
+ {{/if}}
190
+ </dd>
191
+ {{/if}}
192
+
193
+ {{#if app.externalAccount}}
194
+ <dt class="col-sm-3">External account config</dt>
195
+ <dd class="col-sm-9 text-monospace"><small>***** (set)</small></dd>
196
+ {{/if}}
197
+
167
198
  {{#if app.redirectUrl}}
168
199
  <dt class="col-sm-3">Redirect URL</dt>
169
200
  <dd class="col-sm-9 text-monospace"><small>{{app.redirectUrl}}</small></dd>
@@ -337,6 +368,199 @@
337
368
  </div>
338
369
  </div>
339
370
 
371
+ <div class="modal fade" id="verifySetupModal" tabindex="-1" aria-labelledby="verifySetupModalLabel" aria-hidden="true">
372
+ <div class="modal-dialog modal-lg">
373
+ <div class="modal-content">
374
+ <div class="modal-header">
375
+ <h5 class="modal-title" id="verifySetupModalLabel">Verify OAuth2 setup</h5>
376
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
377
+ <span aria-hidden="true">&times;</span>
378
+ </button>
379
+ </div>
380
+ <div class="modal-body">
381
+ <input type="hidden" id="verify-crumb" value="{{crumb}}" />
382
+ <p><small class="text-muted">Runs the provider authentication chain step by step and reports what passes
383
+ or fails.{{#if appShowAuthMethod}} Enter a mailbox address in your Workspace domain to also verify
384
+ domain-wide delegation and live mailbox access.{{/if}}</small></p>
385
+
386
+ <div class="form-group">
387
+ <div class="input-group">
388
+ <input type="email" class="form-control" id="verify-account"
389
+ placeholder="mailbox@example.com (optional)" autocomplete="off" />
390
+ <div class="input-group-append">
391
+ <button type="button" class="btn btn-primary" id="verify-run-btn">
392
+ <i class="fas fa-play fa-fw"></i> Run test
393
+ </button>
394
+ </div>
395
+ </div>
396
+ <small class="form-text text-muted">Leave empty to check configuration and signing only.</small>
397
+ </div>
398
+
399
+ <div id="verify-pending" class="d-none">
400
+ <p><i class="fas fa-spinner fa-spin fa-sm"></i> Running checks, please wait&mldr;</p>
401
+ </div>
402
+ <div id="verify-error" class="d-none">
403
+ <small class="alert alert-danger text-monospace error-message" style="display: block;"></small>
404
+ </div>
405
+ <div id="verify-verdict" class="d-none mb-3"></div>
406
+ <ul id="verify-steps" class="list-group"></ul>
407
+ </div>
408
+ <div class="modal-footer">
409
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
410
+ </div>
411
+ </div>
412
+ </div>
413
+ </div>
414
+
415
+ {{#if canAddServiceAccount}}
416
+ <div class="modal fade" id="addServiceAccountModal" tabindex="-1" aria-labelledby="addServiceAccountLabel"
417
+ aria-hidden="true">
418
+ <div class="modal-dialog">
419
+ <div class="modal-content">
420
+ <div class="modal-header">
421
+ <h5 class="modal-title" id="addServiceAccountLabel">Add an account</h5>
422
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
423
+ <span aria-hidden="true">&times;</span>
424
+ </button>
425
+ </div>
426
+ <form method="post" action="/admin/config/oauth/app/{{app.id}}/add-account">
427
+ <input type="hidden" name="crumb" value="{{crumb}}">
428
+ <div class="modal-body">
429
+ <p><small class="text-muted">This service application authenticates without an interactive consent
430
+ screen, so the account is registered directly. The mailbox must be reachable with the
431
+ application's service credentials.</small></p>
432
+ <div class="form-group">
433
+ <label for="service-account-name" class="col-form-label">Full name:</label>
434
+ <input type="text" class="form-control" id="service-account-name" name="name"
435
+ placeholder="e.g., John Smith">
436
+ </div>
437
+ <div class="form-group">
438
+ <label for="service-account-email" class="col-form-label">Email address:</label>
439
+ <input type="email" class="form-control" id="service-account-email" name="email"
440
+ placeholder="e.g., user@example.com" required>
441
+ </div>
442
+ <div class="form-group">
443
+ <label for="service-account-id" class="col-form-label">Account identifier (optional):</label>
444
+ <input type="text" class="form-control" id="service-account-id" name="account"
445
+ placeholder="e.g., account_123">
446
+ <small class="form-text text-muted">Leave blank to auto-generate. Existing accounts with this ID
447
+ will be updated.</small>
448
+ </div>
449
+ </div>
450
+ <div class="modal-footer">
451
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
452
+ <button type="submit" class="btn btn-primary btn-icon-split">
453
+ <span class="icon text-white-50">
454
+ <i class="fas fa-user-plus"></i>
455
+ </span>
456
+ <span class="text">Add account</span>
457
+ </button>
458
+ </div>
459
+ </form>
460
+ </div>
461
+ </div>
462
+ </div>
463
+
464
+ <script>
465
+ document.addEventListener('DOMContentLoaded', () => {
466
+ $('#addServiceAccountModal').on('shown.bs.modal', () => {
467
+ document.getElementById('service-account-name').focus();
468
+ });
469
+ });
470
+ </script>
471
+ {{/if}}
472
+
473
+ <script>
474
+ document.addEventListener('DOMContentLoaded', () => {
475
+ const appId = "{{app.id}}";
476
+ const stepsElm = document.getElementById('verify-steps');
477
+ const pendingElm = document.getElementById('verify-pending');
478
+ const errorElm = document.getElementById('verify-error');
479
+ const verdictElm = document.getElementById('verify-verdict');
480
+ const runBtn = document.getElementById('verify-run-btn');
481
+ const accountInput = document.getElementById('verify-account');
482
+
483
+ const ICONS = {
484
+ ok: '<i class="fas fa-check-circle text-success fa-fw"></i>',
485
+ fail: '<i class="fas fa-times-circle text-danger fa-fw"></i>',
486
+ skip: '<i class="fas fa-minus-circle text-muted fa-fw"></i>'
487
+ };
488
+
489
+ const esc = str => {
490
+ let div = document.createElement('div');
491
+ div.textContent = str == null ? '' : String(str);
492
+ return div.innerHTML;
493
+ };
494
+
495
+ const renderResult = data => {
496
+ stepsElm.innerHTML = '';
497
+ (data.steps || []).forEach(step => {
498
+ let li = document.createElement('li');
499
+ li.className = 'list-group-item';
500
+ let html = `${ICONS[step.status] || ''} <strong>${esc(step.label)}</strong>`;
501
+ if (step.message) {
502
+ html += `<div class="small text-muted ml-4">${esc(step.message)}</div>`;
503
+ }
504
+ if (step.hint) {
505
+ html += `<div class="small text-info ml-4"><i class="fas fa-lightbulb fa-fw"></i> ${esc(step.hint)}</div>`;
506
+ }
507
+ li.innerHTML = html;
508
+ stepsElm.appendChild(li);
509
+ });
510
+
511
+ let failed = (data.steps || []).some(s => s.status === 'fail');
512
+ verdictElm.className = 'mb-3 alert ' + (failed ? 'alert-danger' : 'alert-success');
513
+ verdictElm.textContent = failed
514
+ ? 'Setup has problems - review the failing step(s) below.'
515
+ : 'Setup verified - all checks passed.';
516
+ verdictElm.classList.remove('d-none');
517
+ };
518
+
519
+ const runVerify = async account => {
520
+ errorElm.classList.add('d-none');
521
+ verdictElm.classList.add('d-none');
522
+ stepsElm.innerHTML = '';
523
+ pendingElm.classList.remove('d-none');
524
+ runBtn.disabled = true;
525
+ try {
526
+ let res = await fetch(`/admin/config/oauth/verify/${encodeURIComponent(appId)}`, {
527
+ method: 'post',
528
+ headers: { 'content-type': 'application/json' },
529
+ body: JSON.stringify({
530
+ crumb: document.getElementById('verify-crumb').value,
531
+ account: account || '',
532
+ testConnection: true
533
+ })
534
+ });
535
+ let data = await res.json();
536
+ if (!res.ok) {
537
+ throw new Error(data.error || `HTTP ${res.status}`);
538
+ }
539
+ renderResult(data);
540
+ } catch (err) {
541
+ errorElm.querySelector('.error-message').textContent = err.message;
542
+ errorElm.classList.remove('d-none');
543
+ } finally {
544
+ pendingElm.classList.add('d-none');
545
+ runBtn.disabled = false;
546
+ }
547
+ };
548
+
549
+ $('#verifySetupModal').on('shown.bs.modal', () => {
550
+ // Auto-run the no-mailbox checks (config + signing chain) on open.
551
+ runVerify(accountInput.value.trim());
552
+ });
553
+
554
+ runBtn.addEventListener('click', () => runVerify(accountInput.value.trim()));
555
+ accountInput.addEventListener('keydown', e => {
556
+ if (e.key === 'Enter') {
557
+ e.preventDefault();
558
+ runVerify(accountInput.value.trim());
559
+ }
560
+ });
561
+ });
562
+ </script>
563
+
340
564
  <script>
341
565
  document.addEventListener('DOMContentLoaded', () => {
342
566
  // not set up by default as this element has a different data-toggle
@@ -89,6 +89,75 @@
89
89
  });
90
90
  }
91
91
 
92
+ let externalAccountFileElm = document.getElementById('externalAccountFile');
93
+ if (externalAccountFileElm) {
94
+ externalAccountFileElm.addEventListener('click', e => {
95
+ e.preventDefault();
96
+ browseFileContents('text').then(jsonStr => {
97
+ if (!jsonStr) {
98
+ return;
99
+ }
100
+
101
+ let data;
102
+ try {
103
+ data = JSON.parse(jsonStr);
104
+ } catch (err) {
105
+ return showToast('Selected file is not JSON formatted', 'alert-triangle');
106
+ }
107
+
108
+ if (data.type !== 'external_account') {
109
+ if (data.type === 'service_account') {
110
+ return showToast('This is a service account key file, not a Workload Identity Federation config. Switch to the "Service account key" tab to load it.', 'alert-triangle');
111
+ }
112
+ return showToast(`Selected file is not an external_account credential config (type: ${data.type || 'missing'})`, 'alert-triangle');
113
+ }
114
+
115
+ let impersonationUrl = data.service_account_impersonation_url || '';
116
+ let emailMatch = impersonationUrl.match(/\/serviceAccounts\/([^/]+):generateAccessToken$/);
117
+ if (emailMatch) {
118
+ let derivedEmail = decodeURIComponent(emailMatch[1]);
119
+ let emailField = document.getElementById('serviceClientEmail');
120
+ if (emailField && !emailField.value) {
121
+ emailField.value = derivedEmail;
122
+ }
123
+ }
124
+
125
+ document.getElementById('externalAccount').value = JSON.stringify(data, null, 2);
126
+ return showToast('Loaded external account configuration from file', 'check-circle');
127
+ }).catch(err => {
128
+ console.error(err);
129
+ return showToast('Failed to load external account file', 'alert-triangle');
130
+ });
131
+ });
132
+ }
133
+
134
+ let authMethodTabs = document.querySelectorAll('.auth-method-tab');
135
+ let authMethodInput = document.getElementById('authMethod');
136
+ if (authMethodTabs.length && authMethodInput) {
137
+ let updateSections = selected => {
138
+ document.querySelectorAll('.auth-method-section').forEach(section => {
139
+ let active = section.classList.contains('auth-method-section-' + selected);
140
+ section.classList.toggle('d-none', !active);
141
+ });
142
+ };
143
+ authMethodTabs.forEach(tab => {
144
+ tab.addEventListener('click', e => {
145
+ e.preventDefault();
146
+ if (tab.classList.contains('disabled') || tab.classList.contains('active')) {
147
+ return;
148
+ }
149
+ let selected = tab.getAttribute('data-auth-method');
150
+ authMethodTabs.forEach(other => {
151
+ let isActive = other === tab;
152
+ other.classList.toggle('active', isActive);
153
+ other.setAttribute('aria-selected', isActive ? 'true' : 'false');
154
+ });
155
+ authMethodInput.value = selected;
156
+ updateSections(selected);
157
+ });
158
+ });
159
+ }
160
+
92
161
  });
93
162
 
94
163
  </script>
@@ -87,6 +87,75 @@
87
87
  });
88
88
  }
89
89
 
90
+ let externalAccountFileElm = document.getElementById('externalAccountFile');
91
+ if (externalAccountFileElm) {
92
+ externalAccountFileElm.addEventListener('click', e => {
93
+ e.preventDefault();
94
+ browseFileContents('text').then(jsonStr => {
95
+ if (!jsonStr) {
96
+ return;
97
+ }
98
+
99
+ let data;
100
+ try {
101
+ data = JSON.parse(jsonStr);
102
+ } catch (err) {
103
+ return showToast('Selected file is not JSON formatted', 'alert-triangle');
104
+ }
105
+
106
+ if (data.type !== 'external_account') {
107
+ if (data.type === 'service_account') {
108
+ return showToast('This is a service account key file, not a Workload Identity Federation config. Switch to the "Service account key" tab to load it.', 'alert-triangle');
109
+ }
110
+ return showToast(`Selected file is not an external_account credential config (type: ${data.type || 'missing'})`, 'alert-triangle');
111
+ }
112
+
113
+ let impersonationUrl = data.service_account_impersonation_url || '';
114
+ let emailMatch = impersonationUrl.match(/\/serviceAccounts\/([^/]+):generateAccessToken$/);
115
+ if (emailMatch) {
116
+ let derivedEmail = decodeURIComponent(emailMatch[1]);
117
+ let emailField = document.getElementById('serviceClientEmail');
118
+ if (emailField && !emailField.value) {
119
+ emailField.value = derivedEmail;
120
+ }
121
+ }
122
+
123
+ document.getElementById('externalAccount').value = JSON.stringify(data, null, 2);
124
+ return showToast('Loaded external account configuration from file', 'check-circle');
125
+ }).catch(err => {
126
+ console.error(err);
127
+ return showToast('Failed to load external account file', 'alert-triangle');
128
+ });
129
+ });
130
+ }
131
+
132
+ let authMethodTabs = document.querySelectorAll('.auth-method-tab');
133
+ let authMethodInput = document.getElementById('authMethod');
134
+ if (authMethodTabs.length && authMethodInput) {
135
+ let updateSections = selected => {
136
+ document.querySelectorAll('.auth-method-section').forEach(section => {
137
+ let active = section.classList.contains('auth-method-section-' + selected);
138
+ section.classList.toggle('d-none', !active);
139
+ });
140
+ };
141
+ authMethodTabs.forEach(tab => {
142
+ tab.addEventListener('click', e => {
143
+ e.preventDefault();
144
+ if (tab.classList.contains('disabled') || tab.classList.contains('active')) {
145
+ return;
146
+ }
147
+ let selected = tab.getAttribute('data-auth-method');
148
+ authMethodTabs.forEach(other => {
149
+ let isActive = other === tab;
150
+ other.classList.toggle('active', isActive);
151
+ other.setAttribute('aria-selected', isActive ? 'true' : 'false');
152
+ });
153
+ authMethodInput.value = selected;
154
+ updateSections(selected);
155
+ });
156
+ });
157
+ }
158
+
90
159
  });
91
160
 
92
161
  </script>