emailengine-app 2.61.5 → 2.62.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/data/google-crawlers.json +1 -1
  3. package/lib/account.js +20 -7
  4. package/lib/api-routes/account-routes.js +28 -5
  5. package/lib/api-routes/chat-routes.js +1 -1
  6. package/lib/api-routes/export-routes.js +316 -0
  7. package/lib/api-routes/message-routes.js +28 -23
  8. package/lib/api-routes/template-routes.js +28 -7
  9. package/lib/arf-detect.js +1 -1
  10. package/lib/consts.js +16 -0
  11. package/lib/db.js +3 -0
  12. package/lib/email-client/base-client.js +6 -4
  13. package/lib/email-client/gmail-client.js +204 -33
  14. package/lib/email-client/imap/mailbox.js +99 -8
  15. package/lib/email-client/imap/subconnection.js +5 -5
  16. package/lib/email-client/imap-client.js +76 -16
  17. package/lib/email-client/message-builder.js +3 -1
  18. package/lib/email-client/notification-handler.js +12 -9
  19. package/lib/email-client/outlook-client.js +362 -69
  20. package/lib/email-client/smtp-pool-manager.js +1 -1
  21. package/lib/export.js +528 -0
  22. package/lib/oauth/gmail.js +21 -13
  23. package/lib/oauth/mail-ru.js +23 -10
  24. package/lib/oauth/outlook.js +26 -16
  25. package/lib/oauth/pubsub/google.js +5 -0
  26. package/lib/routes-ui.js +235 -1
  27. package/lib/schemas.js +260 -80
  28. package/lib/stream-encrypt.js +263 -0
  29. package/lib/tools.js +30 -4
  30. package/lib/ui-routes/account-routes.js +23 -0
  31. package/lib/ui-routes/admin-config-routes.js +11 -4
  32. package/lib/ui-routes/admin-entities-routes.js +18 -0
  33. package/lib/webhooks.js +16 -20
  34. package/package.json +16 -16
  35. package/sbom.json +1 -1
  36. package/server.js +41 -5
  37. package/static/js/ace/ace.js +1 -1
  38. package/static/js/ace/ext-language_tools.js +1 -1
  39. package/static/licenses.html +52 -62
  40. package/translations/de.mo +0 -0
  41. package/translations/de.po +63 -36
  42. package/translations/en.mo +0 -0
  43. package/translations/en.po +64 -37
  44. package/translations/et.mo +0 -0
  45. package/translations/et.po +63 -36
  46. package/translations/fr.mo +0 -0
  47. package/translations/fr.po +63 -36
  48. package/translations/ja.mo +0 -0
  49. package/translations/ja.po +63 -36
  50. package/translations/messages.pot +80 -47
  51. package/translations/nl.mo +0 -0
  52. package/translations/nl.po +63 -36
  53. package/translations/pl.mo +0 -0
  54. package/translations/pl.po +63 -36
  55. package/views/accounts/account.hbs +375 -2
  56. package/views/config/service.hbs +35 -0
  57. package/workers/api.js +123 -44
  58. package/workers/documents.js +1 -0
  59. package/workers/export.js +926 -0
  60. package/workers/imap.js +29 -0
  61. package/workers/submit.js +25 -5
  62. package/workers/webhooks.js +11 -2
@@ -1,7 +1,7 @@
1
1
  msgid ""
2
2
  msgstr ""
3
3
  "Project-Id-Version: \n"
4
- "POT-Creation-Date: 2026-01-14 09:54+0000\n"
4
+ "POT-Creation-Date: 2026-02-05 09:12+0000\n"
5
5
  "PO-Revision-Date: \n"
6
6
  "Last-Translator: \n"
7
7
  "Language-Team: \n"
@@ -69,14 +69,6 @@ msgstr "Adres e-mail"
69
69
  msgid "Enter your email address"
70
70
  msgstr "Wprowadź swój adres e-mail"
71
71
 
72
- #: views/accounts/register/index.hbs:2
73
- msgid "Choose your email account provider"
74
- msgstr "Wybierz dostawcę konta e-mail"
75
-
76
- #: views/accounts/register/index.hbs:15
77
- msgid "Standard IMAP"
78
- msgstr "Standardowy IMAP"
79
-
80
72
  #: views/accounts/register/imap.hbs:11
81
73
  msgid "Your name"
82
74
  msgstr "Twoje imię"
@@ -99,6 +91,14 @@ msgstr "Wprowadź hasło do konta"
99
91
  msgid "Continue"
100
92
  msgstr "Kontynuuj"
101
93
 
94
+ #: views/accounts/register/index.hbs:2
95
+ msgid "Choose your email account provider"
96
+ msgstr "Wybierz dostawcę konta e-mail"
97
+
98
+ #: views/accounts/register/index.hbs:15
99
+ msgid "Standard IMAP"
100
+ msgstr "Standardowy IMAP"
101
+
102
102
  #: views/accounts/register/imap-server.hbs:19
103
103
  msgid "IMAP"
104
104
  msgstr "IMAP"
@@ -220,15 +220,15 @@ msgstr "Nie udalo sie polaczyc z serwerem SMTP"
220
220
  msgid "Request failed."
221
221
  msgstr "Zadanie nie powiodlo sie."
222
222
 
223
- #: lib/routes-ui.js:519 lib/ui-routes/account-routes.js:60
223
+ #: lib/routes-ui.js:520 lib/ui-routes/account-routes.js:60
224
224
  msgid "Delegated"
225
225
  msgstr "Delegowany"
226
226
 
227
- #: lib/routes-ui.js:520 lib/ui-routes/account-routes.js:61
227
+ #: lib/routes-ui.js:521 lib/ui-routes/account-routes.js:61
228
228
  msgid "Using credentials from \"%s\""
229
229
  msgstr "Używanie poświadczeń z \"%s\""
230
230
 
231
- #: lib/routes-ui.js:570 lib/ui-routes/account-routes.js:111
231
+ #: lib/routes-ui.js:571 lib/ui-routes/account-routes.js:111
232
232
  msgid ""
233
233
  "Connection timed out. This usually occurs if you are behind a firewall or "
234
234
  "connecting to the wrong port."
@@ -236,11 +236,11 @@ msgstr ""
236
236
  "Przekroczono limit czasu połączenia. Zwykle dzieje się tak, gdy użytkownik "
237
237
  "znajduje się za zaporą sieciową lub łączy się z niewłaściwym portem."
238
238
 
239
- #: lib/routes-ui.js:573 lib/ui-routes/account-routes.js:114
239
+ #: lib/routes-ui.js:574 lib/ui-routes/account-routes.js:114
240
240
  msgid "The server unexpectedly closed the connection."
241
241
  msgstr "Serwer nieoczekiwanie zamknął połączenie."
242
242
 
243
- #: lib/routes-ui.js:576 lib/ui-routes/account-routes.js:117
243
+ #: lib/routes-ui.js:577 lib/ui-routes/account-routes.js:117
244
244
  msgid ""
245
245
  "The server unexpectedly closed the connection. This usually happens when "
246
246
  "attempting to connect to a TLS port without TLS enabled."
@@ -248,7 +248,7 @@ msgstr ""
248
248
  "Serwer nieoczekiwanie zamknął połączenie. Zwykle dzieje się tak podczas "
249
249
  "próby połączenia z portem TLS bez włączonego protokołu TLS."
250
250
 
251
- #: lib/routes-ui.js:581 lib/ui-routes/account-routes.js:122
251
+ #: lib/routes-ui.js:582 lib/ui-routes/account-routes.js:122
252
252
  msgid ""
253
253
  "The server refused the connection. This typically occurs if the server is "
254
254
  "not running, is overloaded, or you are connecting to the wrong host or port."
@@ -256,55 +256,82 @@ msgstr ""
256
256
  "Serwer odmówił połączenia. Zwykle dzieje się tak, gdy serwer nie działa, "
257
257
  "jest przeciążony lub łączysz się z niewłaściwym hostem lub portem."
258
258
 
259
- #: lib/routes-ui.js:699
259
+ #: lib/routes-ui.js:709
260
260
  msgid "Invalid API key for OpenAI"
261
261
  msgstr "Nieprawidłowy klucz API dla OpenAI"
262
262
 
263
- #: lib/routes-ui.js:4443 lib/routes-ui.js:4478 lib/routes-ui.js:4593
264
- #: lib/routes-ui.js:4640 lib/routes-ui.js:4869 lib/routes-ui.js:4905
265
- #: workers/api.js:2398 lib/ui-routes/account-routes.js:535
266
- #: lib/ui-routes/account-routes.js:571 lib/ui-routes/account-routes.js:688
267
- #: lib/ui-routes/account-routes.js:735 lib/ui-routes/account-routes.js:966
268
- #: lib/ui-routes/account-routes.js:1002
263
+ #: lib/routes-ui.js:4671 lib/routes-ui.js:4706 lib/routes-ui.js:4821
264
+ #: lib/routes-ui.js:4868 lib/routes-ui.js:5115 lib/routes-ui.js:5151
265
+ #: workers/api.js:2413 lib/ui-routes/account-routes.js:549
266
+ #: lib/ui-routes/account-routes.js:585 lib/ui-routes/account-routes.js:702
267
+ #: lib/ui-routes/account-routes.js:749 lib/ui-routes/account-routes.js:998
268
+ #: lib/ui-routes/account-routes.js:1034
269
269
  msgid "Email Account Setup"
270
270
  msgstr "Konfiguracja konta e-mail"
271
271
 
272
- #: lib/routes-ui.js:4503 lib/routes-ui.js:4536 lib/routes-ui.js:7455
273
- #: lib/ui-routes/account-routes.js:596 lib/ui-routes/account-routes.js:630
272
+ #: lib/routes-ui.js:4731 lib/routes-ui.js:4764 lib/routes-ui.js:7701
273
+ #: lib/ui-routes/account-routes.js:610 lib/ui-routes/account-routes.js:644
274
274
  msgid "Invalid request. Check your input and try again."
275
275
  msgstr "Nie udało się zweryfikować argumentów żądania."
276
276
 
277
- #: lib/routes-ui.js:4696 lib/routes-ui.js:4707
278
- #: lib/ui-routes/account-routes.js:792 lib/ui-routes/account-routes.js:803
277
+ #: lib/routes-ui.js:4924 lib/routes-ui.js:4935
278
+ #: lib/ui-routes/account-routes.js:806 lib/ui-routes/account-routes.js:817
279
279
  #: lib/ui-routes/admin-entities-routes.js:2020
280
280
  msgid "Server hostname was not found"
281
281
  msgstr "Nie znaleziono nazwy hosta serwera"
282
282
 
283
- #: lib/routes-ui.js:4699 lib/routes-ui.js:4710
284
- #: lib/ui-routes/account-routes.js:795 lib/ui-routes/account-routes.js:806
283
+ #: lib/routes-ui.js:4927 lib/routes-ui.js:4938
284
+ #: lib/ui-routes/account-routes.js:809 lib/ui-routes/account-routes.js:820
285
285
  #: lib/ui-routes/admin-entities-routes.js:2023
286
286
  msgid "Invalid username or password"
287
287
  msgstr "Nieprawidłowa nazwa użytkownika lub hasło"
288
288
 
289
- #: lib/routes-ui.js:4714 lib/ui-routes/account-routes.js:810
290
- #: lib/ui-routes/admin-entities-routes.js:2027
289
+ #: lib/routes-ui.js:4941 lib/ui-routes/account-routes.js:823
290
+ #: lib/ui-routes/admin-entities-routes.js:2026
291
+ msgid "Authentication credentials were not provided"
292
+ msgstr "Nie podano danych uwierzytelniających"
293
+
294
+ #: lib/routes-ui.js:4944 lib/ui-routes/account-routes.js:826
295
+ #: lib/ui-routes/admin-entities-routes.js:2029
296
+ msgid "OAuth2 authentication failed"
297
+ msgstr "Uwierzytelnianie OAuth2 nie powiodło się"
298
+
299
+ #: lib/routes-ui.js:4947 lib/routes-ui.js:4951
300
+ #: lib/ui-routes/account-routes.js:829 lib/ui-routes/account-routes.js:833
301
+ #: lib/ui-routes/admin-entities-routes.js:2032
302
+ #: lib/ui-routes/admin-entities-routes.js:2036
291
303
  msgid "TLS protocol error"
292
304
  msgstr "Błąd protokołu TLS"
293
305
 
294
- #: lib/routes-ui.js:7418 lib/tools.js:778
306
+ #: lib/routes-ui.js:4955 lib/ui-routes/account-routes.js:837
307
+ #: lib/ui-routes/admin-entities-routes.js:2040
308
+ msgid "Connection timed out"
309
+ msgstr "Przekroczono limit czasu połączenia"
310
+
311
+ #: lib/routes-ui.js:4958 lib/ui-routes/account-routes.js:840
312
+ #: lib/ui-routes/admin-entities-routes.js:2043
313
+ msgid "Could not connect to server"
314
+ msgstr "Nie można połączyć się z serwerem"
315
+
316
+ #: lib/routes-ui.js:4961 lib/ui-routes/account-routes.js:843
317
+ #: lib/ui-routes/admin-entities-routes.js:2046
318
+ msgid "Unexpected server response"
319
+ msgstr "Nieoczekiwana odpowiedź serwera"
320
+
321
+ #: lib/routes-ui.js:7664 lib/tools.js:778
295
322
  msgid "Invalid input"
296
323
  msgstr "Nieprawidłowe dane wejściowe"
297
324
 
298
- #: lib/routes-ui.js:7428 lib/routes-ui.js:7546 lib/routes-ui.js:7563
299
- #: lib/routes-ui.js:7599
325
+ #: lib/routes-ui.js:7674 lib/routes-ui.js:7792 lib/routes-ui.js:7809
326
+ #: lib/routes-ui.js:7845
300
327
  msgid "Subscription Management"
301
328
  msgstr "Zarządzanie subskrypcjami"
302
329
 
303
- #: workers/api.js:6825 workers/api.js:6941
330
+ #: workers/api.js:6904 workers/api.js:7020
304
331
  msgid "Requested page not found"
305
332
  msgstr "Nie znaleziono żądanej strony"
306
333
 
307
- #: workers/api.js:6826
334
+ #: workers/api.js:6905
308
335
  msgid "Something went wrong"
309
336
  msgstr "Ups... Coś poszlo nie tak"
310
337
 
@@ -328,7 +355,7 @@ msgstr ""
328
355
  "365. Aby kontynuować, użyj przycisku \"Zaloguj się za pomocą Microsoft\", "
329
356
  "aby bezpiecznie połączyć swoje konto."
330
357
 
331
- #: lib/ui-routes/account-routes.js:728 lib/ui-routes/account-routes.js:995
358
+ #: lib/ui-routes/account-routes.js:742 lib/ui-routes/account-routes.js:1027
332
359
  msgid "Couldn't set up account. Try again."
333
360
  msgstr "Nie udało się założyć konta. Spróbuj ponownie."
334
361
 
@@ -32,6 +32,14 @@
32
32
  data-toggle="tooltip" data-placement="top">
33
33
  <i class="fas fa-redo fa-fw" id="sync-icon"></i> Run sync
34
34
  </button>
35
+
36
+ {{#if canReadMail}}
37
+ <button type="button" class="btn btn-light" id="request-export" title="Export messages to file"
38
+ data-toggle="modal" data-target="#exportModal">
39
+ <i class="fas fa-file-export fa-fw"></i> Export
40
+ <span id="export-status-indicator" class="badge badge-pill ml-1 d-none"></span>
41
+ </button>
42
+ {{/if}}
35
43
  </div>
36
44
 
37
45
  <div class="btn-group mr-2 mb-1" role="group" aria-label="Third group">
@@ -64,7 +72,7 @@
64
72
  {{#if canUseSmtp}}
65
73
  <a class="dropdown-item" data-toggle="modal" data-target="#testSendModal" data-keyboard="false"
66
74
  data-backdrop="static" data-account="{{account.account}}" href="#">Account's
67
- SMTP server</a>
75
+ Mail server</a>
68
76
  {{#if gateways}}
69
77
  <div class="dropdown-divider"></div>
70
78
  {{/if}}
@@ -935,6 +943,85 @@
935
943
  </div>
936
944
  </div>
937
945
 
946
+ <div class="modal fade" id="exportModal" tabindex="-1" aria-labelledby="exportModalLabel" aria-hidden="true">
947
+ <div class="modal-dialog">
948
+ <div class="modal-content">
949
+ <div class="modal-header">
950
+ <h5 class="modal-title" id="exportModalLabel">Export Messages</h5>
951
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
952
+ <span aria-hidden="true">&times;</span>
953
+ </button>
954
+ </div>
955
+ <div class="modal-body">
956
+ <div class="alert alert-info mb-3">
957
+ <i class="fas fa-flask fa-fw"></i>
958
+ <strong>Beta Feature:</strong> The export feature is currently in beta. If you encounter any issues,
959
+ please contact <a href="mailto:support@postalsys.com">support@postalsys.com</a>.
960
+ </div>
961
+ <div id="export-form-view">
962
+ <div class="form-group">
963
+ <label for="export-start-date">Start Date</label>
964
+ <input type="date" class="form-control" id="export-start-date">
965
+ </div>
966
+ <div class="form-group">
967
+ <label for="export-end-date">End Date</label>
968
+ <input type="date" class="form-control" id="export-end-date">
969
+ </div>
970
+ <div class="form-group">
971
+ <div class="custom-control custom-checkbox">
972
+ <input type="checkbox" class="custom-control-input" id="export-include-attachments">
973
+ <label class="custom-control-label" for="export-include-attachments">Include
974
+ attachments</label>
975
+ </div>
976
+ <small class="form-text text-muted">Download and include attachment file contents in the export.
977
+ This increases export size and time.</small>
978
+ </div>
979
+ <small class="text-muted">Gmail accounts export from All Mail. Other accounts export all folders
980
+ (excluding Junk and Trash).</small>
981
+ </div>
982
+
983
+ <div id="export-status-view" class="d-none">
984
+ <div class="mb-3">
985
+ <strong>Status:</strong>
986
+ <span id="export-status-badge" class="badge badge-info">Queued</span>
987
+ <span id="export-phase-badge" class="badge badge-secondary ml-1 d-none"></span>
988
+ </div>
989
+ <div class="mb-3">
990
+ <strong>Expires:</strong>
991
+ <span id="export-expires-at" class="text-muted"></span>
992
+ </div>
993
+ <div id="export-progress-section">
994
+ <div class="progress mb-2">
995
+ <div id="export-progress-bar"
996
+ class="progress-bar progress-bar-striped progress-bar-animated bg-info"
997
+ role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"
998
+ style="width: 0%"></div>
999
+ </div>
1000
+ <small id="export-progress-text" class="text-muted"></small>
1001
+ </div>
1002
+ <div id="export-error-section" class="d-none">
1003
+ <div class="alert alert-danger" id="export-error-message"></div>
1004
+ </div>
1005
+ <div id="export-download-section" class="d-none mt-3">
1006
+ <a href="#" id="export-download-link" class="btn btn-success btn-block">
1007
+ <i class="fas fa-download fa-fw"></i> Download Export
1008
+ </a>
1009
+ </div>
1010
+ </div>
1011
+ </div>
1012
+ <div class="modal-footer">
1013
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
1014
+ <button type="button" class="btn btn-danger d-none" id="export-reset-btn">
1015
+ <i class="fas fa-trash fa-fw"></i> Reset
1016
+ </button>
1017
+ <button type="button" class="btn btn-primary" id="export-start-btn">
1018
+ <i class="fas fa-file-export fa-fw" id="export-start-icon"></i> Start Export
1019
+ </button>
1020
+ </div>
1021
+ </div>
1022
+ </div>
1023
+ </div>
1024
+
938
1025
  <input type="hidden" id="account-id" value="{{account.account}}">
939
1026
  <input type="hidden" id="crumb" value="{{crumb}}">
940
1027
 
@@ -1107,7 +1194,7 @@
1107
1194
  flushingLogs = false;
1108
1195
  showToast(data.error ? data.error : (data.success ? 'Stored logs were flushed' : 'Failed to flush logs'), data.success ? 'check-circle' : 'alert-triangle');
1109
1196
  }).catch(err => {
1110
- togglinflushingLogsgLogs = false;
1197
+ flushingLogs = false;
1111
1198
  showToast('Request failed\n' + err.message, 'alert-triangle');
1112
1199
  })
1113
1200
  });
@@ -1121,6 +1208,292 @@
1121
1208
 
1122
1209
  $('#renew-grant-btn').tooltip();
1123
1210
  $('#renew-grant-btn').click(() => $('#renew-grant-btn').tooltip('hide'));
1211
+
1212
+ // Export status indicator on page load
1213
+ async function updateExportIndicator() {
1214
+ const account = document.getElementById('account-id').value.trim();
1215
+ const indicator = document.getElementById('export-status-indicator');
1216
+ if (!indicator) return;
1217
+
1218
+ try {
1219
+ const res = await fetch(`/admin/accounts/${account}/exports?pageSize=1`);
1220
+ if (!res.ok) {
1221
+ indicator.classList.add('d-none');
1222
+ return;
1223
+ }
1224
+ const data = await res.json();
1225
+
1226
+ if (data.exports && data.exports.length > 0) {
1227
+ const latest = data.exports[0];
1228
+ if (latest.status === 'queued' || latest.status === 'processing') {
1229
+ indicator.textContent = latest.status === 'queued' ? 'Queued' : 'Exporting';
1230
+ indicator.className = 'badge badge-pill badge-info ml-1';
1231
+ } else if (latest.status === 'completed') {
1232
+ indicator.textContent = 'Ready';
1233
+ indicator.className = 'badge badge-pill badge-success ml-1';
1234
+ } else {
1235
+ indicator.classList.add('d-none');
1236
+ return;
1237
+ }
1238
+ indicator.classList.remove('d-none');
1239
+ } else {
1240
+ indicator.classList.add('d-none');
1241
+ }
1242
+ } catch (err) {
1243
+ console.error('Failed to check export status', err);
1244
+ indicator.classList.add('d-none');
1245
+ }
1246
+ }
1247
+
1248
+ // Check export status on page load
1249
+ updateExportIndicator();
1250
+
1251
+ // Export modal functionality
1252
+ let currentExportId = null;
1253
+ let exportPollingInterval = null;
1254
+
1255
+ function showFormView() {
1256
+ document.getElementById('export-form-view').classList.remove('d-none');
1257
+ document.getElementById('export-status-view').classList.add('d-none');
1258
+ document.getElementById('export-start-btn').classList.remove('d-none');
1259
+ document.getElementById('export-reset-btn').classList.add('d-none');
1260
+
1261
+ // Set default dates (last 1 year)
1262
+ const today = new Date();
1263
+ const lastYear = new Date(today);
1264
+ lastYear.setFullYear(lastYear.getFullYear() - 1);
1265
+ document.getElementById('export-end-date').value = today.toISOString().split('T')[0];
1266
+ document.getElementById('export-start-date').value = lastYear.toISOString().split('T')[0];
1267
+ }
1268
+
1269
+ function showStatusView() {
1270
+ document.getElementById('export-form-view').classList.add('d-none');
1271
+ document.getElementById('export-status-view').classList.remove('d-none');
1272
+ document.getElementById('export-start-btn').classList.add('d-none');
1273
+ document.getElementById('export-reset-btn').classList.remove('d-none');
1274
+ }
1275
+
1276
+ function getStatusBadgeClass(status) {
1277
+ switch (status) {
1278
+ case 'completed': return 'success';
1279
+ case 'failed': return 'danger';
1280
+ case 'processing': return 'info';
1281
+ default: return 'secondary';
1282
+ }
1283
+ }
1284
+
1285
+ function startPolling() {
1286
+ if (exportPollingInterval) return;
1287
+ exportPollingInterval = setInterval(updateExportStatus, 3000);
1288
+ }
1289
+
1290
+ function stopPolling() {
1291
+ if (exportPollingInterval) {
1292
+ clearInterval(exportPollingInterval);
1293
+ exportPollingInterval = null;
1294
+ }
1295
+ }
1296
+
1297
+ async function updateExportStatus() {
1298
+ if (!currentExportId) return;
1299
+
1300
+ const account = document.getElementById('account-id').value.trim();
1301
+ try {
1302
+ const res = await fetch(`/admin/accounts/${account}/export/${currentExportId}`);
1303
+ if (!res.ok) {
1304
+ throw new Error(`HTTP error! status: ${res.status}`);
1305
+ }
1306
+ const data = await res.json();
1307
+
1308
+ // Update status badge
1309
+ const badge = document.getElementById('export-status-badge');
1310
+ badge.textContent = data.status.charAt(0).toUpperCase() + data.status.slice(1);
1311
+ badge.className = 'badge badge-' + getStatusBadgeClass(data.status);
1312
+
1313
+ // Update phase badge
1314
+ const phaseBadge = document.getElementById('export-phase-badge');
1315
+ if (data.phase && data.status === 'processing') {
1316
+ phaseBadge.textContent = data.phase.charAt(0).toUpperCase() + data.phase.slice(1);
1317
+ phaseBadge.classList.remove('d-none');
1318
+ } else {
1319
+ phaseBadge.classList.add('d-none');
1320
+ }
1321
+
1322
+ // Update expiration date
1323
+ const expiresAtEl = document.getElementById('export-expires-at');
1324
+ if (data.expiresAt) {
1325
+ const expiresDate = new Date(data.expiresAt);
1326
+ expiresAtEl.textContent = expiresDate.toLocaleString();
1327
+ } else {
1328
+ expiresAtEl.textContent = 'Unknown';
1329
+ }
1330
+
1331
+ // Update progress bar
1332
+ const progressBar = document.getElementById('export-progress-bar');
1333
+ const progressText = document.getElementById('export-progress-text');
1334
+
1335
+ if (data.phase === 'indexing' && data.progress) {
1336
+ // During indexing, show folders scanned progress
1337
+ const pct = data.progress.foldersTotal > 0
1338
+ ? Math.round((data.progress.foldersScanned / data.progress.foldersTotal) * 100)
1339
+ : 0;
1340
+ progressBar.style.width = pct + '%';
1341
+ progressBar.setAttribute('aria-valuenow', pct);
1342
+ progressText.textContent = `Scanning folders: ${data.progress.foldersScanned || 0} / ${data.progress.foldersTotal || '?'}`;
1343
+ } else if (data.progress) {
1344
+ // During exporting, show messages exported progress
1345
+ const pct = data.progress.messagesQueued > 0
1346
+ ? Math.round((data.progress.messagesExported / data.progress.messagesQueued) * 100)
1347
+ : 0;
1348
+ progressBar.style.width = pct + '%';
1349
+ progressBar.setAttribute('aria-valuenow', pct);
1350
+ progressText.textContent =
1351
+ `${data.progress.messagesExported} / ${data.progress.messagesQueued} messages exported`;
1352
+ }
1353
+
1354
+ // Handle completion
1355
+ if (data.status === 'completed') {
1356
+ stopPolling();
1357
+ document.getElementById('export-download-section').classList.remove('d-none');
1358
+ document.getElementById('export-download-link').href =
1359
+ `/admin/accounts/${account}/export/${currentExportId}/download`;
1360
+ progressBar.style.width = '100%';
1361
+ progressBar.setAttribute('aria-valuenow', 100);
1362
+ progressBar.classList.remove('bg-info', 'progress-bar-striped', 'progress-bar-animated');
1363
+ progressBar.classList.add('bg-success');
1364
+ }
1365
+
1366
+ // Handle failure
1367
+ if (data.status === 'failed') {
1368
+ stopPolling();
1369
+ document.getElementById('export-error-section').classList.remove('d-none');
1370
+ document.getElementById('export-error-message').textContent = data.error || 'Export failed';
1371
+ document.getElementById('export-progress-section').classList.add('d-none');
1372
+ }
1373
+ } catch (err) {
1374
+ console.error('Failed to update export status', err);
1375
+ }
1376
+ }
1377
+
1378
+ async function checkExistingExport() {
1379
+ const account = document.getElementById('account-id').value.trim();
1380
+ try {
1381
+ const res = await fetch(`/admin/accounts/${account}/exports?pageSize=1`);
1382
+ if (!res.ok) {
1383
+ showFormView();
1384
+ return;
1385
+ }
1386
+ const data = await res.json();
1387
+
1388
+ if (data.exports && data.exports.length > 0) {
1389
+ const latest = data.exports[0];
1390
+ if (latest.status === 'queued' || latest.status === 'processing' || latest.status === 'completed') {
1391
+ currentExportId = latest.exportId;
1392
+ showStatusView();
1393
+ // Reset progress display state
1394
+ document.getElementById('export-progress-section').classList.remove('d-none');
1395
+ document.getElementById('export-error-section').classList.add('d-none');
1396
+ document.getElementById('export-download-section').classList.add('d-none');
1397
+ document.getElementById('export-phase-badge').classList.add('d-none');
1398
+ const progressBar = document.getElementById('export-progress-bar');
1399
+ progressBar.classList.remove('bg-success');
1400
+ progressBar.classList.add('bg-info', 'progress-bar-striped', 'progress-bar-animated');
1401
+ await updateExportStatus();
1402
+ if (latest.status !== 'completed' && latest.status !== 'failed') {
1403
+ startPolling();
1404
+ }
1405
+ return;
1406
+ }
1407
+ }
1408
+ showFormView();
1409
+ } catch (err) {
1410
+ console.error('Failed to check existing export', err);
1411
+ showFormView();
1412
+ }
1413
+ }
1414
+
1415
+ $('#exportModal').on('show.bs.modal', async function () {
1416
+ await checkExistingExport();
1417
+ });
1418
+
1419
+ $('#exportModal').on('hidden.bs.modal', function () {
1420
+ stopPolling();
1421
+ updateExportIndicator();
1422
+ });
1423
+
1424
+ document.getElementById('export-start-btn').addEventListener('click', async function () {
1425
+ const account = document.getElementById('account-id').value.trim();
1426
+ const startDate = document.getElementById('export-start-date').value;
1427
+ const endDate = document.getElementById('export-end-date').value;
1428
+ const includeAttachments = document.getElementById('export-include-attachments').checked;
1429
+
1430
+ if (!startDate || !endDate) {
1431
+ showToast('Please select date range', 'alert-triangle');
1432
+ return;
1433
+ }
1434
+
1435
+ document.getElementById('export-start-icon').classList.add('fa-spin');
1436
+
1437
+ try {
1438
+ const res = await fetch(`/admin/accounts/${account}/export`, {
1439
+ method: 'POST',
1440
+ headers: { 'Content-Type': 'application/json' },
1441
+ body: JSON.stringify({
1442
+ crumb: document.getElementById('crumb').value,
1443
+ startDate: startDate + 'T00:00:00Z',
1444
+ endDate: endDate + 'T23:59:59Z',
1445
+ includeAttachments
1446
+ })
1447
+ });
1448
+
1449
+ const data = await res.json();
1450
+ if (data.exportId) {
1451
+ currentExportId = data.exportId;
1452
+ showStatusView();
1453
+ // Reset progress display state
1454
+ document.getElementById('export-progress-section').classList.remove('d-none');
1455
+ document.getElementById('export-error-section').classList.add('d-none');
1456
+ document.getElementById('export-download-section').classList.add('d-none');
1457
+ document.getElementById('export-phase-badge').classList.add('d-none');
1458
+ const progressBar = document.getElementById('export-progress-bar');
1459
+ progressBar.classList.remove('bg-success');
1460
+ progressBar.classList.add('bg-info', 'progress-bar-striped', 'progress-bar-animated');
1461
+ progressBar.style.width = '0%';
1462
+ progressBar.setAttribute('aria-valuenow', 0);
1463
+ document.getElementById('export-progress-text').textContent = '';
1464
+ await updateExportStatus();
1465
+ startPolling();
1466
+ showToast('Export started', 'check-circle');
1467
+ } else {
1468
+ showToast(data.error || data.message || 'Failed to start export', 'alert-triangle');
1469
+ }
1470
+ } catch (err) {
1471
+ showToast('Export failed: ' + err.message, 'alert-triangle');
1472
+ } finally {
1473
+ document.getElementById('export-start-icon').classList.remove('fa-spin');
1474
+ }
1475
+ });
1476
+
1477
+ document.getElementById('export-reset-btn').addEventListener('click', async function () {
1478
+ if (!currentExportId) return;
1479
+
1480
+ const account = document.getElementById('account-id').value.trim();
1481
+
1482
+ try {
1483
+ await fetch(`/admin/accounts/${account}/export/${currentExportId}`, {
1484
+ method: 'DELETE',
1485
+ headers: { 'Content-Type': 'application/json' },
1486
+ body: JSON.stringify({ crumb: document.getElementById('crumb').value })
1487
+ });
1488
+
1489
+ stopPolling();
1490
+ currentExportId = null;
1491
+ showFormView();
1492
+ showToast('Export cleared', 'check-circle');
1493
+ } catch (err) {
1494
+ showToast('Failed to clear export: ' + err.message, 'alert-triangle');
1495
+ }
1496
+ });
1124
1497
  });
1125
1498
  </script>
1126
1499
 
@@ -341,6 +341,41 @@
341
341
  </div>
342
342
  </div>
343
343
 
344
+ <div id="export_settings" class="card mb-4">
345
+ <div class="card-header py-3">
346
+ <h6 class="m-0 font-weight-bold text-primary">Export Settings</h6>
347
+ </div>
348
+ <div class="card-body">
349
+ <p>Configure batch sizes for bulk message export operations.</p>
350
+
351
+ <div class="form-group">
352
+ <div class="text-muted float-right code-link">[<a href="/admin/swagger#/Settings/postV1Settings"
353
+ target="_blank" rel="noopener noreferrer">gmailExportBatchSize</a>]</div>
354
+ <label for="settingsGmailExportBatchSize">Gmail Export Batch Size</label>
355
+ <input type="number" min="1" max="50" class="form-control {{#if errors.gmailExportBatchSize}}is-invalid{{/if}}"
356
+ id="settingsGmailExportBatchSize" name="gmailExportBatchSize" value="{{values.gmailExportBatchSize}}"
357
+ data-lpignore="true" autocomplete="off" />
358
+ {{#if errors.gmailExportBatchSize}}
359
+ <span class="invalid-feedback">{{errors.gmailExportBatchSize}}</span>
360
+ {{/if}}
361
+ <small class="form-text text-muted">Number of parallel requests when fetching Gmail messages for export (1-50, default: 10).</small>
362
+ </div>
363
+
364
+ <div class="form-group">
365
+ <div class="text-muted float-right code-link">[<a href="/admin/swagger#/Settings/postV1Settings"
366
+ target="_blank" rel="noopener noreferrer">outlookExportBatchSize</a>]</div>
367
+ <label for="settingsOutlookExportBatchSize">Outlook Export Batch Size</label>
368
+ <input type="number" min="1" max="20" class="form-control {{#if errors.outlookExportBatchSize}}is-invalid{{/if}}"
369
+ id="settingsOutlookExportBatchSize" name="outlookExportBatchSize" value="{{values.outlookExportBatchSize}}"
370
+ data-lpignore="true" autocomplete="off" />
371
+ {{#if errors.outlookExportBatchSize}}
372
+ <span class="invalid-feedback">{{errors.outlookExportBatchSize}}</span>
373
+ {{/if}}
374
+ <small class="form-text text-muted">Messages per batch request for Outlook exports (1-20, default: 20). Limited by Microsoft Graph API.</small>
375
+ </div>
376
+ </div>
377
+ </div>
378
+
344
379
  <div id="templates_settings" class="card mb-4">
345
380
  <div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
346
381
  <h6 class="m-0 font-weight-bold text-primary">Public Page Customization</h6>