@bobfrankston/mailx 1.0.219 → 1.0.220
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"height":1344,"width":2151,"x":
|
|
1
|
+
{"height":1344,"width":2151,"x":1060,"y":329}
|
package/client/app.js
CHANGED
|
@@ -192,6 +192,24 @@ document.getElementById("folder-tree")?.addEventListener("click", (e) => {
|
|
|
192
192
|
document.querySelector(".folder-panel")?.classList.remove("open");
|
|
193
193
|
}
|
|
194
194
|
});
|
|
195
|
+
// Close folder overlay when user clicks outside it (narrow mode OR
|
|
196
|
+
// medium-width mode where the folder panel slides in as an overlay).
|
|
197
|
+
// Uses capture phase so it beats any child handler that might stopPropagation.
|
|
198
|
+
document.addEventListener("pointerdown", (e) => {
|
|
199
|
+
const panel = document.querySelector(".folder-panel");
|
|
200
|
+
if (!panel || !panel.classList.contains("open"))
|
|
201
|
+
return;
|
|
202
|
+
const target = e.target;
|
|
203
|
+
// Ignore clicks inside the panel itself and on the hamburger toggle
|
|
204
|
+
if (target.closest(".folder-panel") || target.closest("#btn-menu"))
|
|
205
|
+
return;
|
|
206
|
+
// Only auto-dismiss when we're in overlay mode (small or medium screens).
|
|
207
|
+
// On wide screens the panel is a permanent column and the "open" class
|
|
208
|
+
// is irrelevant.
|
|
209
|
+
if (window.innerWidth <= 1100 || window.innerHeight <= 600) {
|
|
210
|
+
panel.classList.remove("open");
|
|
211
|
+
}
|
|
212
|
+
}, true);
|
|
195
213
|
// ── Toolbar actions ──
|
|
196
214
|
document.getElementById("btn-sync")?.addEventListener("click", async () => {
|
|
197
215
|
const btn = document.getElementById("btn-sync");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.220",
|
|
4
4
|
"description": "Local-first email client with IMAP sync and standalone native app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/mailx.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"postinstall": "node bin/postinstall.js"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@bobfrankston/iflow-direct": "^0.1.
|
|
23
|
+
"@bobfrankston/iflow-direct": "^0.1.9",
|
|
24
24
|
"@bobfrankston/iflow-node": "^0.1.2",
|
|
25
25
|
"@bobfrankston/miscinfo": "^1.0.8",
|
|
26
26
|
"@bobfrankston/oauthsupport": "^1.0.22",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
},
|
|
75
75
|
".transformedSnapshot": {
|
|
76
76
|
"dependencies": {
|
|
77
|
-
"@bobfrankston/iflow-direct": "^0.1.
|
|
77
|
+
"@bobfrankston/iflow-direct": "^0.1.9",
|
|
78
78
|
"@bobfrankston/iflow-node": "^0.1.2",
|
|
79
79
|
"@bobfrankston/miscinfo": "^1.0.8",
|
|
80
80
|
"@bobfrankston/oauthsupport": "^1.0.22",
|
|
@@ -341,11 +341,34 @@ export class MailxService {
|
|
|
341
341
|
const account = settings.accounts.find(a => a.id === msg.from);
|
|
342
342
|
if (!account)
|
|
343
343
|
throw new Error(`Unknown account: ${msg.from}`);
|
|
344
|
+
// Vet every recipient address — refuse to send if any field contains a
|
|
345
|
+
// non-email (e.g. "Bob Frankston <Bob Frankston>" from a bad contact
|
|
346
|
+
// autocomplete). This catches garbage BEFORE it hits SMTP, where the
|
|
347
|
+
// server would either accept-and-bounce or reject the whole envelope.
|
|
348
|
+
const emailRe = /^[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+$/;
|
|
349
|
+
const validateList = (label, list) => {
|
|
350
|
+
if (!list)
|
|
351
|
+
return;
|
|
352
|
+
for (const a of list) {
|
|
353
|
+
const addr = (a?.address || "").trim();
|
|
354
|
+
if (!addr)
|
|
355
|
+
throw new Error(`${label} has an empty address`);
|
|
356
|
+
if (!emailRe.test(addr))
|
|
357
|
+
throw new Error(`${label} has an invalid address: "${addr}"${a?.name ? ` (displayed as "${a.name}")` : ""}`);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
validateList("To", msg.to);
|
|
361
|
+
validateList("Cc", msg.cc);
|
|
362
|
+
validateList("Bcc", msg.bcc);
|
|
363
|
+
if (!msg.to?.length)
|
|
364
|
+
throw new Error("No To recipients");
|
|
344
365
|
// Extract bare email from fromAddress (may be "Name <addr>" or just "addr")
|
|
345
366
|
let fromAddr = msg.fromAddress || account.email;
|
|
346
367
|
const angleMatch = fromAddr.match(/<([^>]+)>/);
|
|
347
368
|
if (angleMatch)
|
|
348
369
|
fromAddr = angleMatch[1];
|
|
370
|
+
if (!emailRe.test(fromAddr))
|
|
371
|
+
throw new Error(`From address is not a valid email: "${fromAddr}"`);
|
|
349
372
|
const fromHeader = `${account.name} <${fromAddr}>`;
|
|
350
373
|
const to = msg.to.map((a) => a.name ? `${a.name} <${a.address}>` : a.address).join(", ");
|
|
351
374
|
const cc = msg.cc?.map((a) => a.name ? `${a.name} <${a.address}>` : a.address).join(", ");
|
|
@@ -369,6 +369,11 @@ export class MailxDB {
|
|
|
369
369
|
// ── Contacts ──
|
|
370
370
|
/** Record an address used in sent mail */
|
|
371
371
|
recordSentAddress(name, email) {
|
|
372
|
+
// Don't pollute the contacts table with non-addresses. Anything without
|
|
373
|
+
// an @ or without a TLD-ish tail would show up in autocomplete and end
|
|
374
|
+
// up back in To/Cc headers as "Name <not an email>".
|
|
375
|
+
if (!email || !/^[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+$/.test(email))
|
|
376
|
+
return;
|
|
372
377
|
const now = Date.now();
|
|
373
378
|
const existing = this.db.prepare("SELECT id FROM contacts WHERE email = ?").get(email);
|
|
374
379
|
if (existing) {
|
|
@@ -387,6 +392,9 @@ export class MailxDB {
|
|
|
387
392
|
GROUP BY from_address`).all();
|
|
388
393
|
let added = 0;
|
|
389
394
|
for (const r of rows) {
|
|
395
|
+
// Skip invalid addresses so contact autocomplete never proposes non-emails
|
|
396
|
+
if (!r.from_address || !/^[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+$/.test(r.from_address))
|
|
397
|
+
continue;
|
|
390
398
|
const existing = this.db.prepare("SELECT id FROM contacts WHERE email = ?").get(r.from_address);
|
|
391
399
|
if (!existing) {
|
|
392
400
|
this.db.prepare("INSERT INTO contacts (source, name, email, last_used, use_count, updated_at) VALUES ('received', ?, ?, ?, ?, ?)").run(r.from_name || "", r.from_address, r.last, r.cnt, now);
|