@bobfrankston/rmfmail 1.2.66 → 1.2.67
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/package.json +1 -1
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +59 -43
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +56 -40
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
|
@@ -5055,54 +5055,68 @@ export class ImapManager extends EventEmitter {
|
|
|
5055
5055
|
const result = await this.withConnection(accountId, async (client) => {
|
|
5056
5056
|
const flags = await client.getFlags(outboxFolder.path, uid);
|
|
5057
5057
|
|
|
5058
|
-
//
|
|
5059
|
-
//
|
|
5060
|
-
//
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5058
|
+
// The $Sending/$Failed/$PermanentFailure interlock uses CUSTOM
|
|
5059
|
+
// IMAP keywords, which only work when the server advertises \*
|
|
5060
|
+
// in PERMANENTFLAGS. AOL (and some others) don't, and reject a
|
|
5061
|
+
// STORE of any custom keyword with "[CLIENTBUG] UID STORE
|
|
5062
|
+
// Command arguments invalid" — which wedged the whole outbox in
|
|
5063
|
+
// a 5-minute retry loop, so nothing ever sent (Bob 2026-06-26,
|
|
5064
|
+
// from the log). Gate the keyword dance on real support; without
|
|
5065
|
+
// it, send best-effort with no IMAP-side claim (correct for a
|
|
5066
|
+
// single device — there's no keyword to coordinate on anyway).
|
|
5067
|
+
const supportsKeywords = !!(client as any).mailboxInfo?.permanentFlags?.includes("\\*");
|
|
5068
|
+
|
|
5069
|
+
if (supportsKeywords) {
|
|
5070
|
+
// Sweep stale claims. $Sending-<host>-<sec> with old timestamp,
|
|
5071
|
+
// or legacy $Sending-<host> without timestamp (treated as
|
|
5072
|
+
// stale; if the real owner is alive it'll re-claim next tick).
|
|
5073
|
+
const claimFlags = flags.filter((f: string) => f.startsWith("$Sending"));
|
|
5074
|
+
for (const cf of claimFlags) {
|
|
5075
|
+
const m = cf.match(/^\$Sending-(.+?)(?:-(\d+))?$/);
|
|
5076
|
+
if (!m) continue;
|
|
5077
|
+
const tsSec = m[2] ? parseInt(m[2]) : 0;
|
|
5078
|
+
const ageSec = nowSec - tsSec;
|
|
5079
|
+
if (ageSec * 1000 > STALE_CLAIM_MS) {
|
|
5080
|
+
try {
|
|
5081
|
+
await client.removeFlags(outboxFolder.path, uid, [cf]);
|
|
5082
|
+
console.log(` [outbox] Swept stale claim ${cf} on UID ${uid} (age ${Math.round(ageSec / 60)}m)`);
|
|
5083
|
+
} catch { /* ignore */ }
|
|
5084
|
+
}
|
|
5072
5085
|
}
|
|
5073
|
-
}
|
|
5074
5086
|
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5087
|
+
const flagsNow = (claimFlags.length > 0)
|
|
5088
|
+
? await client.getFlags(outboxFolder.path, uid)
|
|
5089
|
+
: flags;
|
|
5090
|
+
if (flagsNow.some((f: string) => f.startsWith("$Sending"))) return { skip: true };
|
|
5091
|
+
if (flagsNow.includes("$PermanentFailure")) return { skip: true };
|
|
5092
|
+
if (flagsNow.includes("$Failed")) {
|
|
5093
|
+
await client.removeFlags(outboxFolder.path, uid, ["$Failed"]);
|
|
5094
|
+
}
|
|
5083
5095
|
|
|
5084
|
-
|
|
5096
|
+
await client.addFlags(outboxFolder.path, uid, [sendingFlag]);
|
|
5085
5097
|
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5098
|
+
// TOCTOU re-check: if two devices addFlags concurrently both
|
|
5099
|
+
// see ≥2 sending flags. Fail-safe: both back off, next tick
|
|
5100
|
+
// one wins.
|
|
5101
|
+
const flagsAfter = await client.getFlags(outboxFolder.path, uid);
|
|
5102
|
+
const sendingFlags = flagsAfter.filter((f: string) => f.startsWith("$Sending"));
|
|
5103
|
+
if (sendingFlags.length > 1 || (sendingFlags.length === 1 && sendingFlags[0] !== sendingFlag)) {
|
|
5104
|
+
await client.removeFlags(outboxFolder.path, uid, [sendingFlag]);
|
|
5105
|
+
return { skip: true };
|
|
5106
|
+
}
|
|
5094
5107
|
}
|
|
5095
5108
|
|
|
5096
5109
|
const msg = await client.fetchMessageByUid(outboxFolder.path, uid, { source: true });
|
|
5097
5110
|
if (!msg?.source) {
|
|
5098
|
-
await client.removeFlags(outboxFolder.path, uid, [sendingFlag]);
|
|
5111
|
+
if (supportsKeywords) await client.removeFlags(outboxFolder.path, uid, [sendingFlag]);
|
|
5099
5112
|
return { skip: true };
|
|
5100
5113
|
}
|
|
5101
|
-
return { source: msg.source };
|
|
5114
|
+
return { source: msg.source, supportsKeywords };
|
|
5102
5115
|
}, { lane: "outbox" });
|
|
5103
5116
|
|
|
5104
5117
|
if ((result as any).skip) continue;
|
|
5105
5118
|
const source = (result as any).source as string;
|
|
5119
|
+
const supportsKeywords = !!(result as any).supportsKeywords;
|
|
5106
5120
|
|
|
5107
5121
|
// SMTP send is its own connection — not an IMAP op, doesn't go
|
|
5108
5122
|
// through withConnection.
|
|
@@ -5147,12 +5161,14 @@ export class ImapManager extends EventEmitter {
|
|
|
5147
5161
|
} catch (e: any) {
|
|
5148
5162
|
const errMsg = e.message || String(e);
|
|
5149
5163
|
console.error(` [outbox] Send failed UID ${uid}: ${errMsg}`);
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
await
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5164
|
+
if (supportsKeywords) {
|
|
5165
|
+
try {
|
|
5166
|
+
await this.withConnection(accountId, async (client) => {
|
|
5167
|
+
await client.removeFlags(outboxFolder.path, uid, [sendingFlag]);
|
|
5168
|
+
await client.addFlags(outboxFolder.path, uid, ["$Failed"]);
|
|
5169
|
+
}, { lane: "outbox" });
|
|
5170
|
+
} catch { /* best-effort */ }
|
|
5171
|
+
}
|
|
5156
5172
|
// Suppress the banner for transient network errors. ETIMEDOUT
|
|
5157
5173
|
// on a Dovecot send is the server being slow / a TCP idle
|
|
5158
5174
|
// drop, NOT a user-actionable failure — the next outbox tick
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.109",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bobfrankston/mailx-imap",
|
|
9
|
-
"version": "0.1.
|
|
9
|
+
"version": "0.1.109",
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@bobfrankston/iflow-direct": "^0.1.27",
|