@bobfrankston/mailx-imap 0.1.56 → 0.1.58
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 +15 -0
- package/index.js +139 -2
- package/package.json +3 -3
package/index.d.ts
CHANGED
|
@@ -473,6 +473,21 @@ export declare class ImapManager extends EventEmitter {
|
|
|
473
473
|
/** Start background Outbox worker — runs immediately then every 10 seconds */
|
|
474
474
|
private outboxBackoff;
|
|
475
475
|
private outboxBackoffDelay;
|
|
476
|
+
/** Route a file dropped into the general `~/.rmfmail/outbox/` (no acct
|
|
477
|
+
* subdir) to one of the configured accounts. Match order:
|
|
478
|
+
* 1. `From:` address matches an `account.email` exactly (case-insensitive).
|
|
479
|
+
* 2. `From:` domain matches a known-provider domain → first account whose
|
|
480
|
+
* domain matches (gmail.com / googlemail.com → first Gmail account;
|
|
481
|
+
* outlook.com / hotmail.com / live.com → first Outlook account).
|
|
482
|
+
* 3. Default = first account with an explicit IMAP override host (i.e.,
|
|
483
|
+
* not one of the known providers). Bob's bobma is the prototype.
|
|
484
|
+
* Returns the routed accountId or null if no candidate exists. */
|
|
485
|
+
private routeGeneralOutboxFile;
|
|
486
|
+
/** Scan the general outbox (`~/.rmfmail/outbox/*.ltr|*.eml` — no acct
|
|
487
|
+
* subdir) and route each file into the appropriate per-account dir. Runs
|
|
488
|
+
* once per outboxLoop tick before the per-account sweep, so a file
|
|
489
|
+
* manually dropped at the root gets handed off the same tick. */
|
|
490
|
+
private routeGeneralOutbox;
|
|
476
491
|
startOutboxWorker(): void;
|
|
477
492
|
/** Stop Outbox worker */
|
|
478
493
|
stopOutboxWorker(): void;
|
package/index.js
CHANGED
|
@@ -3195,6 +3195,15 @@ export class ImapManager extends EventEmitter {
|
|
|
3195
3195
|
}
|
|
3196
3196
|
continue;
|
|
3197
3197
|
}
|
|
3198
|
+
// Success — clear the in-flight delete/move
|
|
3199
|
+
// suppression (see the IMAP branch below for the
|
|
3200
|
+
// full rationale). After a move the local row lives
|
|
3201
|
+
// at the TARGET folder, so the (uid, folder) lookup
|
|
3202
|
+
// uses targetFolderId.
|
|
3203
|
+
if (action.action === "move") {
|
|
3204
|
+
this.db.clearTombstoneForUid(accountId, action.uid, action.targetFolderId);
|
|
3205
|
+
this.db.clearMessagePendingDelete(accountId, action.uid, action.targetFolderId);
|
|
3206
|
+
}
|
|
3198
3207
|
this.db.completeSyncAction(action.id);
|
|
3199
3208
|
}
|
|
3200
3209
|
catch (e) {
|
|
@@ -3204,7 +3213,11 @@ export class ImapManager extends EventEmitter {
|
|
|
3204
3213
|
// Terminal failure on delete/move → clear tombstone
|
|
3205
3214
|
// so the row reappears on next sync (server still
|
|
3206
3215
|
// has it). Same rationale as the IMAP branch below.
|
|
3207
|
-
if (action.action === "
|
|
3216
|
+
if (action.action === "move") {
|
|
3217
|
+
this.db.clearTombstoneForUid(accountId, action.uid, action.targetFolderId);
|
|
3218
|
+
this.db.clearMessagePendingDelete(accountId, action.uid, action.targetFolderId);
|
|
3219
|
+
}
|
|
3220
|
+
else if (action.action === "delete") {
|
|
3208
3221
|
this.db.clearTombstoneForUid(accountId, action.uid, action.folderId);
|
|
3209
3222
|
}
|
|
3210
3223
|
this.db.completeSyncAction(action.id);
|
|
@@ -3280,6 +3293,22 @@ export class ImapManager extends EventEmitter {
|
|
|
3280
3293
|
break;
|
|
3281
3294
|
}
|
|
3282
3295
|
}
|
|
3296
|
+
// Success: the local action reached the server. Lift the
|
|
3297
|
+
// in-flight delete/move suppression so the destination
|
|
3298
|
+
// folder syncs the moved message under its real
|
|
3299
|
+
// post-move UID. hasTombstone's documented contract is
|
|
3300
|
+
// "cleared on successful action complete OR permanent
|
|
3301
|
+
// failure" — only the failure half had been wired, so a
|
|
3302
|
+
// successful move left a Message-ID tombstone alive for
|
|
3303
|
+
// 30 days, blocking the Trash folder from ever storing
|
|
3304
|
+
// the message (and resurrecting it once the tombstone
|
|
3305
|
+
// finally aged out). After a move the local row lives at
|
|
3306
|
+
// the TARGET folder, so the (uid, folder) lookup uses
|
|
3307
|
+
// targetFolderId.
|
|
3308
|
+
if (action.action === "move") {
|
|
3309
|
+
this.db.clearTombstoneForUid(accountId, action.uid, action.targetFolderId);
|
|
3310
|
+
this.db.clearMessagePendingDelete(accountId, action.uid, action.targetFolderId);
|
|
3311
|
+
}
|
|
3283
3312
|
this.db.completeSyncAction(action.id);
|
|
3284
3313
|
}
|
|
3285
3314
|
catch (e) {
|
|
@@ -3296,7 +3325,13 @@ export class ImapManager extends EventEmitter {
|
|
|
3296
3325
|
// reflecting "your action didn't take, here it is
|
|
3297
3326
|
// again." Applies to delete + move; flags/append
|
|
3298
3327
|
// never tombstone.
|
|
3299
|
-
if (action.action === "
|
|
3328
|
+
if (action.action === "move") {
|
|
3329
|
+
// Local row is at the TARGET folder (the local
|
|
3330
|
+
// move committed; only the server move failed).
|
|
3331
|
+
this.db.clearTombstoneForUid(accountId, action.uid, action.targetFolderId);
|
|
3332
|
+
this.db.clearMessagePendingDelete(accountId, action.uid, action.targetFolderId);
|
|
3333
|
+
}
|
|
3334
|
+
else if (action.action === "delete") {
|
|
3300
3335
|
this.db.clearTombstoneForUid(accountId, action.uid, action.folderId);
|
|
3301
3336
|
}
|
|
3302
3337
|
this.db.completeSyncAction(action.id);
|
|
@@ -4178,11 +4213,113 @@ export class ImapManager extends EventEmitter {
|
|
|
4178
4213
|
/** Start background Outbox worker — runs immediately then every 10 seconds */
|
|
4179
4214
|
outboxBackoff = new Map(); // accountId → next retry timestamp
|
|
4180
4215
|
outboxBackoffDelay = new Map(); // accountId → current delay ms
|
|
4216
|
+
/** Route a file dropped into the general `~/.rmfmail/outbox/` (no acct
|
|
4217
|
+
* subdir) to one of the configured accounts. Match order:
|
|
4218
|
+
* 1. `From:` address matches an `account.email` exactly (case-insensitive).
|
|
4219
|
+
* 2. `From:` domain matches a known-provider domain → first account whose
|
|
4220
|
+
* domain matches (gmail.com / googlemail.com → first Gmail account;
|
|
4221
|
+
* outlook.com / hotmail.com / live.com → first Outlook account).
|
|
4222
|
+
* 3. Default = first account with an explicit IMAP override host (i.e.,
|
|
4223
|
+
* not one of the known providers). Bob's bobma is the prototype.
|
|
4224
|
+
* Returns the routed accountId or null if no candidate exists. */
|
|
4225
|
+
routeGeneralOutboxFile(filePath) {
|
|
4226
|
+
let raw = "";
|
|
4227
|
+
try {
|
|
4228
|
+
raw = fs.readFileSync(filePath, "utf-8");
|
|
4229
|
+
}
|
|
4230
|
+
catch {
|
|
4231
|
+
return null;
|
|
4232
|
+
}
|
|
4233
|
+
const fromMatch = raw.match(/^From:\s*(?:[^<\n]*<\s*)?([^\s<>@]+@[^\s<>]+)/mi);
|
|
4234
|
+
const fromAddr = (fromMatch?.[1] || "").toLowerCase().replace(/[>\s].*$/, "");
|
|
4235
|
+
const fromDomain = fromAddr.split("@")[1] || "";
|
|
4236
|
+
const settings = loadSettings();
|
|
4237
|
+
const accounts = settings.accounts || [];
|
|
4238
|
+
// 1. exact email match
|
|
4239
|
+
if (fromAddr) {
|
|
4240
|
+
const m = accounts.find(a => (a.email || "").toLowerCase() === fromAddr);
|
|
4241
|
+
if (m)
|
|
4242
|
+
return m.id;
|
|
4243
|
+
}
|
|
4244
|
+
// 2. known-provider domain match
|
|
4245
|
+
const GMAIL_DOMAINS = new Set(["gmail.com", "googlemail.com"]);
|
|
4246
|
+
const OUTLOOK_DOMAINS = new Set(["outlook.com", "hotmail.com", "live.com", "msn.com"]);
|
|
4247
|
+
if (fromDomain) {
|
|
4248
|
+
if (GMAIL_DOMAINS.has(fromDomain)) {
|
|
4249
|
+
const m = accounts.find(a => GMAIL_DOMAINS.has((a.email || "").split("@")[1]?.toLowerCase() || ""));
|
|
4250
|
+
if (m)
|
|
4251
|
+
return m.id;
|
|
4252
|
+
}
|
|
4253
|
+
if (OUTLOOK_DOMAINS.has(fromDomain)) {
|
|
4254
|
+
const m = accounts.find(a => OUTLOOK_DOMAINS.has((a.email || "").split("@")[1]?.toLowerCase() || ""));
|
|
4255
|
+
if (m)
|
|
4256
|
+
return m.id;
|
|
4257
|
+
}
|
|
4258
|
+
}
|
|
4259
|
+
// 3. default: first account on a non-known-provider domain (i.e. one
|
|
4260
|
+
// whose IMAP host was explicitly configured rather than auto-detected).
|
|
4261
|
+
const KNOWN_PROVIDER_DOMAINS = new Set([
|
|
4262
|
+
...GMAIL_DOMAINS, ...OUTLOOK_DOMAINS,
|
|
4263
|
+
"yahoo.com", "aol.com", "icloud.com", "me.com", "mac.com",
|
|
4264
|
+
]);
|
|
4265
|
+
const override = accounts.find(a => {
|
|
4266
|
+
const d = (a.email || "").split("@")[1]?.toLowerCase() || "";
|
|
4267
|
+
return d && !KNOWN_PROVIDER_DOMAINS.has(d);
|
|
4268
|
+
});
|
|
4269
|
+
if (override)
|
|
4270
|
+
return override.id;
|
|
4271
|
+
// Last resort — any account at all
|
|
4272
|
+
return accounts[0]?.id || null;
|
|
4273
|
+
}
|
|
4274
|
+
/** Scan the general outbox (`~/.rmfmail/outbox/*.ltr|*.eml` — no acct
|
|
4275
|
+
* subdir) and route each file into the appropriate per-account dir. Runs
|
|
4276
|
+
* once per outboxLoop tick before the per-account sweep, so a file
|
|
4277
|
+
* manually dropped at the root gets handed off the same tick. */
|
|
4278
|
+
routeGeneralOutbox() {
|
|
4279
|
+
const root = path.join(getConfigDir(), "outbox");
|
|
4280
|
+
if (!fs.existsSync(root))
|
|
4281
|
+
return;
|
|
4282
|
+
let entries = [];
|
|
4283
|
+
try {
|
|
4284
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
4285
|
+
}
|
|
4286
|
+
catch {
|
|
4287
|
+
return;
|
|
4288
|
+
}
|
|
4289
|
+
for (const ent of entries) {
|
|
4290
|
+
if (!ent.isFile())
|
|
4291
|
+
continue;
|
|
4292
|
+
if (!/\.(ltr|eml)$/i.test(ent.name))
|
|
4293
|
+
continue;
|
|
4294
|
+
const filePath = path.join(root, ent.name);
|
|
4295
|
+
const accountId = this.routeGeneralOutboxFile(filePath);
|
|
4296
|
+
if (!accountId) {
|
|
4297
|
+
console.error(` [outbox] No account candidate for ${ent.name} — leaving in general outbox`);
|
|
4298
|
+
continue;
|
|
4299
|
+
}
|
|
4300
|
+
const acctDir = path.join(root, accountId);
|
|
4301
|
+
try {
|
|
4302
|
+
fs.mkdirSync(acctDir, { recursive: true });
|
|
4303
|
+
}
|
|
4304
|
+
catch { /* */ }
|
|
4305
|
+
try {
|
|
4306
|
+
fs.renameSync(filePath, path.join(acctDir, ent.name));
|
|
4307
|
+
console.log(` [outbox] Routed ${ent.name} → ${accountId}/`);
|
|
4308
|
+
}
|
|
4309
|
+
catch (e) {
|
|
4310
|
+
console.error(` [outbox] Failed to route ${ent.name}: ${e.message}`);
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4181
4314
|
startOutboxWorker() {
|
|
4182
4315
|
if (this.outboxInterval)
|
|
4183
4316
|
return;
|
|
4184
4317
|
const processAll = async () => {
|
|
4185
4318
|
const now = Date.now();
|
|
4319
|
+
// Auto-route any files dropped into the general (acct-agnostic)
|
|
4320
|
+
// outbox — they move into `outbox/<accountId>/` before the
|
|
4321
|
+
// per-account sweep picks them up below.
|
|
4322
|
+
this.routeGeneralOutbox();
|
|
4186
4323
|
for (const [accountId] of this.configs) {
|
|
4187
4324
|
// Skip accounts in backoff
|
|
4188
4325
|
const retryAfter = this.outboxBackoff.get(accountId) || 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.58",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@bobfrankston/mailx-types": "^0.1.18",
|
|
13
13
|
"@bobfrankston/mailx-settings": "^0.1.22",
|
|
14
|
-
"@bobfrankston/mailx-store": "^0.1.
|
|
14
|
+
"@bobfrankston/mailx-store": "^0.1.34",
|
|
15
15
|
"@bobfrankston/iflow-direct": "^0.1.50",
|
|
16
16
|
"@bobfrankston/tcp-transport": "^0.1.6",
|
|
17
17
|
"@bobfrankston/smtp-direct": "^0.1.8",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bobfrankston/mailx-types": "^0.1.18",
|
|
41
41
|
"@bobfrankston/mailx-settings": "^0.1.22",
|
|
42
|
-
"@bobfrankston/mailx-store": "^0.1.
|
|
42
|
+
"@bobfrankston/mailx-store": "^0.1.34",
|
|
43
43
|
"@bobfrankston/iflow-direct": "^0.1.50",
|
|
44
44
|
"@bobfrankston/tcp-transport": "^0.1.6",
|
|
45
45
|
"@bobfrankston/smtp-direct": "^0.1.8",
|