@bobfrankston/rmfmail 1.1.129 → 1.1.130

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.
@@ -4166,11 +4166,95 @@ export class ImapManager extends EventEmitter {
4166
4166
  private outboxBackoff = new Map<string, number>(); // accountId → next retry timestamp
4167
4167
  private outboxBackoffDelay = new Map<string, number>(); // accountId → current delay ms
4168
4168
 
4169
+ /** Route a file dropped into the general `~/.rmfmail/outbox/` (no acct
4170
+ * subdir) to one of the configured accounts. Match order:
4171
+ * 1. `From:` address matches an `account.email` exactly (case-insensitive).
4172
+ * 2. `From:` domain matches a known-provider domain → first account whose
4173
+ * domain matches (gmail.com / googlemail.com → first Gmail account;
4174
+ * outlook.com / hotmail.com / live.com → first Outlook account).
4175
+ * 3. Default = first account with an explicit IMAP override host (i.e.,
4176
+ * not one of the known providers). Bob's bobma is the prototype.
4177
+ * Returns the routed accountId or null if no candidate exists. */
4178
+ private routeGeneralOutboxFile(filePath: string): string | null {
4179
+ let raw = "";
4180
+ try { raw = fs.readFileSync(filePath, "utf-8"); } catch { return null; }
4181
+ const fromMatch = raw.match(/^From:\s*(?:[^<\n]*<\s*)?([^\s<>@]+@[^\s<>]+)/mi);
4182
+ const fromAddr = (fromMatch?.[1] || "").toLowerCase().replace(/[>\s].*$/, "");
4183
+ const fromDomain = fromAddr.split("@")[1] || "";
4184
+
4185
+ const settings = loadSettings();
4186
+ const accounts = settings.accounts || [];
4187
+ // 1. exact email match
4188
+ if (fromAddr) {
4189
+ const m = accounts.find(a => (a.email || "").toLowerCase() === fromAddr);
4190
+ if (m) return m.id;
4191
+ }
4192
+ // 2. known-provider domain match
4193
+ const GMAIL_DOMAINS = new Set(["gmail.com", "googlemail.com"]);
4194
+ const OUTLOOK_DOMAINS = new Set(["outlook.com", "hotmail.com", "live.com", "msn.com"]);
4195
+ if (fromDomain) {
4196
+ if (GMAIL_DOMAINS.has(fromDomain)) {
4197
+ const m = accounts.find(a => GMAIL_DOMAINS.has((a.email || "").split("@")[1]?.toLowerCase() || ""));
4198
+ if (m) return m.id;
4199
+ }
4200
+ if (OUTLOOK_DOMAINS.has(fromDomain)) {
4201
+ const m = accounts.find(a => OUTLOOK_DOMAINS.has((a.email || "").split("@")[1]?.toLowerCase() || ""));
4202
+ if (m) return m.id;
4203
+ }
4204
+ }
4205
+ // 3. default: first account on a non-known-provider domain (i.e. one
4206
+ // whose IMAP host was explicitly configured rather than auto-detected).
4207
+ const KNOWN_PROVIDER_DOMAINS = new Set([
4208
+ ...GMAIL_DOMAINS, ...OUTLOOK_DOMAINS,
4209
+ "yahoo.com", "aol.com", "icloud.com", "me.com", "mac.com",
4210
+ ]);
4211
+ const override = accounts.find(a => {
4212
+ const d = (a.email || "").split("@")[1]?.toLowerCase() || "";
4213
+ return d && !KNOWN_PROVIDER_DOMAINS.has(d);
4214
+ });
4215
+ if (override) return override.id;
4216
+ // Last resort — any account at all
4217
+ return accounts[0]?.id || null;
4218
+ }
4219
+
4220
+ /** Scan the general outbox (`~/.rmfmail/outbox/*.ltr|*.eml` — no acct
4221
+ * subdir) and route each file into the appropriate per-account dir. Runs
4222
+ * once per outboxLoop tick before the per-account sweep, so a file
4223
+ * manually dropped at the root gets handed off the same tick. */
4224
+ private routeGeneralOutbox(): void {
4225
+ const root = path.join(getConfigDir(), "outbox");
4226
+ if (!fs.existsSync(root)) return;
4227
+ let entries: fs.Dirent[] = [];
4228
+ try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return; }
4229
+ for (const ent of entries) {
4230
+ if (!ent.isFile()) continue;
4231
+ if (!/\.(ltr|eml)$/i.test(ent.name)) continue;
4232
+ const filePath = path.join(root, ent.name);
4233
+ const accountId = this.routeGeneralOutboxFile(filePath);
4234
+ if (!accountId) {
4235
+ console.error(` [outbox] No account candidate for ${ent.name} — leaving in general outbox`);
4236
+ continue;
4237
+ }
4238
+ const acctDir = path.join(root, accountId);
4239
+ try { fs.mkdirSync(acctDir, { recursive: true }); } catch { /* */ }
4240
+ try {
4241
+ fs.renameSync(filePath, path.join(acctDir, ent.name));
4242
+ console.log(` [outbox] Routed ${ent.name} → ${accountId}/`);
4243
+ } catch (e: any) {
4244
+ console.error(` [outbox] Failed to route ${ent.name}: ${e.message}`);
4245
+ }
4246
+ }
4247
+ }
4248
+
4169
4249
  startOutboxWorker(): void {
4170
4250
  if (this.outboxInterval) return;
4171
4251
 
4172
4252
  const processAll = async () => {
4173
4253
  const now = Date.now();
4254
+ // Auto-route any files dropped into the general (acct-agnostic)
4255
+ // outbox — they move into `outbox/<accountId>/` before the
4256
+ // per-account sweep picks them up below.
4257
+ this.routeGeneralOutbox();
4174
4258
  for (const [accountId] of this.configs) {
4175
4259
  // Skip accounts in backoff
4176
4260
  const retryAfter = this.outboxBackoff.get(accountId) || 0;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-imap",
3
- "version": "0.1.57",
3
+ "version": "0.1.58",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bobfrankston/mailx-imap",
9
- "version": "0.1.57",
9
+ "version": "0.1.58",
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
12
  "@bobfrankston/iflow-direct": "^0.1.27",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-imap",
3
- "version": "0.1.57",
3
+ "version": "0.1.58",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-store",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",