@bobfrankston/mailx 1.0.12 → 1.0.14
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 +52 -28
- package/client/app.js +113 -30
- package/client/components/folder-tree.js +84 -3
- package/client/components/message-list.js +164 -10
- package/client/components/message-viewer.js +130 -13
- package/client/compose/compose.html +4 -4
- package/client/compose/compose.js +53 -34
- package/client/index.html +50 -21
- package/client/lib/api-client.js +112 -31
- package/client/lib/mailxapi.js +123 -0
- package/client/package.json +1 -1
- package/client/styles/components.css +206 -16
- package/client/styles/layout.css +2 -1
- package/killmail.cmd +6 -0
- package/launch.ps1 +47 -5
- package/launcher/bin/mailx-app-linux +0 -0
- package/launcher/bin/mailx-app.exe +0 -0
- package/launcher/builder/build-config.json +11 -0
- package/launcher/builder/postinstall.js +81 -0
- package/package.json +2 -4
- package/packages/mailx-api/index.js +125 -29
- package/packages/mailx-core/index.d.ts +129 -0
- package/packages/mailx-core/index.js +323 -0
- package/packages/mailx-core/ipc.d.ts +13 -0
- package/packages/mailx-core/ipc.js +56 -0
- package/packages/mailx-core/package.json +18 -0
- package/packages/mailx-imap/index.d.ts +7 -1
- package/packages/mailx-imap/index.js +89 -14
- package/packages/mailx-server/index.js +42 -31
- package/packages/mailx-server/package.json +1 -2
- package/packages/mailx-settings/index.d.ts +1 -1
- package/packages/mailx-settings/index.js +21 -12
- package/packages/mailx-store/db.d.ts +6 -2
- package/packages/mailx-store/db.js +78 -16
- package/packages/mailx-store/file-store.d.ts +2 -8
- package/packages/mailx-store/file-store.js +7 -31
- package/packages/mailx-types/index.d.ts +3 -1
- package/.tswalk.json +0 -7396
- package/launcher/release.cmd +0 -4
- package/mailx.json +0 -9
- package/packages/mailx-api/node_modules/nodemailer/.ncurc.js +0 -9
- package/packages/mailx-api/node_modules/nodemailer/.prettierignore +0 -8
- package/packages/mailx-api/node_modules/nodemailer/.prettierrc +0 -12
- package/packages/mailx-api/node_modules/nodemailer/.prettierrc.js +0 -10
- package/packages/mailx-api/node_modules/nodemailer/.release-please-config.json +0 -9
- package/packages/mailx-api/node_modules/nodemailer/LICENSE +0 -16
- package/packages/mailx-api/node_modules/nodemailer/README.md +0 -86
- package/packages/mailx-api/node_modules/nodemailer/SECURITY.txt +0 -22
- package/packages/mailx-api/node_modules/nodemailer/eslint.config.js +0 -88
- package/packages/mailx-api/node_modules/nodemailer/lib/addressparser/index.js +0 -383
- package/packages/mailx-api/node_modules/nodemailer/lib/base64/index.js +0 -139
- package/packages/mailx-api/node_modules/nodemailer/lib/dkim/index.js +0 -253
- package/packages/mailx-api/node_modules/nodemailer/lib/dkim/message-parser.js +0 -155
- package/packages/mailx-api/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
- package/packages/mailx-api/node_modules/nodemailer/lib/dkim/sign.js +0 -117
- package/packages/mailx-api/node_modules/nodemailer/lib/fetch/cookies.js +0 -281
- package/packages/mailx-api/node_modules/nodemailer/lib/fetch/index.js +0 -280
- package/packages/mailx-api/node_modules/nodemailer/lib/json-transport/index.js +0 -82
- package/packages/mailx-api/node_modules/nodemailer/lib/mail-composer/index.js +0 -629
- package/packages/mailx-api/node_modules/nodemailer/lib/mailer/index.js +0 -441
- package/packages/mailx-api/node_modules/nodemailer/lib/mailer/mail-message.js +0 -316
- package/packages/mailx-api/node_modules/nodemailer/lib/mime-funcs/index.js +0 -625
- package/packages/mailx-api/node_modules/nodemailer/lib/mime-funcs/mime-types.js +0 -2113
- package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/index.js +0 -1316
- package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/last-newline.js +0 -33
- package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/le-unix.js +0 -43
- package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/le-windows.js +0 -52
- package/packages/mailx-api/node_modules/nodemailer/lib/nodemailer.js +0 -157
- package/packages/mailx-api/node_modules/nodemailer/lib/punycode/index.js +0 -460
- package/packages/mailx-api/node_modules/nodemailer/lib/qp/index.js +0 -227
- package/packages/mailx-api/node_modules/nodemailer/lib/sendmail-transport/index.js +0 -210
- package/packages/mailx-api/node_modules/nodemailer/lib/ses-transport/index.js +0 -234
- package/packages/mailx-api/node_modules/nodemailer/lib/shared/index.js +0 -754
- package/packages/mailx-api/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -108
- package/packages/mailx-api/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +0 -143
- package/packages/mailx-api/node_modules/nodemailer/lib/smtp-connection/index.js +0 -1870
- package/packages/mailx-api/node_modules/nodemailer/lib/smtp-pool/index.js +0 -652
- package/packages/mailx-api/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +0 -259
- package/packages/mailx-api/node_modules/nodemailer/lib/smtp-transport/index.js +0 -421
- package/packages/mailx-api/node_modules/nodemailer/lib/stream-transport/index.js +0 -135
- package/packages/mailx-api/node_modules/nodemailer/lib/well-known/index.js +0 -47
- package/packages/mailx-api/node_modules/nodemailer/lib/well-known/services.json +0 -611
- package/packages/mailx-api/node_modules/nodemailer/lib/xoauth2/index.js +0 -427
- package/packages/mailx-api/node_modules/nodemailer/package.json +0 -47
- package/packages/mailx-imap/node_modules/nodemailer/.ncurc.js +0 -9
- package/packages/mailx-imap/node_modules/nodemailer/.prettierignore +0 -8
- package/packages/mailx-imap/node_modules/nodemailer/.prettierrc +0 -12
- package/packages/mailx-imap/node_modules/nodemailer/.prettierrc.js +0 -10
- package/packages/mailx-imap/node_modules/nodemailer/.release-please-config.json +0 -9
- package/packages/mailx-imap/node_modules/nodemailer/LICENSE +0 -16
- package/packages/mailx-imap/node_modules/nodemailer/README.md +0 -86
- package/packages/mailx-imap/node_modules/nodemailer/SECURITY.txt +0 -22
- package/packages/mailx-imap/node_modules/nodemailer/eslint.config.js +0 -88
- package/packages/mailx-imap/node_modules/nodemailer/lib/addressparser/index.js +0 -383
- package/packages/mailx-imap/node_modules/nodemailer/lib/base64/index.js +0 -139
- package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/index.js +0 -253
- package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/message-parser.js +0 -155
- package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
- package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/sign.js +0 -117
- package/packages/mailx-imap/node_modules/nodemailer/lib/fetch/cookies.js +0 -281
- package/packages/mailx-imap/node_modules/nodemailer/lib/fetch/index.js +0 -280
- package/packages/mailx-imap/node_modules/nodemailer/lib/json-transport/index.js +0 -82
- package/packages/mailx-imap/node_modules/nodemailer/lib/mail-composer/index.js +0 -629
- package/packages/mailx-imap/node_modules/nodemailer/lib/mailer/index.js +0 -441
- package/packages/mailx-imap/node_modules/nodemailer/lib/mailer/mail-message.js +0 -316
- package/packages/mailx-imap/node_modules/nodemailer/lib/mime-funcs/index.js +0 -625
- package/packages/mailx-imap/node_modules/nodemailer/lib/mime-funcs/mime-types.js +0 -2113
- package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/index.js +0 -1316
- package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/last-newline.js +0 -33
- package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/le-unix.js +0 -43
- package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/le-windows.js +0 -52
- package/packages/mailx-imap/node_modules/nodemailer/lib/nodemailer.js +0 -157
- package/packages/mailx-imap/node_modules/nodemailer/lib/punycode/index.js +0 -460
- package/packages/mailx-imap/node_modules/nodemailer/lib/qp/index.js +0 -227
- package/packages/mailx-imap/node_modules/nodemailer/lib/sendmail-transport/index.js +0 -210
- package/packages/mailx-imap/node_modules/nodemailer/lib/ses-transport/index.js +0 -234
- package/packages/mailx-imap/node_modules/nodemailer/lib/shared/index.js +0 -754
- package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -108
- package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +0 -143
- package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-connection/index.js +0 -1870
- package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-pool/index.js +0 -652
- package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +0 -259
- package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-transport/index.js +0 -421
- package/packages/mailx-imap/node_modules/nodemailer/lib/stream-transport/index.js +0 -135
- package/packages/mailx-imap/node_modules/nodemailer/lib/well-known/index.js +0 -47
- package/packages/mailx-imap/node_modules/nodemailer/lib/well-known/services.json +0 -611
- package/packages/mailx-imap/node_modules/nodemailer/lib/xoauth2/index.js +0 -427
- package/packages/mailx-imap/node_modules/nodemailer/package.json +0 -47
- package/packages/mailx-send/node_modules/nodemailer/.ncurc.js +0 -9
- package/packages/mailx-send/node_modules/nodemailer/.prettierignore +0 -8
- package/packages/mailx-send/node_modules/nodemailer/.prettierrc +0 -12
- package/packages/mailx-send/node_modules/nodemailer/.prettierrc.js +0 -10
- package/packages/mailx-send/node_modules/nodemailer/.release-please-config.json +0 -9
- package/packages/mailx-send/node_modules/nodemailer/LICENSE +0 -16
- package/packages/mailx-send/node_modules/nodemailer/README.md +0 -86
- package/packages/mailx-send/node_modules/nodemailer/SECURITY.txt +0 -22
- package/packages/mailx-send/node_modules/nodemailer/eslint.config.js +0 -88
- package/packages/mailx-send/node_modules/nodemailer/lib/addressparser/index.js +0 -383
- package/packages/mailx-send/node_modules/nodemailer/lib/base64/index.js +0 -139
- package/packages/mailx-send/node_modules/nodemailer/lib/dkim/index.js +0 -253
- package/packages/mailx-send/node_modules/nodemailer/lib/dkim/message-parser.js +0 -155
- package/packages/mailx-send/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
- package/packages/mailx-send/node_modules/nodemailer/lib/dkim/sign.js +0 -117
- package/packages/mailx-send/node_modules/nodemailer/lib/fetch/cookies.js +0 -281
- package/packages/mailx-send/node_modules/nodemailer/lib/fetch/index.js +0 -280
- package/packages/mailx-send/node_modules/nodemailer/lib/json-transport/index.js +0 -82
- package/packages/mailx-send/node_modules/nodemailer/lib/mail-composer/index.js +0 -629
- package/packages/mailx-send/node_modules/nodemailer/lib/mailer/index.js +0 -441
- package/packages/mailx-send/node_modules/nodemailer/lib/mailer/mail-message.js +0 -316
- package/packages/mailx-send/node_modules/nodemailer/lib/mime-funcs/index.js +0 -625
- package/packages/mailx-send/node_modules/nodemailer/lib/mime-funcs/mime-types.js +0 -2113
- package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/index.js +0 -1316
- package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/last-newline.js +0 -33
- package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/le-unix.js +0 -43
- package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/le-windows.js +0 -52
- package/packages/mailx-send/node_modules/nodemailer/lib/nodemailer.js +0 -157
- package/packages/mailx-send/node_modules/nodemailer/lib/punycode/index.js +0 -460
- package/packages/mailx-send/node_modules/nodemailer/lib/qp/index.js +0 -227
- package/packages/mailx-send/node_modules/nodemailer/lib/sendmail-transport/index.js +0 -210
- package/packages/mailx-send/node_modules/nodemailer/lib/ses-transport/index.js +0 -234
- package/packages/mailx-send/node_modules/nodemailer/lib/shared/index.js +0 -754
- package/packages/mailx-send/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -108
- package/packages/mailx-send/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +0 -143
- package/packages/mailx-send/node_modules/nodemailer/lib/smtp-connection/index.js +0 -1870
- package/packages/mailx-send/node_modules/nodemailer/lib/smtp-pool/index.js +0 -652
- package/packages/mailx-send/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +0 -259
- package/packages/mailx-send/node_modules/nodemailer/lib/smtp-transport/index.js +0 -421
- package/packages/mailx-send/node_modules/nodemailer/lib/stream-transport/index.js +0 -135
- package/packages/mailx-send/node_modules/nodemailer/lib/well-known/index.js +0 -47
- package/packages/mailx-send/node_modules/nodemailer/lib/well-known/services.json +0 -611
- package/packages/mailx-send/node_modules/nodemailer/lib/xoauth2/index.js +0 -427
- package/packages/mailx-send/node_modules/nodemailer/package.json +0 -47
|
@@ -3,10 +3,24 @@
|
|
|
3
3
|
* Loads more messages on scroll.
|
|
4
4
|
*/
|
|
5
5
|
import { getMessages, getUnifiedInbox, searchMessages } from "../lib/api-client.js";
|
|
6
|
+
function clearFilter() {
|
|
7
|
+
const f = document.getElementById("ml-filter-input");
|
|
8
|
+
if (f)
|
|
9
|
+
f.value = "";
|
|
10
|
+
}
|
|
11
|
+
/** Clear the message viewer when no message is selected */
|
|
12
|
+
function clearViewer() {
|
|
13
|
+
const bodyEl = document.getElementById("mv-body");
|
|
14
|
+
const headerEl = document.getElementById("mv-header");
|
|
15
|
+
if (bodyEl)
|
|
16
|
+
bodyEl.innerHTML = `<div class="mv-empty">Select a message to read</div>`;
|
|
17
|
+
if (headerEl)
|
|
18
|
+
headerEl.hidden = true;
|
|
19
|
+
}
|
|
6
20
|
let onMessageSelect;
|
|
7
21
|
let currentAccountId;
|
|
8
22
|
let currentFolderId;
|
|
9
|
-
let
|
|
23
|
+
let lastClickedRow = null;
|
|
10
24
|
let currentPage;
|
|
11
25
|
let totalMessages;
|
|
12
26
|
let loading = false;
|
|
@@ -14,11 +28,62 @@ let unifiedMode = false;
|
|
|
14
28
|
let searchMode = false;
|
|
15
29
|
let currentSearchQuery = "";
|
|
16
30
|
let showToInsteadOfFrom = false;
|
|
31
|
+
/** Get all selected message rows */
|
|
32
|
+
export function getSelectedMessages() {
|
|
33
|
+
const body = document.getElementById("ml-body");
|
|
34
|
+
if (!body)
|
|
35
|
+
return [];
|
|
36
|
+
const rows = body.querySelectorAll(".ml-row.selected");
|
|
37
|
+
return Array.from(rows).map(r => ({
|
|
38
|
+
accountId: r.dataset.accountId || "",
|
|
39
|
+
uid: Number(r.dataset.uid),
|
|
40
|
+
folderId: Number(r.dataset.folderId),
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
function clearSelection() {
|
|
44
|
+
const body = document.getElementById("ml-body");
|
|
45
|
+
if (body)
|
|
46
|
+
body.querySelectorAll(".ml-row.selected").forEach(r => r.classList.remove("selected"));
|
|
47
|
+
}
|
|
48
|
+
function selectRange(from, to) {
|
|
49
|
+
const body = document.getElementById("ml-body");
|
|
50
|
+
if (!body)
|
|
51
|
+
return;
|
|
52
|
+
const rows = Array.from(body.querySelectorAll(".ml-row"));
|
|
53
|
+
const fromIdx = rows.indexOf(from);
|
|
54
|
+
const toIdx = rows.indexOf(to);
|
|
55
|
+
if (fromIdx < 0 || toIdx < 0)
|
|
56
|
+
return;
|
|
57
|
+
const lo = Math.min(fromIdx, toIdx);
|
|
58
|
+
const hi = Math.max(fromIdx, toIdx);
|
|
59
|
+
for (let i = lo; i <= hi; i++)
|
|
60
|
+
rows[i].classList.add("selected");
|
|
61
|
+
}
|
|
17
62
|
const timeFmt = { hour: "2-digit", minute: "2-digit", hour12: false };
|
|
18
63
|
const dateFmt = { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false };
|
|
19
64
|
const dateFmtSameYear = { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false };
|
|
20
65
|
export function initMessageList(handler) {
|
|
21
66
|
onMessageSelect = handler;
|
|
67
|
+
// Client-side filter
|
|
68
|
+
const filterInput = document.getElementById("ml-filter-input");
|
|
69
|
+
if (filterInput) {
|
|
70
|
+
filterInput.addEventListener("input", () => {
|
|
71
|
+
const query = filterInput.value.toLowerCase();
|
|
72
|
+
const body = document.getElementById("ml-body");
|
|
73
|
+
if (!body)
|
|
74
|
+
return;
|
|
75
|
+
for (const row of body.querySelectorAll(".ml-row")) {
|
|
76
|
+
const text = row.textContent?.toLowerCase() || "";
|
|
77
|
+
row.classList.toggle("filter-hidden", query.length > 0 && !text.includes(query));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
filterInput.addEventListener("keydown", (e) => {
|
|
81
|
+
if (e.key === "Escape") {
|
|
82
|
+
filterInput.value = "";
|
|
83
|
+
filterInput.dispatchEvent(new Event("input"));
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
22
87
|
// Infinite scroll
|
|
23
88
|
const body = document.getElementById("ml-body");
|
|
24
89
|
if (body) {
|
|
@@ -46,6 +111,7 @@ export function reloadCurrentFolder() {
|
|
|
46
111
|
}
|
|
47
112
|
/** Load unified inbox (all accounts) */
|
|
48
113
|
export async function loadUnifiedInbox() {
|
|
114
|
+
clearFilter();
|
|
49
115
|
unifiedMode = true;
|
|
50
116
|
currentPage = 1;
|
|
51
117
|
totalMessages = 0;
|
|
@@ -58,17 +124,24 @@ export async function loadUnifiedInbox() {
|
|
|
58
124
|
totalMessages = result.total;
|
|
59
125
|
if (result.items.length === 0) {
|
|
60
126
|
body.innerHTML = `<div class="ml-empty">No messages</div>`;
|
|
127
|
+
clearViewer();
|
|
61
128
|
return;
|
|
62
129
|
}
|
|
63
130
|
body.innerHTML = "";
|
|
64
131
|
appendMessages(body, "", result.items);
|
|
132
|
+
const firstRow = body.querySelector(".ml-row");
|
|
133
|
+
if (firstRow)
|
|
134
|
+
firstRow.click();
|
|
65
135
|
}
|
|
66
136
|
catch (e) {
|
|
137
|
+
if (e.name === "AbortError")
|
|
138
|
+
return;
|
|
67
139
|
body.innerHTML = `<div class="ml-empty">Error: ${e.message}</div>`;
|
|
68
140
|
}
|
|
69
141
|
}
|
|
70
142
|
/** Load search results */
|
|
71
|
-
export async function loadSearchResults(query) {
|
|
143
|
+
export async function loadSearchResults(query, scope = "all", accountId = "", folderId = 0) {
|
|
144
|
+
clearFilter();
|
|
72
145
|
searchMode = true;
|
|
73
146
|
unifiedMode = false;
|
|
74
147
|
currentSearchQuery = query;
|
|
@@ -79,7 +152,35 @@ export async function loadSearchResults(query) {
|
|
|
79
152
|
return;
|
|
80
153
|
body.innerHTML = `<div class="ml-empty">Searching...</div>`;
|
|
81
154
|
try {
|
|
82
|
-
|
|
155
|
+
// Regex search: filter client-side
|
|
156
|
+
if (query.startsWith("/") && query.endsWith("/") && query.length > 2) {
|
|
157
|
+
const pattern = query.slice(1, -1);
|
|
158
|
+
let regex;
|
|
159
|
+
try {
|
|
160
|
+
regex = new RegExp(pattern, "i");
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
body.innerHTML = `<div class="ml-empty">Invalid regex</div>`;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// Get all messages from current context and filter
|
|
167
|
+
const source = scope === "current" && accountId
|
|
168
|
+
? await getMessages(accountId, folderId, 1, 10000)
|
|
169
|
+
: await searchMessages("*", 1, 10000, "all");
|
|
170
|
+
const matches = source.items.filter((m) => regex.test(m.subject || "") || regex.test(m.from?.name || "") || regex.test(m.from?.address || "") || regex.test(m.preview || ""));
|
|
171
|
+
totalMessages = matches.length;
|
|
172
|
+
if (matches.length === 0) {
|
|
173
|
+
body.innerHTML = `<div class="ml-empty">No regex matches</div>`;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
body.innerHTML = "";
|
|
177
|
+
appendMessages(body, "", matches);
|
|
178
|
+
const firstRow = body.querySelector(".ml-row");
|
|
179
|
+
if (firstRow)
|
|
180
|
+
firstRow.click();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const result = await searchMessages(query, 1, 50, scope, accountId, folderId);
|
|
83
184
|
totalMessages = result.total;
|
|
84
185
|
if (result.items.length === 0) {
|
|
85
186
|
body.innerHTML = `<div class="ml-empty">No results for "${query}"</div>`;
|
|
@@ -93,6 +194,7 @@ export async function loadSearchResults(query) {
|
|
|
93
194
|
}
|
|
94
195
|
}
|
|
95
196
|
export async function loadMessages(accountId, folderId, page = 1, specialUse = "") {
|
|
197
|
+
clearFilter();
|
|
96
198
|
searchMode = false;
|
|
97
199
|
unifiedMode = false;
|
|
98
200
|
showToInsteadOfFrom = ["sent", "drafts", "outbox"].includes(specialUse) ||
|
|
@@ -114,12 +216,19 @@ export async function loadMessages(accountId, folderId, page = 1, specialUse = "
|
|
|
114
216
|
totalMessages = result.total;
|
|
115
217
|
if (result.items.length === 0) {
|
|
116
218
|
body.innerHTML = `<div class="ml-empty">No messages</div>`;
|
|
219
|
+
clearViewer();
|
|
117
220
|
return;
|
|
118
221
|
}
|
|
119
222
|
body.innerHTML = "";
|
|
120
223
|
appendMessages(body, accountId, result.items);
|
|
224
|
+
// Auto-select first message
|
|
225
|
+
const firstRow = body.querySelector(".ml-row");
|
|
226
|
+
if (firstRow)
|
|
227
|
+
firstRow.click();
|
|
121
228
|
}
|
|
122
229
|
catch (e) {
|
|
230
|
+
if (e.name === "AbortError")
|
|
231
|
+
return; // Superseded by newer request
|
|
123
232
|
body.innerHTML = `<div class="ml-empty">Error: ${e.message}</div>`;
|
|
124
233
|
}
|
|
125
234
|
}
|
|
@@ -146,13 +255,18 @@ async function loadMoreMessages() {
|
|
|
146
255
|
}
|
|
147
256
|
function appendMessages(body, accountId, items) {
|
|
148
257
|
for (const msg of items) {
|
|
258
|
+
// Use per-message accountId for unified inbox, fallback to list-level accountId
|
|
259
|
+
const msgAccountId = msg.accountId || accountId;
|
|
149
260
|
const row = document.createElement("div");
|
|
150
261
|
row.className = "ml-row";
|
|
262
|
+
row.draggable = true;
|
|
151
263
|
if (!msg.flags.includes("\\Seen"))
|
|
152
264
|
row.classList.add("unread");
|
|
153
265
|
if (msg.flags.includes("\\Flagged"))
|
|
154
266
|
row.classList.add("flagged");
|
|
155
267
|
row.dataset.uid = String(msg.uid);
|
|
268
|
+
row.dataset.accountId = msgAccountId;
|
|
269
|
+
row.dataset.folderId = String(msg.folderId);
|
|
156
270
|
const flag = document.createElement("span");
|
|
157
271
|
flag.className = "ml-flag";
|
|
158
272
|
flag.textContent = msg.flags.includes("\\Flagged") ? "\u2605" : "\u2606"; // ★ or ☆
|
|
@@ -185,7 +299,7 @@ function appendMessages(body, accountId, items) {
|
|
|
185
299
|
? currentFlags.filter((f) => f !== "\\Flagged")
|
|
186
300
|
: [...currentFlags, "\\Flagged"];
|
|
187
301
|
try {
|
|
188
|
-
await fetch(`/api/message/${
|
|
302
|
+
await fetch(`/api/message/${msgAccountId}/${msg.uid}/flags`, {
|
|
189
303
|
method: "PATCH",
|
|
190
304
|
headers: { "Content-Type": "application/json" },
|
|
191
305
|
body: JSON.stringify({ flags: newFlags }),
|
|
@@ -200,14 +314,54 @@ function appendMessages(body, accountId, items) {
|
|
|
200
314
|
row.appendChild(from);
|
|
201
315
|
row.appendChild(date);
|
|
202
316
|
row.appendChild(subject);
|
|
203
|
-
row.addEventListener("click", () => {
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
317
|
+
row.addEventListener("click", (e) => {
|
|
318
|
+
if (e.shiftKey && lastClickedRow) {
|
|
319
|
+
// Shift+click: range select from last clicked to this row
|
|
320
|
+
clearSelection();
|
|
321
|
+
selectRange(lastClickedRow, row);
|
|
322
|
+
}
|
|
323
|
+
else if (e.ctrlKey || e.metaKey) {
|
|
324
|
+
// Ctrl+click: toggle this row
|
|
325
|
+
row.classList.toggle("selected");
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
// Plain click: single select
|
|
329
|
+
clearSelection();
|
|
330
|
+
row.classList.add("selected");
|
|
331
|
+
}
|
|
332
|
+
lastClickedRow = row;
|
|
208
333
|
row.classList.remove("unread");
|
|
209
|
-
onMessageSelect(
|
|
334
|
+
onMessageSelect(msgAccountId, msg.uid, msg.folderId);
|
|
335
|
+
});
|
|
336
|
+
row.addEventListener("dragstart", (e) => {
|
|
337
|
+
// If dragging a non-selected row, select it first
|
|
338
|
+
if (!row.classList.contains("selected")) {
|
|
339
|
+
clearSelection();
|
|
340
|
+
row.classList.add("selected");
|
|
341
|
+
lastClickedRow = row;
|
|
342
|
+
}
|
|
343
|
+
const selected = getSelectedMessages();
|
|
344
|
+
e.dataTransfer.setData("application/x-mailx-messages", JSON.stringify(selected));
|
|
345
|
+
// Legacy single-message format for backwards compat
|
|
346
|
+
e.dataTransfer.setData("application/x-mailx-message", JSON.stringify({
|
|
347
|
+
accountId: msgAccountId,
|
|
348
|
+
uid: msg.uid,
|
|
349
|
+
folderId: msg.folderId,
|
|
350
|
+
subject: msg.subject,
|
|
351
|
+
}));
|
|
352
|
+
e.dataTransfer.effectAllowed = "copyMove";
|
|
353
|
+
row.classList.add("dragging");
|
|
354
|
+
// Show drag count
|
|
355
|
+
if (selected.length > 1) {
|
|
356
|
+
const badge = document.createElement("div");
|
|
357
|
+
badge.textContent = `${selected.length} messages`;
|
|
358
|
+
badge.style.cssText = "position:absolute;top:-1000px;background:#333;color:white;padding:4px 8px;border-radius:4px;font-size:12px";
|
|
359
|
+
document.body.appendChild(badge);
|
|
360
|
+
e.dataTransfer.setDragImage(badge, 0, 0);
|
|
361
|
+
setTimeout(() => badge.remove(), 0);
|
|
362
|
+
}
|
|
210
363
|
});
|
|
364
|
+
row.addEventListener("dragend", () => row.classList.remove("dragging"));
|
|
211
365
|
body.appendChild(row);
|
|
212
366
|
}
|
|
213
367
|
}
|
|
@@ -5,12 +5,14 @@ import { getMessage, updateFlags } from "../lib/api-client.js";
|
|
|
5
5
|
/** Currently displayed message (for reply/forward) */
|
|
6
6
|
let currentMessage = null;
|
|
7
7
|
let currentAccountId = "";
|
|
8
|
+
let showMessageGeneration = 0; // Cancel stale fetches
|
|
8
9
|
export function getCurrentMessage() {
|
|
9
10
|
if (!currentMessage)
|
|
10
11
|
return null;
|
|
11
12
|
return { accountId: currentAccountId, message: currentMessage };
|
|
12
13
|
}
|
|
13
|
-
export async function showMessage(accountId, uid) {
|
|
14
|
+
export async function showMessage(accountId, uid, folderId, specialUse) {
|
|
15
|
+
const gen = ++showMessageGeneration;
|
|
14
16
|
const headerEl = document.getElementById("mv-header");
|
|
15
17
|
const bodyEl = document.getElementById("mv-body");
|
|
16
18
|
const attEl = document.getElementById("mv-attachments");
|
|
@@ -18,9 +20,10 @@ export async function showMessage(accountId, uid) {
|
|
|
18
20
|
headerEl.hidden = true;
|
|
19
21
|
attEl.hidden = true;
|
|
20
22
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
const msg = await getMessage(accountId, uid, false, folderId);
|
|
24
|
+
// Stale response — a newer showMessage was called while we were fetching
|
|
25
|
+
if (gen !== showMessageGeneration)
|
|
26
|
+
return;
|
|
24
27
|
currentMessage = msg;
|
|
25
28
|
currentAccountId = accountId;
|
|
26
29
|
// Mark as read
|
|
@@ -37,18 +40,114 @@ export async function showMessage(accountId, uid) {
|
|
|
37
40
|
}
|
|
38
41
|
headerEl.querySelector(".mv-subject").textContent = msg.subject;
|
|
39
42
|
headerEl.querySelector(".mv-date").textContent = new Date(msg.date).toLocaleString(undefined, { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false });
|
|
40
|
-
//
|
|
43
|
+
// Unsubscribe button (upper right of header)
|
|
44
|
+
const unsubBtn = document.getElementById("mv-unsubscribe");
|
|
45
|
+
const headerUnsub = msg.listUnsubscribe || "";
|
|
46
|
+
const headerUnsubUrl = headerUnsub.match(/<(https?:\/\/[^>]+)>/)?.[1]
|
|
47
|
+
|| headerUnsub.match(/<(mailto:[^>]+)>/)?.[1] || "";
|
|
48
|
+
if (unsubBtn) {
|
|
49
|
+
if (headerUnsubUrl) {
|
|
50
|
+
unsubBtn.hidden = false;
|
|
51
|
+
unsubBtn.href = headerUnsubUrl;
|
|
52
|
+
unsubBtn.target = "_blank";
|
|
53
|
+
unsubBtn.rel = "noopener noreferrer";
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
unsubBtn.hidden = true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// View Source button — shows .eml file path
|
|
60
|
+
const srcBtn = document.getElementById("mv-view-source");
|
|
61
|
+
if (srcBtn) {
|
|
62
|
+
if (msg.emlPath) {
|
|
63
|
+
srcBtn.hidden = false;
|
|
64
|
+
srcBtn.title = msg.emlPath;
|
|
65
|
+
srcBtn.onclick = () => {
|
|
66
|
+
// Copy path to clipboard and show in status bar
|
|
67
|
+
navigator.clipboard.writeText(msg.emlPath).then(() => {
|
|
68
|
+
const status = document.getElementById("status-sync");
|
|
69
|
+
if (status)
|
|
70
|
+
status.textContent = `Path copied: ${msg.emlPath}`;
|
|
71
|
+
}).catch(() => {
|
|
72
|
+
prompt("EML file path:", msg.emlPath);
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
srcBtn.hidden = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Edit Draft / Send from Outbox button
|
|
81
|
+
const editBtn = document.getElementById("mv-edit-draft");
|
|
82
|
+
if (editBtn) {
|
|
83
|
+
const isDraft = specialUse === "drafts" || specialUse === "outbox";
|
|
84
|
+
if (isDraft) {
|
|
85
|
+
editBtn.hidden = false;
|
|
86
|
+
editBtn.textContent = specialUse === "outbox" ? "Edit & Send" : "Edit Draft";
|
|
87
|
+
editBtn.onclick = () => {
|
|
88
|
+
// Open compose window pre-filled with this draft
|
|
89
|
+
const init = {
|
|
90
|
+
mode: "draft",
|
|
91
|
+
accountId,
|
|
92
|
+
to: msg.to || [],
|
|
93
|
+
cc: msg.cc || [],
|
|
94
|
+
subject: msg.subject || "",
|
|
95
|
+
bodyHtml: msg.bodyHtml || "",
|
|
96
|
+
inReplyTo: msg.inReplyTo || "",
|
|
97
|
+
references: msg.references || [],
|
|
98
|
+
accounts: [],
|
|
99
|
+
draftUid: msg.uid,
|
|
100
|
+
draftFolderId: msg.folderId,
|
|
101
|
+
};
|
|
102
|
+
sessionStorage.setItem("composeInit", JSON.stringify(init));
|
|
103
|
+
window.open("/compose/compose.html", "_blank", "width=800,height=600,menubar=no,toolbar=no,status=no");
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
editBtn.hidden = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Remote content banner (collapsible dropdown with sender/recipient details)
|
|
41
111
|
bodyEl.innerHTML = "";
|
|
42
112
|
if (msg.hasRemoteContent) {
|
|
43
113
|
const senderAddr = msg.from?.address || "";
|
|
114
|
+
const senderName = msg.from?.name || "";
|
|
44
115
|
const senderDomain = senderAddr.split("@")[1] || "";
|
|
116
|
+
const deliveredTo = msg.deliveredTo || "";
|
|
117
|
+
const toAddr = msg.to?.[0]?.address || "";
|
|
118
|
+
const returnPath = msg.returnPath || "";
|
|
45
119
|
const banner = document.createElement("div");
|
|
46
120
|
banner.className = "mv-remote-banner";
|
|
47
|
-
banner.innerHTML =
|
|
48
|
-
`<
|
|
49
|
-
|
|
50
|
-
|
|
121
|
+
banner.innerHTML =
|
|
122
|
+
`<div class="mv-rb-summary">` +
|
|
123
|
+
`<span class="mv-rb-toggle">▸</span>` +
|
|
124
|
+
`<span>Remote content blocked</span>` +
|
|
125
|
+
`<span class="mv-rb-buttons">` +
|
|
126
|
+
`<button id="btn-load-remote">Load once</button>` +
|
|
127
|
+
`<button id="btn-allow-sender" title="${escapeText(senderAddr)}">Always: ${escapeText(senderAddr)}</button>` +
|
|
128
|
+
(senderDomain ? `<button id="btn-allow-domain" title="*@${escapeText(senderDomain)}">Always: *@${escapeText(senderDomain)}</button>` : "") +
|
|
129
|
+
`</span>` +
|
|
130
|
+
`</div>` +
|
|
131
|
+
`<div class="mv-rb-details" hidden>` +
|
|
132
|
+
`<div class="mv-rb-info">` +
|
|
133
|
+
`<div><span class="mv-rb-label">From:</span> ${escapeText(senderName ? `${senderName} <${senderAddr}>` : senderAddr)}</div>` +
|
|
134
|
+
(deliveredTo ? `<div><span class="mv-rb-label">Delivered-To:</span> ${escapeText(deliveredTo)}</div>` : "") +
|
|
135
|
+
(toAddr && toAddr !== deliveredTo ? `<div><span class="mv-rb-label">To:</span> ${escapeText(toAddr)}</div>` : "") +
|
|
136
|
+
(returnPath && returnPath !== senderAddr ? `<div><span class="mv-rb-label">Return-Path:</span> ${escapeText(returnPath)}</div>` : "") +
|
|
137
|
+
`</div>` +
|
|
138
|
+
(deliveredTo || toAddr ? `<div class="mv-rb-actions"><button id="btn-allow-to">Always allow to: ${escapeText(deliveredTo || toAddr)}</button></div>` : "") +
|
|
139
|
+
`</div>`;
|
|
51
140
|
bodyEl.appendChild(banner);
|
|
141
|
+
// Toggle dropdown — click arrow or text to expand details
|
|
142
|
+
const summary = banner.querySelector(".mv-rb-summary");
|
|
143
|
+
const details = banner.querySelector(".mv-rb-details");
|
|
144
|
+
const toggle = banner.querySelector(".mv-rb-toggle");
|
|
145
|
+
summary.addEventListener("click", (e) => {
|
|
146
|
+
if (e.target.tagName === "BUTTON")
|
|
147
|
+
return;
|
|
148
|
+
details.hidden = !details.hidden;
|
|
149
|
+
toggle.textContent = details.hidden ? "\u25B8" : "\u25BE";
|
|
150
|
+
});
|
|
52
151
|
const loadRemote = async () => {
|
|
53
152
|
banner.remove();
|
|
54
153
|
const full = await getMessage(accountId, uid, true);
|
|
@@ -78,6 +177,17 @@ export async function showMessage(accountId, uid) {
|
|
|
78
177
|
});
|
|
79
178
|
loadRemote();
|
|
80
179
|
});
|
|
180
|
+
banner.querySelector("#btn-allow-to")?.addEventListener("click", async () => {
|
|
181
|
+
const addr = deliveredTo || toAddr;
|
|
182
|
+
if (!addr)
|
|
183
|
+
return;
|
|
184
|
+
await fetch("/api/settings/allow-remote", {
|
|
185
|
+
method: "POST",
|
|
186
|
+
headers: { "Content-Type": "application/json" },
|
|
187
|
+
body: JSON.stringify({ type: "recipient", value: addr }),
|
|
188
|
+
});
|
|
189
|
+
loadRemote();
|
|
190
|
+
});
|
|
81
191
|
}
|
|
82
192
|
// Body in sandboxed iframe
|
|
83
193
|
if (msg.bodyHtml) {
|
|
@@ -85,7 +195,8 @@ export async function showMessage(accountId, uid) {
|
|
|
85
195
|
iframe.sandbox.add("allow-same-origin");
|
|
86
196
|
iframe.sandbox.add("allow-popups");
|
|
87
197
|
iframe.sandbox.add("allow-popups-to-escape-sandbox");
|
|
88
|
-
iframe.
|
|
198
|
+
iframe.sandbox.add("allow-top-navigation-by-user-activation");
|
|
199
|
+
iframe.srcdoc = wrapHtmlBody(msg.bodyHtml, msg.remoteAllowed);
|
|
89
200
|
bodyEl.appendChild(iframe);
|
|
90
201
|
}
|
|
91
202
|
else if (msg.bodyText) {
|
|
@@ -118,6 +229,11 @@ function formatAddr(addr) {
|
|
|
118
229
|
return `${addr.name} <${addr.address}>`;
|
|
119
230
|
return addr.address;
|
|
120
231
|
}
|
|
232
|
+
function escapeText(s) {
|
|
233
|
+
const div = document.createElement("div");
|
|
234
|
+
div.textContent = s;
|
|
235
|
+
return div.innerHTML;
|
|
236
|
+
}
|
|
121
237
|
function formatSize(bytes) {
|
|
122
238
|
if (bytes < 1024)
|
|
123
239
|
return `${bytes} B`;
|
|
@@ -126,9 +242,10 @@ function formatSize(bytes) {
|
|
|
126
242
|
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
127
243
|
}
|
|
128
244
|
function wrapHtmlBody(html, allowRemote = false) {
|
|
245
|
+
// CSP blocks remote resource loading (tracking pixels, external CSS) but allows link clicks
|
|
129
246
|
const csp = allowRemote
|
|
130
|
-
? ""
|
|
131
|
-
: `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data: cid:;">`;
|
|
247
|
+
? ""
|
|
248
|
+
: `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data: cid:; form-action 'none';">`;
|
|
132
249
|
return `<!DOCTYPE html>
|
|
133
250
|
<html><head>
|
|
134
251
|
<meta charset="UTF-8">
|
|
@@ -136,7 +253,7 @@ ${csp}
|
|
|
136
253
|
<style>
|
|
137
254
|
body {
|
|
138
255
|
font-family: system-ui, sans-serif;
|
|
139
|
-
font-size:
|
|
256
|
+
font-size: 17.5px;
|
|
140
257
|
line-height: 1.5;
|
|
141
258
|
color: #1a1a2e;
|
|
142
259
|
background: #fff;
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div class="compose-header">
|
|
16
|
-
<div class="compose-field">
|
|
17
|
-
<label for="compose-from">From</label>
|
|
18
|
-
<
|
|
19
|
-
<
|
|
16
|
+
<div class="compose-field compose-from-field">
|
|
17
|
+
<label for="compose-from-select">From</label>
|
|
18
|
+
<select id="compose-from-select"></select>
|
|
19
|
+
<input type="text" id="compose-from-custom" placeholder="Custom address..." hidden>
|
|
20
20
|
</div>
|
|
21
21
|
<div class="compose-field">
|
|
22
22
|
<label for="compose-to">To</label>
|
|
@@ -18,27 +18,59 @@ const editor = new Quill("#compose-editor", {
|
|
|
18
18
|
}
|
|
19
19
|
});
|
|
20
20
|
// ── Populate from init data ──
|
|
21
|
-
const
|
|
22
|
-
const
|
|
21
|
+
const fromSelect = document.getElementById("compose-from-select");
|
|
22
|
+
const fromCustom = document.getElementById("compose-from-custom");
|
|
23
23
|
const toInput = document.getElementById("compose-to");
|
|
24
24
|
const ccInput = document.getElementById("compose-cc");
|
|
25
25
|
const bccInput = document.getElementById("compose-bcc");
|
|
26
26
|
const subjectInput = document.getElementById("compose-subject");
|
|
27
|
-
/**
|
|
27
|
+
/** Populate the From dropdown with accounts */
|
|
28
|
+
function populateFromSelect(accounts, selectedId) {
|
|
29
|
+
fromSelect.innerHTML = "";
|
|
30
|
+
for (const acct of accounts) {
|
|
31
|
+
const opt = document.createElement("option");
|
|
32
|
+
opt.value = acct.id;
|
|
33
|
+
const displayLabel = acct.label || acct.name;
|
|
34
|
+
opt.textContent = `${displayLabel}: ${acct.name} <${acct.email}>`;
|
|
35
|
+
opt.dataset.email = acct.email;
|
|
36
|
+
opt.dataset.name = acct.name;
|
|
37
|
+
if (acct.defaultSend)
|
|
38
|
+
opt.dataset.defaultSend = "true";
|
|
39
|
+
if (acct.id === selectedId)
|
|
40
|
+
opt.selected = true;
|
|
41
|
+
fromSelect.appendChild(opt);
|
|
42
|
+
}
|
|
43
|
+
// "Other..." option for custom address
|
|
44
|
+
const other = document.createElement("option");
|
|
45
|
+
other.value = "__custom__";
|
|
46
|
+
other.textContent = "Other...";
|
|
47
|
+
fromSelect.appendChild(other);
|
|
48
|
+
}
|
|
49
|
+
fromSelect.addEventListener("change", () => {
|
|
50
|
+
if (fromSelect.value === "__custom__") {
|
|
51
|
+
fromCustom.hidden = false;
|
|
52
|
+
fromCustom.focus();
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
fromCustom.hidden = true;
|
|
56
|
+
fromCustom.value = "";
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
/** Extract account ID from the From field */
|
|
28
60
|
function getFromAccountId() {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return opt.dataset.accountId;
|
|
61
|
+
if (fromSelect.value === "__custom__") {
|
|
62
|
+
// Custom address — use default send account, fallback to first
|
|
63
|
+
const defaultOpt = Array.from(fromSelect.options).find(o => o.dataset.defaultSend === "true");
|
|
64
|
+
return defaultOpt?.value || fromSelect.options[0]?.value || "";
|
|
34
65
|
}
|
|
35
|
-
|
|
36
|
-
const first = fromDatalist.options[0];
|
|
37
|
-
return first?.dataset.accountId || val;
|
|
66
|
+
return fromSelect.value;
|
|
38
67
|
}
|
|
39
68
|
/** Get the From address string for the message headers */
|
|
40
69
|
function getFromAddress() {
|
|
41
|
-
|
|
70
|
+
if (fromSelect.value === "__custom__")
|
|
71
|
+
return fromCustom.value;
|
|
72
|
+
const opt = fromSelect.selectedOptions[0];
|
|
73
|
+
return opt ? `${opt.dataset.name} <${opt.dataset.email}>` : "";
|
|
42
74
|
}
|
|
43
75
|
/** Smart tab — skip to next empty field, ending at body */
|
|
44
76
|
function smartTab(current) {
|
|
@@ -145,8 +177,7 @@ function setupAutocomplete(input) {
|
|
|
145
177
|
e.preventDefault();
|
|
146
178
|
const idx = activeIndex >= 0 ? activeIndex : 0;
|
|
147
179
|
items[idx].dispatchEvent(new MouseEvent("mousedown"));
|
|
148
|
-
//
|
|
149
|
-
setTimeout(() => smartTab(input), 50);
|
|
180
|
+
// Stay in field — user may want to add more addresses
|
|
150
181
|
return;
|
|
151
182
|
}
|
|
152
183
|
}
|
|
@@ -176,16 +207,7 @@ function parseAddrs(s) {
|
|
|
176
207
|
}
|
|
177
208
|
function applyInit(init) {
|
|
178
209
|
// Populate From dropdown
|
|
179
|
-
|
|
180
|
-
for (const acct of init.accounts) {
|
|
181
|
-
const opt = document.createElement("option");
|
|
182
|
-
opt.value = `${acct.name} <${acct.email}>`;
|
|
183
|
-
opt.dataset.accountId = acct.id;
|
|
184
|
-
fromDatalist.appendChild(opt);
|
|
185
|
-
}
|
|
186
|
-
const selectedAcct = init.accounts.find((a) => a.id === init.accountId) || init.accounts[0];
|
|
187
|
-
if (selectedAcct)
|
|
188
|
-
fromInput.value = `${selectedAcct.name} <${selectedAcct.email}>`;
|
|
210
|
+
populateFromSelect(init.accounts, init.accountId);
|
|
189
211
|
toInput.value = formatAddrs(init.to);
|
|
190
212
|
ccInput.value = formatAddrs(init.cc);
|
|
191
213
|
subjectInput.value = init.subject;
|
|
@@ -193,6 +215,10 @@ function applyInit(init) {
|
|
|
193
215
|
editor.clipboard.dangerouslyPasteHTML(init.bodyHtml);
|
|
194
216
|
editor.setSelection(0, 0);
|
|
195
217
|
}
|
|
218
|
+
// If resuming a draft, track its UID for deletion after send
|
|
219
|
+
if (init.draftUid) {
|
|
220
|
+
draftUid = init.draftUid;
|
|
221
|
+
}
|
|
196
222
|
document.title = init.subject ? `${init.subject} - Compose` : "Compose - mailx";
|
|
197
223
|
// Focus first empty field: To → Subject → body
|
|
198
224
|
if (!toInput.value)
|
|
@@ -213,19 +239,11 @@ else {
|
|
|
213
239
|
toInput.focus();
|
|
214
240
|
}
|
|
215
241
|
// If From dropdown is empty (new compose without init, or init had no accounts), fetch from API
|
|
216
|
-
if (
|
|
242
|
+
if (fromSelect.options.length === 0) {
|
|
217
243
|
fetch("/api/accounts")
|
|
218
244
|
.then(r => r.json())
|
|
219
245
|
.then((accounts) => {
|
|
220
|
-
|
|
221
|
-
const opt = document.createElement("option");
|
|
222
|
-
opt.value = `${acct.name} <${acct.email}>`;
|
|
223
|
-
opt.dataset.accountId = acct.id;
|
|
224
|
-
fromDatalist.appendChild(opt);
|
|
225
|
-
}
|
|
226
|
-
if (!fromInput.value && accounts.length > 0) {
|
|
227
|
-
fromInput.value = `${accounts[0].name} <${accounts[0].email}>`;
|
|
228
|
-
}
|
|
246
|
+
populateFromSelect(accounts);
|
|
229
247
|
})
|
|
230
248
|
.catch(e => console.error("Failed to load accounts:", e));
|
|
231
249
|
}
|
|
@@ -270,6 +288,7 @@ document.getElementById("btn-send")?.addEventListener("click", async () => {
|
|
|
270
288
|
btn.textContent = "Sending...";
|
|
271
289
|
const body = {
|
|
272
290
|
from: getFromAccountId(),
|
|
291
|
+
fromAddress: getFromAddress(),
|
|
273
292
|
to: parseAddrs(toInput.value),
|
|
274
293
|
cc: parseAddrs(ccInput.value),
|
|
275
294
|
bcc: parseAddrs(bccInput.value),
|