@bobfrankston/mailx-imap 0.1.78 → 0.1.79
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/index.d.ts +22 -0
- package/index.js +45 -30
- package/package.json +3 -3
package/index.d.ts
CHANGED
|
@@ -479,6 +479,28 @@ export declare class ImapManager extends EventEmitter {
|
|
|
479
479
|
* sending/queued/ on every send — that write is gone now, so scanning the
|
|
480
480
|
* directory is safe again. Any legitimate files that land there (crash
|
|
481
481
|
* recovery, manual drop) will get sent. */
|
|
482
|
+
/** Recover stale `.sending-<host>-<pid>` claims for an account back to
|
|
483
|
+
* plain `.ltr` so the next tick can retry. A claim is stale if:
|
|
484
|
+
* (a) the PID is dead — original owner crashed/was replaced mid-send;
|
|
485
|
+
* (b) the PID is alive but it's not us and the file mtime is older than
|
|
486
|
+
* STALE_CLAIM_MS — `process.kill(pid,0)` only proves *some* process
|
|
487
|
+
* owns that PID, not our long-dead daemon (OS recycles PIDs); the
|
|
488
|
+
* age guard stops an unrelated Node (statusline/msger/npm) inheriting
|
|
489
|
+
* a recycled PID from pinning the claim forever (Bob's 7-hour stuck
|
|
490
|
+
* `.sending-rmf39-63196`);
|
|
491
|
+
* (c) it's our PID — never sweep our own live claim.
|
|
492
|
+
* Foreign hosts are left alone (we can't probe their PIDs); cross-host
|
|
493
|
+
* recovery is the IMAP-folder sweeper's job.
|
|
494
|
+
*
|
|
495
|
+
* CRITICAL: this is a local filesystem op with NO network I/O, so it MUST
|
|
496
|
+
* run independently of send-backoff. It used to live inside
|
|
497
|
+
* processLocalQueue, which the outbox worker SKIPS when an account is in
|
|
498
|
+
* backoff — so a persistently-erroring account (e.g. Dovecot "Not
|
|
499
|
+
* connected" storms) never recovered its claims, leaving a message stuck
|
|
500
|
+
* "sending…" forever while healthy accounts drained. Bob 2026-05-31:
|
|
501
|
+
* "stuck while others are getting sent." Now called every tick for every
|
|
502
|
+
* account before the backoff gate. */
|
|
503
|
+
private recoverStaleClaims;
|
|
482
504
|
private processLocalQueue;
|
|
483
505
|
/** Send a raw RFC 2822 message via SMTP for a given account.
|
|
484
506
|
* Uses @bobfrankston/smtp-direct with the same TransportFactory as IMAP —
|
package/index.js
CHANGED
|
@@ -4089,34 +4089,35 @@ export class ImapManager extends EventEmitter {
|
|
|
4089
4089
|
* sending/queued/ on every send — that write is gone now, so scanning the
|
|
4090
4090
|
* directory is safe again. Any legitimate files that land there (crash
|
|
4091
4091
|
* recovery, manual drop) will get sent. */
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
// got recycled. Earlier 1h was a band-aid that left Bob's
|
|
4115
|
-
// outbox stuck for hours when an SMTP wedge crashed the
|
|
4116
|
-
// worker mid-flight.
|
|
4092
|
+
/** Recover stale `.sending-<host>-<pid>` claims for an account back to
|
|
4093
|
+
* plain `.ltr` so the next tick can retry. A claim is stale if:
|
|
4094
|
+
* (a) the PID is dead — original owner crashed/was replaced mid-send;
|
|
4095
|
+
* (b) the PID is alive but it's not us and the file mtime is older than
|
|
4096
|
+
* STALE_CLAIM_MS — `process.kill(pid,0)` only proves *some* process
|
|
4097
|
+
* owns that PID, not our long-dead daemon (OS recycles PIDs); the
|
|
4098
|
+
* age guard stops an unrelated Node (statusline/msger/npm) inheriting
|
|
4099
|
+
* a recycled PID from pinning the claim forever (Bob's 7-hour stuck
|
|
4100
|
+
* `.sending-rmf39-63196`);
|
|
4101
|
+
* (c) it's our PID — never sweep our own live claim.
|
|
4102
|
+
* Foreign hosts are left alone (we can't probe their PIDs); cross-host
|
|
4103
|
+
* recovery is the IMAP-folder sweeper's job.
|
|
4104
|
+
*
|
|
4105
|
+
* CRITICAL: this is a local filesystem op with NO network I/O, so it MUST
|
|
4106
|
+
* run independently of send-backoff. It used to live inside
|
|
4107
|
+
* processLocalQueue, which the outbox worker SKIPS when an account is in
|
|
4108
|
+
* backoff — so a persistently-erroring account (e.g. Dovecot "Not
|
|
4109
|
+
* connected" storms) never recovered its claims, leaving a message stuck
|
|
4110
|
+
* "sending…" forever while healthy accounts drained. Bob 2026-05-31:
|
|
4111
|
+
* "stuck while others are getting sent." Now called every tick for every
|
|
4112
|
+
* account before the backoff gate. */
|
|
4113
|
+
recoverStaleClaims(accountId) {
|
|
4117
4114
|
const STALE_CLAIM_MS = 5 * 60_000;
|
|
4118
4115
|
const myPid = process.pid;
|
|
4119
|
-
|
|
4116
|
+
const dirs = [
|
|
4117
|
+
path.join(getConfigDir(), "outbox", accountId),
|
|
4118
|
+
path.join(getConfigDir(), "sending", accountId, "queued"),
|
|
4119
|
+
];
|
|
4120
|
+
for (const dir of dirs) {
|
|
4120
4121
|
if (!fs.existsSync(dir))
|
|
4121
4122
|
continue;
|
|
4122
4123
|
for (const f of fs.readdirSync(dir)) {
|
|
@@ -4140,9 +4141,8 @@ export class ImapManager extends EventEmitter {
|
|
|
4140
4141
|
ageMs = Date.now() - fs.statSync(path.join(dir, f)).mtimeMs;
|
|
4141
4142
|
}
|
|
4142
4143
|
catch { /* */ }
|
|
4143
|
-
// Live PID + recent mtime →
|
|
4144
|
-
// Live PID + ancient mtime → PID
|
|
4145
|
-
// Dead PID → sweep regardless of age.
|
|
4144
|
+
// Live PID + recent mtime → genuine sibling owner, leave it.
|
|
4145
|
+
// Live PID + ancient mtime → recycled PID, sweep. Dead PID → sweep.
|
|
4146
4146
|
if (alive && ageMs < STALE_CLAIM_MS)
|
|
4147
4147
|
continue;
|
|
4148
4148
|
try {
|
|
@@ -4153,6 +4153,14 @@ export class ImapManager extends EventEmitter {
|
|
|
4153
4153
|
catch { /* ignore */ }
|
|
4154
4154
|
}
|
|
4155
4155
|
}
|
|
4156
|
+
}
|
|
4157
|
+
async processLocalQueue(accountId) {
|
|
4158
|
+
const outboxDir = path.join(getConfigDir(), "outbox", accountId);
|
|
4159
|
+
const queuedDir = path.join(getConfigDir(), "sending", accountId, "queued");
|
|
4160
|
+
// Recovery also runs unconditionally in the worker tick (before the
|
|
4161
|
+
// backoff gate); keep it here too for the direct-call path from
|
|
4162
|
+
// queueOutgoing. Idempotent — a no-op when nothing is stale.
|
|
4163
|
+
this.recoverStaleClaims(accountId);
|
|
4156
4164
|
const filesToSend = [];
|
|
4157
4165
|
for (const dir of [outboxDir, queuedDir]) {
|
|
4158
4166
|
if (!fs.existsSync(dir))
|
|
@@ -4634,7 +4642,14 @@ export class ImapManager extends EventEmitter {
|
|
|
4634
4642
|
// per-account sweep picks them up below.
|
|
4635
4643
|
this.routeGeneralOutbox();
|
|
4636
4644
|
for (const [accountId] of this.configs) {
|
|
4637
|
-
//
|
|
4645
|
+
// Recover stale claims FIRST, unconditionally — it's a local
|
|
4646
|
+
// FS op with no network, so it must not be gated by send
|
|
4647
|
+
// backoff. Otherwise an account stuck in backoff (Dovecot "Not
|
|
4648
|
+
// connected" storms) never unclaims an orphaned .sending- file,
|
|
4649
|
+
// and a message sits "sending…" forever while other accounts
|
|
4650
|
+
// drain (Bob 2026-05-31).
|
|
4651
|
+
this.recoverStaleClaims(accountId);
|
|
4652
|
+
// Skip the SEND for accounts in backoff (network ops only).
|
|
4638
4653
|
const retryAfter = this.outboxBackoff.get(accountId) || 0;
|
|
4639
4654
|
if (now < retryAfter)
|
|
4640
4655
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.79",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@bobfrankston/mailx-types": "^0.1.18",
|
|
13
|
-
"@bobfrankston/mailx-settings": "^0.1.
|
|
13
|
+
"@bobfrankston/mailx-settings": "^0.1.26",
|
|
14
14
|
"@bobfrankston/mailx-store": "^0.1.42",
|
|
15
15
|
"@bobfrankston/iflow-direct": "^0.1.51",
|
|
16
16
|
"@bobfrankston/tcp-transport": "^0.1.6",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
".transformedSnapshot": {
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bobfrankston/mailx-types": "^0.1.18",
|
|
41
|
-
"@bobfrankston/mailx-settings": "^0.1.
|
|
41
|
+
"@bobfrankston/mailx-settings": "^0.1.26",
|
|
42
42
|
"@bobfrankston/mailx-store": "^0.1.42",
|
|
43
43
|
"@bobfrankston/iflow-direct": "^0.1.51",
|
|
44
44
|
"@bobfrankston/tcp-transport": "^0.1.6",
|