@bobfrankston/mailx 1.0.184 → 1.0.185
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 +12 -7
- package/client/compose/compose.js +18 -8
- package/package.json +2 -2
- package/packages/mailx-imap/index.js +19 -53
package/client/app.js
CHANGED
|
@@ -268,16 +268,15 @@ async function openCompose(mode) {
|
|
|
268
268
|
// Auto-detect reply From: if the message was delivered to an identity domain,
|
|
269
269
|
// reply from that address instead of the default account address.
|
|
270
270
|
// Identity domains configured per-account in accounts.jsonc (identityDomains array).
|
|
271
|
-
|
|
272
|
-
const identityDomains = [
|
|
271
|
+
const account = accounts.find((a) => a.id === accountId);
|
|
272
|
+
const identityDomains = account?.identityDomains || [];
|
|
273
273
|
function detectReplyFrom() {
|
|
274
|
-
if (!msg)
|
|
274
|
+
if (!msg || identityDomains.length === 0)
|
|
275
275
|
return undefined;
|
|
276
|
-
// Check deliveredTo first (most reliable), then To addresses
|
|
277
276
|
const candidates = [msg.deliveredTo, ...(msg.to || []).map((a) => a.address)].filter(Boolean);
|
|
278
277
|
for (const addr of candidates) {
|
|
279
278
|
const domain = addr.split("@")[1]?.toLowerCase();
|
|
280
|
-
if (domain && identityDomains.some(d => domain === d
|
|
279
|
+
if (domain && identityDomains.some((d) => domain === d.toLowerCase())) {
|
|
281
280
|
return addr;
|
|
282
281
|
}
|
|
283
282
|
}
|
|
@@ -585,10 +584,16 @@ onWsEvent((event) => {
|
|
|
585
584
|
break;
|
|
586
585
|
}
|
|
587
586
|
case "folderCountsChanged": {
|
|
588
|
-
// Update folder badges
|
|
589
|
-
// The list refreshes when the user clicks a folder or presses Sync.
|
|
587
|
+
// Update folder badges + silently refresh message list (preserves selection and viewer)
|
|
590
588
|
updateFolderCounts();
|
|
591
589
|
updateNewMessageCount();
|
|
590
|
+
// Debounced silent reload — preserves scroll position, selection, and viewer
|
|
591
|
+
if (reloadDebounceTimer)
|
|
592
|
+
clearTimeout(reloadDebounceTimer);
|
|
593
|
+
reloadDebounceTimer = setTimeout(() => {
|
|
594
|
+
reloadDebounceTimer = null;
|
|
595
|
+
reloadCurrentFolder();
|
|
596
|
+
}, 2000);
|
|
592
597
|
// Sync finished — re-enable sync button
|
|
593
598
|
const syncBtn = document.getElementById("btn-sync");
|
|
594
599
|
if (syncBtn) {
|
|
@@ -258,14 +258,24 @@ function parseAddrs(s) {
|
|
|
258
258
|
});
|
|
259
259
|
}
|
|
260
260
|
function applyInit(init) {
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
261
|
+
// If identity domain matched, add as first option in dropdown
|
|
262
|
+
const replyAddr = init.fromAddress;
|
|
263
|
+
const account = init.accounts.find(a => a.id === init.accountId);
|
|
264
|
+
const displayName = account?.name || "";
|
|
265
|
+
if (replyAddr) {
|
|
266
|
+
// Insert identity address as first option, selected
|
|
267
|
+
const idOpt = document.createElement("option");
|
|
268
|
+
idOpt.value = init.accountId;
|
|
269
|
+
idOpt.textContent = `${displayName} <${replyAddr}>`;
|
|
270
|
+
idOpt.dataset.email = replyAddr;
|
|
271
|
+
idOpt.dataset.name = displayName;
|
|
272
|
+
idOpt.selected = true;
|
|
273
|
+
// Populate rest of dropdown, then prepend identity option
|
|
274
|
+
populateFromSelect(init.accounts, "");
|
|
275
|
+
fromSelect.prepend(idOpt);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
populateFromSelect(init.accounts, init.accountId);
|
|
269
279
|
}
|
|
270
280
|
toInput.value = formatAddrs(init.to);
|
|
271
281
|
ccInput.value = formatAddrs(init.cc);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.185",
|
|
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.236",
|
|
28
28
|
"@capacitor/android": "^8.3.0",
|
|
29
29
|
"@capacitor/cli": "^8.3.0",
|
|
30
30
|
"@capacitor/core": "^8.3.0",
|
|
@@ -12,8 +12,10 @@ import * as fs from "node:fs";
|
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { simpleParser } from "mailparser";
|
|
14
14
|
import { GmailApiProvider } from "./providers/gmail-api.js";
|
|
15
|
-
import { createTransport } from "nodemailer";
|
|
16
15
|
import * as os from "node:os";
|
|
16
|
+
// Well-known ports — no magic numbers
|
|
17
|
+
const SMTP_PORT_STARTTLS = 587;
|
|
18
|
+
const SMTP_PORT_IMPLICIT_TLS = 465;
|
|
17
19
|
/** Extract full error detail with provenance */
|
|
18
20
|
function imapError(err) {
|
|
19
21
|
const msg = err.message || err.reason || err.code || (typeof err === "string" ? err : "");
|
|
@@ -1734,21 +1736,27 @@ export class ImapManager extends EventEmitter {
|
|
|
1734
1736
|
const account = settings.accounts.find(a => a.id === accountId);
|
|
1735
1737
|
if (!account?.smtp)
|
|
1736
1738
|
throw new Error(`No SMTP config for ${accountId}`);
|
|
1739
|
+
// SMTP auth: use explicit SMTP credentials, fall back to IMAP credentials
|
|
1737
1740
|
let smtpAuth;
|
|
1738
|
-
|
|
1739
|
-
|
|
1741
|
+
const smtpAuthType = account.smtp.auth || (account.imap?.password ? "password" : undefined);
|
|
1742
|
+
if (smtpAuthType === "password") {
|
|
1743
|
+
smtpAuth = {
|
|
1744
|
+
user: account.smtp.user || account.imap?.user || account.email,
|
|
1745
|
+
pass: account.smtp.password || account.imap?.password,
|
|
1746
|
+
};
|
|
1740
1747
|
}
|
|
1741
|
-
else if (
|
|
1748
|
+
else if (smtpAuthType === "oauth2") {
|
|
1742
1749
|
const accessToken = await this.getOAuthToken(accountId);
|
|
1743
1750
|
if (!accessToken)
|
|
1744
1751
|
throw new Error("OAuth token not available");
|
|
1745
|
-
smtpAuth = { type: "OAuth2", user: account.smtp.user, accessToken };
|
|
1752
|
+
smtpAuth = { type: "OAuth2", user: account.smtp.user || account.imap?.user || account.email, accessToken };
|
|
1746
1753
|
}
|
|
1747
1754
|
const { createTransport } = await import("nodemailer");
|
|
1755
|
+
const smtpPort = account.smtp.port || SMTP_PORT_STARTTLS;
|
|
1748
1756
|
const transport = createTransport({
|
|
1749
|
-
host: account.smtp.host,
|
|
1750
|
-
port:
|
|
1751
|
-
secure:
|
|
1757
|
+
host: account.smtp.host || account.imap?.host,
|
|
1758
|
+
port: smtpPort,
|
|
1759
|
+
secure: smtpPort === SMTP_PORT_IMPLICIT_TLS, // 465 = implicit TLS, 587 = STARTTLS
|
|
1752
1760
|
auth: smtpAuth,
|
|
1753
1761
|
tls: { rejectUnauthorized: false },
|
|
1754
1762
|
});
|
|
@@ -1824,52 +1832,10 @@ export class ImapManager extends EventEmitter {
|
|
|
1824
1832
|
await client.removeFlags(outboxFolder.path, uid, [sendingFlag]);
|
|
1825
1833
|
continue;
|
|
1826
1834
|
}
|
|
1827
|
-
// Send via SMTP
|
|
1835
|
+
// Send via shared SMTP method
|
|
1828
1836
|
try {
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
smtpAuth = { user: account.smtp.user, pass: account.smtp.password };
|
|
1832
|
-
}
|
|
1833
|
-
else if (account.smtp.auth === "oauth2") {
|
|
1834
|
-
const accessToken = await this.getOAuthToken(accountId);
|
|
1835
|
-
if (!accessToken)
|
|
1836
|
-
throw new Error("OAuth token not available — re-authenticate");
|
|
1837
|
-
smtpAuth = { type: "OAuth2", user: account.smtp.user, accessToken };
|
|
1838
|
-
}
|
|
1839
|
-
const transport = createTransport({
|
|
1840
|
-
host: account.smtp.host,
|
|
1841
|
-
port: account.smtp.port,
|
|
1842
|
-
secure: account.smtp.port === 465,
|
|
1843
|
-
auth: smtpAuth,
|
|
1844
|
-
tls: { rejectUnauthorized: false },
|
|
1845
|
-
});
|
|
1846
|
-
// Parse recipients from raw message headers for SMTP envelope
|
|
1847
|
-
const toMatch = msg.source.match(/^To:\s*(.+)$/mi);
|
|
1848
|
-
const ccMatch = msg.source.match(/^Cc:\s*(.+)$/mi);
|
|
1849
|
-
const bccMatch = msg.source.match(/^Bcc:\s*(.+)$/mi);
|
|
1850
|
-
const fromMatch = msg.source.match(/^From:\s*(.+)$/mi);
|
|
1851
|
-
const parseAddrs = (s) => s.match(/[\w.+-]+@[\w.-]+/g) || [];
|
|
1852
|
-
const recipients = [
|
|
1853
|
-
...(toMatch ? parseAddrs(toMatch[1]) : []),
|
|
1854
|
-
...(ccMatch ? parseAddrs(ccMatch[1]) : []),
|
|
1855
|
-
...(bccMatch ? parseAddrs(bccMatch[1]) : []),
|
|
1856
|
-
];
|
|
1857
|
-
const sender = fromMatch ? (parseAddrs(fromMatch[1])[0] || account.email) : account.email;
|
|
1858
|
-
if (recipients.length === 0) {
|
|
1859
|
-
console.error(` [outbox] No recipients in UID ${uid} — permanent failure`);
|
|
1860
|
-
await client.removeFlags(outboxFolder.path, uid, [sendingFlag]);
|
|
1861
|
-
await client.addFlags(outboxFolder.path, uid, ["$PermanentFailure"]);
|
|
1862
|
-
continue;
|
|
1863
|
-
}
|
|
1864
|
-
// Strip Bcc header from raw message before sending
|
|
1865
|
-
const rawToSend = msg.source.replace(/^Bcc:.*\r?\n/mi, "");
|
|
1866
|
-
// Save debug copy before sending
|
|
1867
|
-
this.saveSendingCopy(accountId, rawToSend, `sent-${uid}`);
|
|
1868
|
-
await transport.sendMail({
|
|
1869
|
-
raw: rawToSend,
|
|
1870
|
-
envelope: { from: sender, to: recipients },
|
|
1871
|
-
});
|
|
1872
|
-
console.log(` [outbox] Sent UID ${uid} → ${recipients.join(", ")}`);
|
|
1837
|
+
await this.sendRawViaSMTP(accountId, msg.source);
|
|
1838
|
+
console.log(` [outbox] Sent UID ${uid}`);
|
|
1873
1839
|
// Delete from Outbox FIRST to prevent double-send if move-to-Sent fails.
|
|
1874
1840
|
// The message is already sent via SMTP — worst case we lose the Sent copy,
|
|
1875
1841
|
// which is better than sending the message twice.
|