@bobfrankston/mailx 1.0.214 → 1.0.216
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":243,"y":28}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.216",
|
|
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,11 +20,11 @@
|
|
|
20
20
|
"postinstall": "node bin/postinstall.js"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@bobfrankston/iflow-direct": "^0.1.
|
|
23
|
+
"@bobfrankston/iflow-direct": "^0.1.7",
|
|
24
24
|
"@bobfrankston/iflow-node": "^0.1.2",
|
|
25
25
|
"@bobfrankston/miscinfo": "^1.0.8",
|
|
26
26
|
"@bobfrankston/oauthsupport": "^1.0.22",
|
|
27
|
-
"@bobfrankston/msger": "^0.1.
|
|
27
|
+
"@bobfrankston/msger": "^0.1.278",
|
|
28
28
|
"@capacitor/android": "^8.3.0",
|
|
29
29
|
"@capacitor/cli": "^8.3.0",
|
|
30
30
|
"@capacitor/core": "^8.3.0",
|
|
@@ -74,11 +74,11 @@
|
|
|
74
74
|
},
|
|
75
75
|
".transformedSnapshot": {
|
|
76
76
|
"dependencies": {
|
|
77
|
-
"@bobfrankston/iflow-direct": "^0.1.
|
|
77
|
+
"@bobfrankston/iflow-direct": "^0.1.7",
|
|
78
78
|
"@bobfrankston/iflow-node": "^0.1.2",
|
|
79
79
|
"@bobfrankston/miscinfo": "^1.0.8",
|
|
80
80
|
"@bobfrankston/oauthsupport": "^1.0.22",
|
|
81
|
-
"@bobfrankston/msger": "^0.1.
|
|
81
|
+
"@bobfrankston/msger": "^0.1.278",
|
|
82
82
|
"@capacitor/android": "^8.3.0",
|
|
83
83
|
"@capacitor/cli": "^8.3.0",
|
|
84
84
|
"@capacitor/core": "^8.3.0",
|
|
@@ -16,6 +16,32 @@ import * as os from "node:os";
|
|
|
16
16
|
// Well-known ports — no magic numbers
|
|
17
17
|
const SMTP_PORT_STARTTLS = 587;
|
|
18
18
|
const SMTP_PORT_IMPLICIT_TLS = 465;
|
|
19
|
+
/** Per-message SMTP retry delay: if a send attempt fails, wait this long before
|
|
20
|
+
* the same file is retried. Gives the server time to settle so a retry after a
|
|
21
|
+
* lost-ack doesn't arrive while the first copy is still being processed. */
|
|
22
|
+
const OUTBOX_RETRY_DELAY_MS = 60000;
|
|
23
|
+
/** Parse X-Mailx-Retry* tracking headers from a raw RFC822 message. */
|
|
24
|
+
function parseRetryInfo(raw) {
|
|
25
|
+
const headerEnd = raw.search(/\r?\n\r?\n/);
|
|
26
|
+
const headers = headerEnd >= 0 ? raw.slice(0, headerEnd) : raw;
|
|
27
|
+
const attemptCount = (headers.match(/^X-Mailx-Retry:/gmi) || []).length;
|
|
28
|
+
const nextMatch = headers.match(/^X-Mailx-Retry-After:\s*(.+)$/mi);
|
|
29
|
+
const parsed = nextMatch ? Date.parse(nextMatch[1].trim()) : NaN;
|
|
30
|
+
return { attemptCount, nextAttemptAt: Number.isFinite(parsed) ? parsed : 0 };
|
|
31
|
+
}
|
|
32
|
+
/** Remove every occurrence of a header field from a raw RFC822 message. */
|
|
33
|
+
function stripHeaderField(raw, name) {
|
|
34
|
+
const re = new RegExp(`^${name.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&")}:.*\\r?\\n`, "gmi");
|
|
35
|
+
return raw.replace(re, "");
|
|
36
|
+
}
|
|
37
|
+
/** Insert a header line just before the header/body blank line. Preserves CRLF vs LF. */
|
|
38
|
+
function insertHeaderBeforeBody(raw, line) {
|
|
39
|
+
const m = raw.match(/\r?\n\r?\n/);
|
|
40
|
+
if (!m)
|
|
41
|
+
return raw + "\r\n" + line + "\r\n";
|
|
42
|
+
const nl = m[0].startsWith("\r\n") ? "\r\n" : "\n";
|
|
43
|
+
return raw.slice(0, m.index) + nl + line + raw.slice(m.index);
|
|
44
|
+
}
|
|
19
45
|
/** Extract full error detail with provenance */
|
|
20
46
|
function imapError(err) {
|
|
21
47
|
const msg = err.message || err.reason || err.code || (typeof err === "string" ? err : "");
|
|
@@ -1734,36 +1760,38 @@ export class ImapManager extends EventEmitter {
|
|
|
1734
1760
|
return;
|
|
1735
1761
|
// Gmail/API accounts: send directly via SMTP from local queue (no IMAP outbox)
|
|
1736
1762
|
const sentDir = path.join(getConfigDir(), "sending", accountId, "sent");
|
|
1737
|
-
const failedDir = path.join(getConfigDir(), "sending", accountId, "failed");
|
|
1738
1763
|
fs.mkdirSync(sentDir, { recursive: true });
|
|
1739
|
-
fs.mkdirSync(failedDir, { recursive: true });
|
|
1740
1764
|
if (this.isGmailAccount(accountId)) {
|
|
1765
|
+
const nowMs = Date.now();
|
|
1741
1766
|
for (const { dir, file } of filesToSend) {
|
|
1742
1767
|
const filePath = path.join(dir, file);
|
|
1743
|
-
|
|
1768
|
+
let raw = fs.readFileSync(filePath, "utf-8");
|
|
1769
|
+
// Per-message rate limit: if a prior attempt set X-Mailx-Retry-After
|
|
1770
|
+
// in the future, skip this file for now. Minimizes the race where the
|
|
1771
|
+
// SMTP server actually accepted DATA but we lost the ack and would
|
|
1772
|
+
// otherwise retry immediately on the next 10s tick.
|
|
1773
|
+
const retryInfo = parseRetryInfo(raw);
|
|
1774
|
+
if (retryInfo.nextAttemptAt > nowMs)
|
|
1775
|
+
continue;
|
|
1776
|
+
// Record this attempt: strip internal X-Mailx-Retry-After, append a new
|
|
1777
|
+
// X-Mailx-Retry: N <ISO-timestamp> line to the headers. The updated file
|
|
1778
|
+
// is written back *before* the send so a crash mid-send doesn't lose state.
|
|
1779
|
+
const attempt = retryInfo.attemptCount + 1;
|
|
1780
|
+
raw = stripHeaderField(raw, "X-Mailx-Retry-After");
|
|
1781
|
+
raw = insertHeaderBeforeBody(raw, `X-Mailx-Retry: ${attempt} ${new Date().toISOString()}`);
|
|
1782
|
+
fs.writeFileSync(filePath, raw, "utf-8");
|
|
1744
1783
|
try {
|
|
1745
1784
|
await this.sendRawViaSMTP(accountId, raw);
|
|
1746
1785
|
fs.renameSync(filePath, path.join(sentDir, file));
|
|
1747
|
-
console.log(` [outbox] Sent ${file} via SMTP → sent
|
|
1786
|
+
console.log(` [outbox] Sent ${file} via SMTP → sent/ (attempt ${attempt})`);
|
|
1748
1787
|
}
|
|
1749
1788
|
catch (e) {
|
|
1750
|
-
//
|
|
1751
|
-
//
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
const cmd = String(e?.command || "").toUpperCase();
|
|
1757
|
-
const preConnect = /^(ECONNREFUSED|ENOTFOUND|EAI_AGAIN|ECONNECTION|ETIMEDOUT)$/.test(code)
|
|
1758
|
-
&& (!cmd || cmd === "CONN");
|
|
1759
|
-
if (preConnect) {
|
|
1760
|
-
console.error(` [outbox] Pre-connect failure for ${file} (${code}), will retry: ${e.message}`);
|
|
1761
|
-
}
|
|
1762
|
-
else {
|
|
1763
|
-
// Ambiguous or terminal — move to failed/ so we don't resend if SMTP actually delivered
|
|
1764
|
-
fs.renameSync(filePath, path.join(failedDir, file));
|
|
1765
|
-
console.error(` [outbox] Send failed for ${file} → failed/ (no auto-retry, code=${code || "?"}): ${e.message}`);
|
|
1766
|
-
}
|
|
1789
|
+
// Persist a next-attempt timestamp so the same file won't be retried
|
|
1790
|
+
// until RETRY_DELAY_MS later — gives the server time to settle.
|
|
1791
|
+
const nextAt = new Date(nowMs + OUTBOX_RETRY_DELAY_MS).toISOString();
|
|
1792
|
+
const withDelay = insertHeaderBeforeBody(raw, `X-Mailx-Retry-After: ${nextAt}`);
|
|
1793
|
+
fs.writeFileSync(filePath, withDelay, "utf-8");
|
|
1794
|
+
console.error(` [outbox] Send failed for ${file} (attempt ${attempt}, retry after ${nextAt}): ${e.message}`);
|
|
1767
1795
|
}
|
|
1768
1796
|
}
|
|
1769
1797
|
return;
|