@bobfrankston/mailx 1.0.391 → 1.0.392

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/bin/mailx.js CHANGED
@@ -907,7 +907,9 @@ async function main() {
907
907
  fs.mkdirSync(logDir, { recursive: true });
908
908
  // Prune logs older than LOG_RETENTION_DAYS on startup. Keep it simple:
909
909
  // scan the dir, stat, delete. Cheap even with years of history.
910
- const LOG_RETENTION_DAYS = 7;
910
+ // Bumped 7 → 30 days so "where did my letter go?" reports can still
911
+ // reach the `[reconcile-delete]` log entry weeks after the fact.
912
+ const LOG_RETENTION_DAYS = 30;
911
913
  const cutoff = Date.now() - LOG_RETENTION_DAYS * 86400000;
912
914
  try {
913
915
  for (const name of fs.readdirSync(logDir)) {
package/client/app.js CHANGED
@@ -1677,6 +1677,52 @@ onWsEvent((event) => {
1677
1677
  case "outboxStatus":
1678
1678
  renderOutboxStatus(event);
1679
1679
  break;
1680
+ case "calendarUpdated":
1681
+ case "tasksUpdated":
1682
+ // Reauth succeeded (or was never broken): clear any lingering
1683
+ // scope banner for this feature. Handled here (not just in the
1684
+ // sidebar) because the global fallback banner isn't tied to the
1685
+ // sidebar's lifecycle.
1686
+ if (alertBanner && /^scope-(calendar|tasks|google)$/.test(alertBanner.dataset.key || "")) {
1687
+ alertBanner.hidden = true;
1688
+ alertBanner.dataset.key = "";
1689
+ alertBanner.querySelector(".status-action")?.remove();
1690
+ }
1691
+ break;
1692
+ case "authScopeError": {
1693
+ // Fallback banner: calendar-sidebar.ts already shows this inline
1694
+ // when the sidebar is visible, but if the user has the sidebar
1695
+ // off or is on a narrow tier where it's hidden, the error would
1696
+ // otherwise be invisible. Global banner with the same button.
1697
+ const feat = event.feature || "google";
1698
+ const key = `scope-${feat}`;
1699
+ const msg = event.message || `Google ${feat} access needs re-consent.`;
1700
+ showAlert(msg, key, { sticky: true });
1701
+ const bannerText = document.getElementById("alert-text");
1702
+ if (bannerText && bannerText.textContent === msg) {
1703
+ const existing = bannerText.parentElement?.querySelector(".status-action");
1704
+ if (!existing) {
1705
+ const btn = document.createElement("button");
1706
+ btn.className = "status-action";
1707
+ btn.textContent = "Re-authenticate";
1708
+ btn.addEventListener("click", async () => {
1709
+ btn.disabled = true;
1710
+ btn.textContent = "Opening browser…";
1711
+ try {
1712
+ const { reauthGoogleScopes } = await import("./lib/api-client.js");
1713
+ await reauthGoogleScopes();
1714
+ btn.textContent = "Consent opened — finish in browser";
1715
+ }
1716
+ catch (err) {
1717
+ btn.disabled = false;
1718
+ btn.textContent = `Failed: ${err?.message || err}`;
1719
+ }
1720
+ });
1721
+ bannerText.parentElement?.insertBefore(btn, document.getElementById("alert-dismiss"));
1722
+ }
1723
+ }
1724
+ break;
1725
+ }
1680
1726
  case "accountError": {
1681
1727
  // Show actual error + hint in banner
1682
1728
  const msg = `${event.accountId}: ${event.error}`;
@@ -853,10 +853,8 @@ function formatSize(n) {
853
853
  document.getElementById("btn-attach")?.addEventListener("click", () => {
854
854
  fileInput?.click();
855
855
  });
856
- fileInput?.addEventListener("change", async () => {
857
- if (!fileInput.files)
858
- return;
859
- for (const file of Array.from(fileInput.files)) {
856
+ async function ingestFiles(files) {
857
+ for (const file of Array.from(files)) {
860
858
  const buf = await file.arrayBuffer();
861
859
  // base64 the whole thing — mailx-service builds the multipart/mixed
862
860
  let binary = "";
@@ -871,10 +869,61 @@ fileInput?.addEventListener("change", async () => {
871
869
  dataBase64,
872
870
  });
873
871
  }
874
- fileInput.value = "";
875
872
  renderAttachmentChips();
876
873
  scheduleDraftSave();
874
+ }
875
+ fileInput?.addEventListener("change", async () => {
876
+ if (!fileInput.files)
877
+ return;
878
+ await ingestFiles(fileInput.files);
879
+ fileInput.value = "";
877
880
  });
881
+ // Drag-and-drop: dropping files anywhere on the compose window attaches them.
882
+ // Highlights a subtle overlay while dragging so the target is obvious. The
883
+ // editor iframe swallows drag events internally so we attach to the compose
884
+ // document root; Quill's own paste/drop handling doesn't fight us because
885
+ // files-with-no-HTML-or-text dragover never hits Quill's clipboard module.
886
+ (() => {
887
+ let dragDepth = 0;
888
+ const root = document.body;
889
+ const overlay = document.createElement("div");
890
+ overlay.id = "compose-drop-overlay";
891
+ overlay.hidden = true;
892
+ overlay.style.cssText = "position:fixed;inset:0;background:oklch(0.6 0.18 250 / 0.15);border:3px dashed oklch(0.55 0.2 250);z-index:9999;pointer-events:none;display:flex;align-items:center;justify-content:center;font-size:1.5rem;font-weight:500;color:oklch(0.35 0.2 250)";
893
+ overlay.textContent = "Drop files to attach";
894
+ root.appendChild(overlay);
895
+ const hasFiles = (e) => Array.from(e.dataTransfer?.types || []).includes("Files");
896
+ root.addEventListener("dragenter", (e) => {
897
+ if (!hasFiles(e))
898
+ return;
899
+ dragDepth++;
900
+ overlay.hidden = false;
901
+ });
902
+ root.addEventListener("dragleave", (e) => {
903
+ if (!hasFiles(e))
904
+ return;
905
+ dragDepth = Math.max(0, dragDepth - 1);
906
+ if (dragDepth === 0)
907
+ overlay.hidden = true;
908
+ });
909
+ root.addEventListener("dragover", (e) => {
910
+ if (!hasFiles(e))
911
+ return;
912
+ e.preventDefault(); // required so drop fires
913
+ if (e.dataTransfer)
914
+ e.dataTransfer.dropEffect = "copy";
915
+ });
916
+ root.addEventListener("drop", async (e) => {
917
+ if (!hasFiles(e))
918
+ return;
919
+ e.preventDefault();
920
+ dragDepth = 0;
921
+ overlay.hidden = true;
922
+ const files = e.dataTransfer?.files;
923
+ if (files && files.length > 0)
924
+ await ingestFiles(files);
925
+ });
926
+ })();
878
927
  // ── Save and close (X button from parent) ──
879
928
  window.addEventListener("compose-save-and-close", () => {
880
929
  handleCloseRequest();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.391",
3
+ "version": "1.0.392",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",