@bobfrankston/mailx 1.0.383 → 1.0.384
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/client/app.js +30 -0
- package/client/index.html +6 -0
- package/client/lib/api-client.js +6 -0
- package/client/lib/mailxapi.js +3 -0
- package/package.json +1 -1
- package/packages/mailx-service/index.d.ts +12 -0
- package/packages/mailx-service/index.js +50 -0
- package/packages/mailx-service/jsonrpc.js +2 -0
package/client/app.js
CHANGED
|
@@ -971,6 +971,36 @@ async function refreshSpamButtonVisibility() {
|
|
|
971
971
|
}
|
|
972
972
|
document.addEventListener("mailx-message-shown", refreshSpamButtonVisibility);
|
|
973
973
|
document.addEventListener("mailx-folder-changed", refreshSpamButtonVisibility);
|
|
974
|
+
// Q100 placeholder — append a row to ~/.mailx/spam.csv for later analysis.
|
|
975
|
+
// No folder move, no flag change, no auto-delete. Button is always visible
|
|
976
|
+
// (no configuration required; unlike btn-spam which needs a junk folder).
|
|
977
|
+
document.getElementById("btn-spam-report")?.addEventListener("click", async () => {
|
|
978
|
+
const current = getCurrentMessage();
|
|
979
|
+
const msg = current?.message;
|
|
980
|
+
const accountId = current?.accountId;
|
|
981
|
+
if (!msg || !accountId)
|
|
982
|
+
return;
|
|
983
|
+
const btn = document.getElementById("btn-spam-report");
|
|
984
|
+
const originalLabel = btn.textContent;
|
|
985
|
+
btn.disabled = true;
|
|
986
|
+
btn.textContent = "…";
|
|
987
|
+
try {
|
|
988
|
+
const { recordSpamReport } = await import("./lib/api-client.js");
|
|
989
|
+
await recordSpamReport(accountId, msg.uid, msg.folderId);
|
|
990
|
+
btn.textContent = "✓";
|
|
991
|
+
const status = document.getElementById("status-sync");
|
|
992
|
+
if (status)
|
|
993
|
+
status.textContent = "Logged to ~/.mailx/spam.csv";
|
|
994
|
+
setTimeout(() => { btn.textContent = originalLabel; btn.disabled = false; }, 1500);
|
|
995
|
+
}
|
|
996
|
+
catch (e) {
|
|
997
|
+
btn.textContent = "✗";
|
|
998
|
+
const status = document.getElementById("status-sync");
|
|
999
|
+
if (status)
|
|
1000
|
+
status.textContent = `Spam log failed: ${e?.message || e}`;
|
|
1001
|
+
setTimeout(() => { btn.textContent = originalLabel; btn.disabled = false; }, 2500);
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
974
1004
|
document.getElementById("btn-compose")?.addEventListener("click", () => openCompose("new"));
|
|
975
1005
|
document.getElementById("btn-mark-unread")?.addEventListener("click", () => {
|
|
976
1006
|
// Toggle \Seen on the currently-selected message. Mirrors the R
|
package/client/index.html
CHANGED
|
@@ -35,6 +35,11 @@
|
|
|
35
35
|
<div class="tb-menu" id="settings-menu">
|
|
36
36
|
<button class="tb-btn" id="btn-settings">Settings</button>
|
|
37
37
|
<div class="tb-menu-dropdown" id="settings-dropdown" hidden>
|
|
38
|
+
<span class="tb-menu-label">Theme</span>
|
|
39
|
+
<label class="tb-menu-item"><input type="radio" name="opt-theme" value="system" id="opt-theme-system"> System</label>
|
|
40
|
+
<label class="tb-menu-item"><input type="radio" name="opt-theme" value="light" id="opt-theme-light"> Light</label>
|
|
41
|
+
<label class="tb-menu-item"><input type="radio" name="opt-theme" value="dark" id="opt-theme-dark"> Dark</label>
|
|
42
|
+
<hr class="tb-menu-sep">
|
|
38
43
|
<span class="tb-menu-label">Editor</span>
|
|
39
44
|
<label class="tb-menu-item"><input type="radio" name="opt-editor" value="quill" id="opt-editor-quill" checked> Quill</label>
|
|
40
45
|
<label class="tb-menu-item"><input type="radio" name="opt-editor" value="tiptap" id="opt-editor-tiptap"> tiptap</label>
|
|
@@ -133,6 +138,7 @@
|
|
|
133
138
|
<button class="tb-btn" id="btn-forward" title="Forward">→</button>
|
|
134
139
|
<button class="tb-btn" id="btn-delete" title="Delete (Del)">🗑</button>
|
|
135
140
|
<button class="tb-btn" id="btn-spam" title="Mark as spam — move to configured spam folder" hidden>⚠</button>
|
|
141
|
+
<button class="tb-btn" id="btn-spam-report" title="Report as spam (append to ~/.mailx/spam.csv for later analysis)">🚫</button>
|
|
136
142
|
<button class="tb-btn" id="btn-flag" title="Flag">⚑</button>
|
|
137
143
|
<button class="tb-btn" id="btn-mark-unread" title="Mark unread (R)">◉</button>
|
|
138
144
|
<button class="tb-btn" id="mv-view-thread" title="View thread (conversation)" hidden>💬</button>
|
package/client/lib/api-client.js
CHANGED
|
@@ -170,6 +170,12 @@ export function deleteTask(uuid) {
|
|
|
170
170
|
export function drainStoreSync() {
|
|
171
171
|
return ipc().drainStoreSync?.();
|
|
172
172
|
}
|
|
173
|
+
/** Report the currently-viewed message as spam → appends a row to
|
|
174
|
+
* `~/.mailx/spam.csv`. Placeholder: no folder move, no flag change, no
|
|
175
|
+
* auto-delete. Training data for a smarter pass later. */
|
|
176
|
+
export function recordSpamReport(accountId, uid, folderId) {
|
|
177
|
+
return ipc().recordSpamReport?.(accountId, uid, folderId);
|
|
178
|
+
}
|
|
173
179
|
export function getOutboxStatus() {
|
|
174
180
|
return ipc().getOutboxStatus();
|
|
175
181
|
}
|
package/client/lib/mailxapi.js
CHANGED
|
@@ -122,6 +122,9 @@
|
|
|
122
122
|
updateTask: function(uuid, patch) { return callNode("updateTask", { uuid: uuid, patch: patch }); },
|
|
123
123
|
deleteTask: function(uuid) { return callNode("deleteTask", { uuid: uuid }); },
|
|
124
124
|
drainStoreSync: function() { return callNode("drainStoreSync"); },
|
|
125
|
+
recordSpamReport: function(accountId, uid, folderId) {
|
|
126
|
+
return callNode("recordSpamReport", { accountId: accountId, uid: uid, folderId: folderId });
|
|
127
|
+
},
|
|
125
128
|
readJsoncFile: function(name) {
|
|
126
129
|
return callNode("readJsoncFile", { name: name });
|
|
127
130
|
},
|
package/package.json
CHANGED
|
@@ -109,6 +109,18 @@ export declare class MailxService {
|
|
|
109
109
|
targetFolderId: number;
|
|
110
110
|
moved: number;
|
|
111
111
|
}>;
|
|
112
|
+
/** Append a spam report row to `~/.mailx/spam.csv` — placeholder mechanism
|
|
113
|
+
* per user 2026-04-23 ("let's make it smart later; no auto-delete until
|
|
114
|
+
* safety issues are addressed"). One row per click. Columns: timestamp
|
|
115
|
+
* (ms since epoch), ISO date, ISO time, accountId, Delivered-To, From
|
|
116
|
+
* address, Subject, eml file path. CSV fields RFC 4180-quoted so commas
|
|
117
|
+
* and quotes in subjects survive. No move, no flag change, no server
|
|
118
|
+
* hit — just the log. Useful as training data for a future classifier.
|
|
119
|
+
*/
|
|
120
|
+
recordSpamReport(accountId: string, uid: number, folderId: number): Promise<{
|
|
121
|
+
ok: true;
|
|
122
|
+
row: string;
|
|
123
|
+
}>;
|
|
112
124
|
undeleteMessage(accountId: string, uid: number, folderId: number): Promise<void>;
|
|
113
125
|
deleteOnServer(accountId: string, folderPath: string, uid: number): Promise<void>;
|
|
114
126
|
createFolder(accountId: string, parentPath: string, name: string): Promise<void>;
|
|
@@ -943,6 +943,56 @@ export class MailxService {
|
|
|
943
943
|
await this.moveMessages(accountId, uids, target.id);
|
|
944
944
|
return { targetFolderId: target.id, moved: uids.length };
|
|
945
945
|
}
|
|
946
|
+
/** Append a spam report row to `~/.mailx/spam.csv` — placeholder mechanism
|
|
947
|
+
* per user 2026-04-23 ("let's make it smart later; no auto-delete until
|
|
948
|
+
* safety issues are addressed"). One row per click. Columns: timestamp
|
|
949
|
+
* (ms since epoch), ISO date, ISO time, accountId, Delivered-To, From
|
|
950
|
+
* address, Subject, eml file path. CSV fields RFC 4180-quoted so commas
|
|
951
|
+
* and quotes in subjects survive. No move, no flag change, no server
|
|
952
|
+
* hit — just the log. Useful as training data for a future classifier.
|
|
953
|
+
*/
|
|
954
|
+
async recordSpamReport(accountId, uid, folderId) {
|
|
955
|
+
const env = this.db.getMessageByUid(accountId, uid, folderId);
|
|
956
|
+
if (!env)
|
|
957
|
+
throw new Error(`Message not found: ${accountId}/${uid}`);
|
|
958
|
+
const bodyPath = env.bodyPath || "";
|
|
959
|
+
// Prefer `body_path` (authoritative). `Delivered-To` isn't in the
|
|
960
|
+
// envelope struct, so parse from the cached `.eml` if available.
|
|
961
|
+
let deliveredTo = "";
|
|
962
|
+
if (bodyPath) {
|
|
963
|
+
try {
|
|
964
|
+
const raw = fs.readFileSync(bodyPath, "utf-8").slice(0, 4096);
|
|
965
|
+
const m = raw.match(/^Delivered-To:\s*(.+)$/mi);
|
|
966
|
+
if (m)
|
|
967
|
+
deliveredTo = m[1].trim();
|
|
968
|
+
}
|
|
969
|
+
catch { /* not fatal — leave blank */ }
|
|
970
|
+
}
|
|
971
|
+
const now = new Date();
|
|
972
|
+
const isoDate = now.toISOString().slice(0, 10);
|
|
973
|
+
const isoTime = now.toISOString().slice(11, 19);
|
|
974
|
+
const fromAddr = env.from?.address || "";
|
|
975
|
+
const subject = env.subject || "";
|
|
976
|
+
const csvEscape = (s) => `"${String(s).replace(/"/g, '""')}"`;
|
|
977
|
+
const row = [
|
|
978
|
+
String(now.getTime()),
|
|
979
|
+
csvEscape(isoDate),
|
|
980
|
+
csvEscape(isoTime),
|
|
981
|
+
csvEscape(accountId),
|
|
982
|
+
csvEscape(deliveredTo),
|
|
983
|
+
csvEscape(fromAddr),
|
|
984
|
+
csvEscape(subject),
|
|
985
|
+
csvEscape(bodyPath),
|
|
986
|
+
].join(",") + "\n";
|
|
987
|
+
const spamCsvPath = path.join(getConfigDir(), "spam.csv");
|
|
988
|
+
// Write a header if the file doesn't exist yet so the CSV is self-describing.
|
|
989
|
+
if (!fs.existsSync(spamCsvPath)) {
|
|
990
|
+
fs.writeFileSync(spamCsvPath, "timestamp_ms,date,time,account,delivered_to,from,subject,eml_path\n", "utf-8");
|
|
991
|
+
}
|
|
992
|
+
fs.appendFileSync(spamCsvPath, row, "utf-8");
|
|
993
|
+
console.log(` [spam] reported ${accountId}/${uid} → spam.csv`);
|
|
994
|
+
return { ok: true, row };
|
|
995
|
+
}
|
|
946
996
|
async undeleteMessage(accountId, uid, folderId) {
|
|
947
997
|
// Clear the tombstone first so a subsequent sync can re-import if
|
|
948
998
|
// the server still has the row. Messages with no Message-ID just
|
|
@@ -123,6 +123,8 @@ async function dispatchAction(svc, action, p) {
|
|
|
123
123
|
case "drainStoreSync":
|
|
124
124
|
await svc.drainStoreSync();
|
|
125
125
|
return { ok: true };
|
|
126
|
+
case "recordSpamReport":
|
|
127
|
+
return await svc.recordSpamReport(p.accountId, p.uid, p.folderId);
|
|
126
128
|
case "getOutboxStatus":
|
|
127
129
|
return svc.getOutboxStatus();
|
|
128
130
|
case "listQueuedOutgoing":
|