@bobfrankston/rmfmail 1.1.203 → 1.1.205
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/compose/compose.bundle.js +100 -25
- package/client/compose/compose.bundle.js.map +3 -3
- package/client/lib/rmf-tiny.js +121 -25
- package/package.json +3 -3
- package/packages/mailx-imap/index.d.ts +22 -0
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +45 -30
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +47 -31
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-18260 → node_modules.npmglobalize-stash-32632}/.package-lock.json +0 -0
|
@@ -4038,35 +4038,35 @@ export class ImapManager extends EventEmitter {
|
|
|
4038
4038
|
* sending/queued/ on every send — that write is gone now, so scanning the
|
|
4039
4039
|
* directory is safe again. Any legitimate files that land there (crash
|
|
4040
4040
|
* recovery, manual drop) will get sent. */
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
// claim with no progress means the worker is wedged or PID
|
|
4064
|
-
// got recycled. Earlier 1h was a band-aid that left Bob's
|
|
4065
|
-
// outbox stuck for hours when an SMTP wedge crashed the
|
|
4066
|
-
// worker mid-flight.
|
|
4041
|
+
/** Recover stale `.sending-<host>-<pid>` claims for an account back to
|
|
4042
|
+
* plain `.ltr` so the next tick can retry. A claim is stale if:
|
|
4043
|
+
* (a) the PID is dead — original owner crashed/was replaced mid-send;
|
|
4044
|
+
* (b) the PID is alive but it's not us and the file mtime is older than
|
|
4045
|
+
* STALE_CLAIM_MS — `process.kill(pid,0)` only proves *some* process
|
|
4046
|
+
* owns that PID, not our long-dead daemon (OS recycles PIDs); the
|
|
4047
|
+
* age guard stops an unrelated Node (statusline/msger/npm) inheriting
|
|
4048
|
+
* a recycled PID from pinning the claim forever (Bob's 7-hour stuck
|
|
4049
|
+
* `.sending-rmf39-63196`);
|
|
4050
|
+
* (c) it's our PID — never sweep our own live claim.
|
|
4051
|
+
* Foreign hosts are left alone (we can't probe their PIDs); cross-host
|
|
4052
|
+
* recovery is the IMAP-folder sweeper's job.
|
|
4053
|
+
*
|
|
4054
|
+
* CRITICAL: this is a local filesystem op with NO network I/O, so it MUST
|
|
4055
|
+
* run independently of send-backoff. It used to live inside
|
|
4056
|
+
* processLocalQueue, which the outbox worker SKIPS when an account is in
|
|
4057
|
+
* backoff — so a persistently-erroring account (e.g. Dovecot "Not
|
|
4058
|
+
* connected" storms) never recovered its claims, leaving a message stuck
|
|
4059
|
+
* "sending…" forever while healthy accounts drained. Bob 2026-05-31:
|
|
4060
|
+
* "stuck while others are getting sent." Now called every tick for every
|
|
4061
|
+
* account before the backoff gate. */
|
|
4062
|
+
private recoverStaleClaims(accountId: string): void {
|
|
4067
4063
|
const STALE_CLAIM_MS = 5 * 60_000;
|
|
4068
4064
|
const myPid = process.pid;
|
|
4069
|
-
|
|
4065
|
+
const dirs = [
|
|
4066
|
+
path.join(getConfigDir(), "outbox", accountId),
|
|
4067
|
+
path.join(getConfigDir(), "sending", accountId, "queued"),
|
|
4068
|
+
];
|
|
4069
|
+
for (const dir of dirs) {
|
|
4070
4070
|
if (!fs.existsSync(dir)) continue;
|
|
4071
4071
|
for (const f of fs.readdirSync(dir)) {
|
|
4072
4072
|
const m = f.match(/^(.+)\.sending-([^-]+)-(\d+)$/);
|
|
@@ -4079,9 +4079,8 @@ export class ImapManager extends EventEmitter {
|
|
|
4079
4079
|
try { process.kill(pid, 0); alive = true; } catch { /* dead */ }
|
|
4080
4080
|
let ageMs = Infinity;
|
|
4081
4081
|
try { ageMs = Date.now() - fs.statSync(path.join(dir, f)).mtimeMs; } catch { /* */ }
|
|
4082
|
-
// Live PID + recent mtime →
|
|
4083
|
-
// Live PID + ancient mtime → PID
|
|
4084
|
-
// Dead PID → sweep regardless of age.
|
|
4082
|
+
// Live PID + recent mtime → genuine sibling owner, leave it.
|
|
4083
|
+
// Live PID + ancient mtime → recycled PID, sweep. Dead PID → sweep.
|
|
4085
4084
|
if (alive && ageMs < STALE_CLAIM_MS) continue;
|
|
4086
4085
|
try {
|
|
4087
4086
|
fs.renameSync(path.join(dir, f), path.join(dir, original));
|
|
@@ -4090,6 +4089,16 @@ export class ImapManager extends EventEmitter {
|
|
|
4090
4089
|
} catch { /* ignore */ }
|
|
4091
4090
|
}
|
|
4092
4091
|
}
|
|
4092
|
+
}
|
|
4093
|
+
|
|
4094
|
+
private async processLocalQueue(accountId: string): Promise<void> {
|
|
4095
|
+
const outboxDir = path.join(getConfigDir(), "outbox", accountId);
|
|
4096
|
+
const queuedDir = path.join(getConfigDir(), "sending", accountId, "queued");
|
|
4097
|
+
|
|
4098
|
+
// Recovery also runs unconditionally in the worker tick (before the
|
|
4099
|
+
// backoff gate); keep it here too for the direct-call path from
|
|
4100
|
+
// queueOutgoing. Idempotent — a no-op when nothing is stale.
|
|
4101
|
+
this.recoverStaleClaims(accountId);
|
|
4093
4102
|
|
|
4094
4103
|
const filesToSend: { dir: string; file: string }[] = [];
|
|
4095
4104
|
for (const dir of [outboxDir, queuedDir]) {
|
|
@@ -4541,7 +4550,14 @@ export class ImapManager extends EventEmitter {
|
|
|
4541
4550
|
// per-account sweep picks them up below.
|
|
4542
4551
|
this.routeGeneralOutbox();
|
|
4543
4552
|
for (const [accountId] of this.configs) {
|
|
4544
|
-
//
|
|
4553
|
+
// Recover stale claims FIRST, unconditionally — it's a local
|
|
4554
|
+
// FS op with no network, so it must not be gated by send
|
|
4555
|
+
// backoff. Otherwise an account stuck in backoff (Dovecot "Not
|
|
4556
|
+
// connected" storms) never unclaims an orphaned .sending- file,
|
|
4557
|
+
// and a message sits "sending…" forever while other accounts
|
|
4558
|
+
// drain (Bob 2026-05-31).
|
|
4559
|
+
this.recoverStaleClaims(accountId);
|
|
4560
|
+
// Skip the SEND for accounts in backoff (network ops only).
|
|
4545
4561
|
const retryAfter = this.outboxBackoff.get(accountId) || 0;
|
|
4546
4562
|
if (now < retryAfter) continue;
|
|
4547
4563
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.79",
|
|
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.79",
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@bobfrankston/iflow-direct": "^0.1.27",
|