@bobfrankston/mailx 1.0.181 → 1.0.183
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":
|
|
1
|
+
{"height":1344,"width":1438,"x":216,"y":107}
|
package/client/app.js
CHANGED
|
@@ -265,12 +265,31 @@ async function openCompose(mode) {
|
|
|
265
265
|
references: [],
|
|
266
266
|
accounts: accounts.map((a) => ({ id: a.id, name: a.name, email: a.email })),
|
|
267
267
|
};
|
|
268
|
+
// Auto-detect reply From: if the message was delivered to an identity domain,
|
|
269
|
+
// reply from that address instead of the default account address.
|
|
270
|
+
// Identity domains configured per-account in accounts.jsonc (identityDomains array).
|
|
271
|
+
// Default identity domains for bob.ma account:
|
|
272
|
+
const identityDomains = ["bob.ma", "frankston.com"];
|
|
273
|
+
function detectReplyFrom() {
|
|
274
|
+
if (!msg)
|
|
275
|
+
return undefined;
|
|
276
|
+
// Check deliveredTo first (most reliable), then To addresses
|
|
277
|
+
const candidates = [msg.deliveredTo, ...(msg.to || []).map((a) => a.address)].filter(Boolean);
|
|
278
|
+
for (const addr of candidates) {
|
|
279
|
+
const domain = addr.split("@")[1]?.toLowerCase();
|
|
280
|
+
if (domain && identityDomains.some(d => domain === d || domain.endsWith(`.${d}`))) {
|
|
281
|
+
return addr;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
268
286
|
if (msg && mode === "reply") {
|
|
269
287
|
init.to = [msg.from];
|
|
270
288
|
init.subject = `Re: ${cleanSubject}`;
|
|
271
289
|
init.bodyHtml = quoteBody(msg);
|
|
272
290
|
init.inReplyTo = msg.messageId;
|
|
273
291
|
init.references = [...(msg.references || []), msg.messageId];
|
|
292
|
+
init.fromAddress = detectReplyFrom();
|
|
274
293
|
}
|
|
275
294
|
else if (msg && mode === "replyAll") {
|
|
276
295
|
init.to = [msg.from, ...msg.to.filter((a) => a.address !== msg.from.address)];
|
|
@@ -279,6 +298,7 @@ async function openCompose(mode) {
|
|
|
279
298
|
init.bodyHtml = quoteBody(msg);
|
|
280
299
|
init.inReplyTo = msg.messageId;
|
|
281
300
|
init.references = [...(msg.references || []), msg.messageId];
|
|
301
|
+
init.fromAddress = detectReplyFrom();
|
|
282
302
|
}
|
|
283
303
|
else if (msg && mode === "forward") {
|
|
284
304
|
init.subject = `Fwd: ${cleanSubject}`;
|
|
@@ -260,6 +260,13 @@ function parseAddrs(s) {
|
|
|
260
260
|
function applyInit(init) {
|
|
261
261
|
// Populate From dropdown
|
|
262
262
|
populateFromSelect(init.accounts, init.accountId);
|
|
263
|
+
// Auto-detect reply From: if fromAddress is set (identity domain match),
|
|
264
|
+
// use it as the From address via the "Other..." custom field
|
|
265
|
+
if (init.fromAddress) {
|
|
266
|
+
fromSelect.value = "__custom__";
|
|
267
|
+
fromCustom.hidden = false;
|
|
268
|
+
fromCustom.value = init.fromAddress;
|
|
269
|
+
}
|
|
263
270
|
toInput.value = formatAddrs(init.to);
|
|
264
271
|
ccInput.value = formatAddrs(init.cc);
|
|
265
272
|
subjectInput.value = init.subject;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.183",
|
|
4
4
|
"description": "Local-first email client with IMAP sync and standalone native app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/mailx.js",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@bobfrankston/iflow-node": "^0.1.2",
|
|
25
25
|
"@bobfrankston/miscinfo": "^1.0.8",
|
|
26
26
|
"@bobfrankston/oauthsupport": "^1.0.21",
|
|
27
|
-
"@bobfrankston/msger": "^0.1.
|
|
27
|
+
"@bobfrankston/msger": "^0.1.233",
|
|
28
28
|
"@capacitor/android": "^8.3.0",
|
|
29
29
|
"@capacitor/cli": "^8.3.0",
|
|
30
30
|
"@capacitor/core": "^8.3.0",
|
|
@@ -178,7 +178,7 @@ export declare class ImapManager extends EventEmitter {
|
|
|
178
178
|
private readonly hostname;
|
|
179
179
|
/** Ensure Outbox folder exists, create if needed */
|
|
180
180
|
private ensureOutbox;
|
|
181
|
-
/** Save a
|
|
181
|
+
/** Save a copy of outgoing mail — label is a subdirectory (editing/queued/sent) */
|
|
182
182
|
private saveSendingCopy;
|
|
183
183
|
/** Queue a message for sending. Tries IMAP Outbox, falls back to local file. */
|
|
184
184
|
queueOutgoing(accountId: string, rawMessage: string | Buffer): Promise<void>;
|
|
@@ -1629,20 +1629,19 @@ export class ImapManager extends EventEmitter {
|
|
|
1629
1629
|
outbox = this.findFolder(accountId, "outbox");
|
|
1630
1630
|
return outbox?.path || "Outbox";
|
|
1631
1631
|
}
|
|
1632
|
-
/** Save a
|
|
1632
|
+
/** Save a copy of outgoing mail — label is a subdirectory (editing/queued/sent) */
|
|
1633
1633
|
saveSendingCopy(accountId, rawMessage, label) {
|
|
1634
1634
|
try {
|
|
1635
|
-
const
|
|
1636
|
-
fs.mkdirSync(
|
|
1635
|
+
const dir = path.join(getConfigDir(), "sending", accountId, label);
|
|
1636
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1637
1637
|
const now = new Date();
|
|
1638
1638
|
const pad2 = (n) => String(n).padStart(2, "0");
|
|
1639
1639
|
const ts = `${now.getFullYear()}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}_${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`;
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
console.log(` [sending] Saved debug copy: ${filename}`);
|
|
1640
|
+
fs.writeFileSync(path.join(dir, `${ts}.eml`), rawMessage);
|
|
1641
|
+
console.log(` [sending] ${label}/${ts}.eml`);
|
|
1643
1642
|
}
|
|
1644
1643
|
catch (e) {
|
|
1645
|
-
console.error(` [sending] Failed to save
|
|
1644
|
+
console.error(` [sending] Failed to save copy: ${e.message}`);
|
|
1646
1645
|
}
|
|
1647
1646
|
}
|
|
1648
1647
|
/** Queue a message for sending. Tries IMAP Outbox, falls back to local file. */
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* Both the Express API (mailx-api) and the Android bridge call these functions.
|
|
5
5
|
*/
|
|
6
6
|
import * as dns from "node:dns/promises";
|
|
7
|
-
import
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import { loadSettings, saveSettings, loadAccounts, loadAccountsAsync, saveAccounts, initCloudConfig, loadAllowlist, saveAllowlist, loadAutocomplete, saveAutocomplete, getStorePath, getStorageInfo, getConfigDir } from "@bobfrankston/mailx-settings";
|
|
8
10
|
import { simpleParser } from "mailparser";
|
|
9
11
|
// ── Quoted-printable encoding (readable in debug .eml files) ──
|
|
10
12
|
function encodeQuotedPrintable(text) {
|
|
@@ -106,7 +108,7 @@ export class MailxService {
|
|
|
106
108
|
for (const cfg of settings.accounts) {
|
|
107
109
|
const a = dbAccounts.find(d => d.id === cfg.id);
|
|
108
110
|
if (a)
|
|
109
|
-
ordered.push({ ...a, label: cfg.label, defaultSend: cfg.defaultSend || false });
|
|
111
|
+
ordered.push({ ...a, label: cfg.label, defaultSend: cfg.defaultSend || false, identityDomains: cfg.identityDomains || [] });
|
|
110
112
|
}
|
|
111
113
|
// Append any DB accounts not in settings
|
|
112
114
|
for (const a of dbAccounts) {
|
|
@@ -339,7 +341,9 @@ export class MailxService {
|
|
|
339
341
|
const account = settings.accounts.find(a => a.id === msg.from);
|
|
340
342
|
if (!account)
|
|
341
343
|
throw new Error(`Unknown account: ${msg.from}`);
|
|
342
|
-
|
|
344
|
+
// Use custom From address if set (identity domain reply), but always wrap with account display name
|
|
345
|
+
const fromAddr = msg.fromAddress || account.email;
|
|
346
|
+
const fromHeader = `${account.name} <${fromAddr}>`;
|
|
343
347
|
const to = msg.to.map((a) => a.name ? `${a.name} <${a.address}>` : a.address).join(", ");
|
|
344
348
|
const cc = msg.cc?.map((a) => a.name ? `${a.name} <${a.address}>` : a.address).join(", ");
|
|
345
349
|
const bcc = msg.bcc?.map((a) => a.name ? `${a.name} <${a.address}>` : a.address).join(", ");
|
|
@@ -536,6 +540,21 @@ export class MailxService {
|
|
|
536
540
|
`MIME-Version: 1.0`, `Content-Type: text/html; charset=UTF-8`, `Content-Transfer-Encoding: quoted-printable`,
|
|
537
541
|
].filter(h => h !== null).join("\r\n");
|
|
538
542
|
const raw = `${headers}\r\n\r\n${bodyEncoded}`;
|
|
543
|
+
// Save local editing copy — crash recovery, keep last 3
|
|
544
|
+
try {
|
|
545
|
+
const editingDir = path.join(getConfigDir(), "sending", accountId, "editing");
|
|
546
|
+
fs.mkdirSync(editingDir, { recursive: true });
|
|
547
|
+
const pad2 = (n) => String(n).padStart(2, "0");
|
|
548
|
+
const now = new Date();
|
|
549
|
+
const ts = `${now.getFullYear()}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}_${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`;
|
|
550
|
+
fs.writeFileSync(path.join(editingDir, `${ts}.eml`), raw);
|
|
551
|
+
// Keep only last 3
|
|
552
|
+
const files = fs.readdirSync(editingDir).filter(f => f.endsWith(".eml")).sort();
|
|
553
|
+
while (files.length > 3) {
|
|
554
|
+
fs.unlinkSync(path.join(editingDir, files.shift()));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
catch { /* ignore */ }
|
|
539
558
|
const uid = await this.imapManager.saveDraft(accountId, raw, previousDraftUid, id);
|
|
540
559
|
return { uid, draftId: id };
|
|
541
560
|
}
|