@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 +3 -1
- package/client/app.js +46 -0
- package/client/compose/compose.js +54 -5
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
857
|
-
|
|
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();
|