@bobfrankston/mailx-settings 0.1.23 → 0.1.25
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/cloud.d.ts.map +1 -1
- package/cloud.js +140 -1
- package/index.d.ts +19 -0
- package/index.d.ts.map +1 -1
- package/index.js +37 -0
- package/package.json +1 -1
package/cloud.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["cloud.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;
|
|
1
|
+
{"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["cloud.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AA+RH;;;;;;;;;oDASoD;AACpD,wBAAsB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA8BnF;AAcD;;;;;;;;;;;;;;;;;6CAiB6C;AAC7C,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAwBvE;AA6FD,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE1D,MAAM,WAAW,SAAS;IACtB,kFAAkF;IAClF,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC/C,uGAAuG;IACvG,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,8BAA8B;IAC9B,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9C;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAuBtF;AAED;;;;2DAI2D;AAC3D,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAoBvG"}
|
package/cloud.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import fs from "node:fs";
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
import { authenticateOAuth } from "@bobfrankston/oauthsupport";
|
|
22
|
+
import { loadAccounts, tokenDirName } from "./index.js";
|
|
22
23
|
const SETTINGS_DIR = path.join(process.env.USERPROFILE || process.env.HOME || ".", ".rmfmail");
|
|
23
24
|
// ── Credentials ──
|
|
24
25
|
// Google Drive: reuse iflow's OAuth credentials (same Google Cloud project)
|
|
@@ -55,19 +56,157 @@ const GDRIVE_TOKEN_DIR = path.join(SETTINGS_DIR, "tokens", "gdrive");
|
|
|
55
56
|
// bigger privacy ask. Token cache at tokens/gdrive/ will re-auth on scope change.
|
|
56
57
|
const GDRIVE_SCOPES = "https://www.googleapis.com/auth/drive";
|
|
57
58
|
// ── Token helpers ──
|
|
59
|
+
/** One-time-per-process archive pass: move every token directory that
|
|
60
|
+
* doesn't correspond to a currently-configured account into
|
|
61
|
+
* `tokens/.archive/<original-name>/`. Without this, `cloud.ts` would
|
|
62
|
+
* walk all token dirs in `~/.rmfmail/tokens/` — including leftovers from
|
|
63
|
+
* past setups (a Gmail address typed with a dot creates one dir, the
|
|
64
|
+
* same mailbox without the dot creates another) — and trigger a fresh
|
|
65
|
+
* OAuth flow on a stale dir that can't refresh, looping the user through
|
|
66
|
+
* consent forever (Bob 2026-05-25). Archived dirs are recoverable: move
|
|
67
|
+
* them back if I guessed wrong about which is stale. */
|
|
68
|
+
let _staleArchiveRun = false;
|
|
69
|
+
function archiveStaleTokenDirs(activeDirs) {
|
|
70
|
+
if (_staleArchiveRun)
|
|
71
|
+
return;
|
|
72
|
+
_staleArchiveRun = true;
|
|
73
|
+
const tokensDir = path.join(SETTINGS_DIR, "tokens");
|
|
74
|
+
if (!fs.existsSync(tokensDir))
|
|
75
|
+
return;
|
|
76
|
+
const archiveRoot = path.join(tokensDir, ".archive");
|
|
77
|
+
let archived = 0;
|
|
78
|
+
let migrated = 0;
|
|
79
|
+
try {
|
|
80
|
+
for (const entry of fs.readdirSync(tokensDir)) {
|
|
81
|
+
// Preserve: special sub-dirs, the archive itself, all active entries.
|
|
82
|
+
if (entry === "gdrive" || entry === ".archive")
|
|
83
|
+
continue;
|
|
84
|
+
if (entry.startsWith("."))
|
|
85
|
+
continue;
|
|
86
|
+
if (activeDirs.has(entry))
|
|
87
|
+
continue;
|
|
88
|
+
const src = path.join(tokensDir, entry);
|
|
89
|
+
try {
|
|
90
|
+
if (!fs.statSync(src).isDirectory())
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// MIGRATE before archive: older builds wrote the token dir with
|
|
97
|
+
// different casing (Pascal vs. lowercase) or different dot handling.
|
|
98
|
+
// If this dir's lowercased name matches an active dir, it's the
|
|
99
|
+
// same account's tokens under an obsolete naming convention —
|
|
100
|
+
// moving the token file to the canonical dir lets the user keep
|
|
101
|
+
// signing in instead of being thrown back to Google's consent
|
|
102
|
+
// screen for the third time (Bob 2026-05-26 "why authenticating
|
|
103
|
+
// again"). The migration preserves the oauth-token.json file; the
|
|
104
|
+
// now-empty source dir falls through to archive below.
|
|
105
|
+
const entryLc = entry.toLowerCase();
|
|
106
|
+
if (activeDirs.has(entryLc) && entryLc !== entry) {
|
|
107
|
+
const tokenFile = path.join(src, "oauth-token.json");
|
|
108
|
+
if (fs.existsSync(tokenFile)) {
|
|
109
|
+
const dstDir = path.join(tokensDir, entryLc);
|
|
110
|
+
const dstTokenFile = path.join(dstDir, "oauth-token.json");
|
|
111
|
+
try {
|
|
112
|
+
if (fs.existsSync(dstTokenFile)) {
|
|
113
|
+
// Target dir already has a token — pick the newer one.
|
|
114
|
+
const srcMtime = fs.statSync(tokenFile).mtimeMs;
|
|
115
|
+
const dstMtime = fs.statSync(dstTokenFile).mtimeMs;
|
|
116
|
+
if (srcMtime > dstMtime) {
|
|
117
|
+
fs.copyFileSync(tokenFile, dstTokenFile);
|
|
118
|
+
console.log(` [cloud] Migrated newer token from ${entry} → ${entryLc} (replacing older)`);
|
|
119
|
+
migrated++;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
console.log(` [cloud] Skipped migrating ${entry} → ${entryLc} (target token is newer)`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
fs.mkdirSync(dstDir, { recursive: true });
|
|
127
|
+
fs.copyFileSync(tokenFile, dstTokenFile);
|
|
128
|
+
console.log(` [cloud] Migrated token from ${entry} → ${entryLc}`);
|
|
129
|
+
migrated++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
console.error(` [cloud] Token migration ${entry} → ${entryLc} failed: ${e?.message || e}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Fall through to archive — the source dir is now an artifact.
|
|
137
|
+
}
|
|
138
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
139
|
+
const dst = path.join(archiveRoot, `${stamp}-${entry}`);
|
|
140
|
+
try {
|
|
141
|
+
fs.mkdirSync(archiveRoot, { recursive: true });
|
|
142
|
+
fs.renameSync(src, dst);
|
|
143
|
+
console.log(` [cloud] Archived stale token dir: ${entry} → .archive/${path.basename(dst)}`);
|
|
144
|
+
archived++;
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
console.error(` [cloud] Failed to archive ${entry}: ${e?.message || e}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
console.error(` [cloud] tokens dir scan failed: ${e?.message || e}`);
|
|
153
|
+
}
|
|
154
|
+
if (migrated > 0) {
|
|
155
|
+
console.log(` [cloud] Stale token cleanup: ${migrated} token(s) migrated to canonical dirs`);
|
|
156
|
+
}
|
|
157
|
+
if (archived > 0) {
|
|
158
|
+
console.log(` [cloud] Stale token cleanup: ${archived} dir(s) archived to tokens/.archive/`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Compute the set of token dir names corresponding to currently-configured
|
|
162
|
+
* accounts. Used to decide which token dirs are "active" vs orphans. */
|
|
163
|
+
function activeTokenDirs() {
|
|
164
|
+
const active = new Set();
|
|
165
|
+
try {
|
|
166
|
+
const accts = loadAccounts();
|
|
167
|
+
for (const a of accts) {
|
|
168
|
+
const u = a.imap?.user || a.email;
|
|
169
|
+
if (u)
|
|
170
|
+
active.add(tokenDirName(u));
|
|
171
|
+
// Also the email address itself — covers accounts where imap.user
|
|
172
|
+
// differs (e.g. iecc.com uses "bobf2" as user but the email is
|
|
173
|
+
// bob@bob.ma; tokens are keyed by the auth identity).
|
|
174
|
+
if (a.email)
|
|
175
|
+
active.add(tokenDirName(a.email));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch { /* accounts.jsonc unreadable — treat all as active (safe default) */ }
|
|
179
|
+
return active;
|
|
180
|
+
}
|
|
58
181
|
/** Get a GDrive-capable token. Prefers the Gmail token (which now includes
|
|
59
182
|
* drive scope) — avoids a second OAuth consent prompt. Falls back to the
|
|
60
183
|
* dedicated GDrive token dir for non-Gmail setups or when Gmail token
|
|
61
184
|
* doesn't have drive scope yet. */
|
|
62
185
|
async function getGoogleDriveToken() {
|
|
186
|
+
// Archive stale token dirs once per process before iteration. Cheap and
|
|
187
|
+
// self-healing — any future stray dir gets stashed out of the loop.
|
|
188
|
+
const active = activeTokenDirs();
|
|
189
|
+
if (active.size > 0)
|
|
190
|
+
archiveStaleTokenDirs(active);
|
|
63
191
|
// Strategy 1: reuse a Gmail token that already has drive scope.
|
|
64
192
|
// Scan the tokens/ dir for any account token with drive in its scope.
|
|
65
193
|
const tokensDir = path.join(SETTINGS_DIR, "tokens");
|
|
66
194
|
if (fs.existsSync(tokensDir)) {
|
|
67
195
|
try {
|
|
68
196
|
for (const entry of fs.readdirSync(tokensDir)) {
|
|
69
|
-
if (entry === "gdrive")
|
|
197
|
+
if (entry === "gdrive" || entry === ".archive")
|
|
70
198
|
continue;
|
|
199
|
+
if (entry.startsWith("."))
|
|
200
|
+
continue;
|
|
201
|
+
// Only consider dirs that match a currently-configured account.
|
|
202
|
+
// Loads the gating logic before the iteration above (`active`)
|
|
203
|
+
// already excluded orphans by moving them — this is belt-and-
|
|
204
|
+
// braces for the case where loadAccounts() returned empty
|
|
205
|
+
// (e.g. accounts.jsonc unreadable) and we skipped archiving.
|
|
206
|
+
if (active.size > 0 && !active.has(entry)) {
|
|
207
|
+
console.log(` [cloud] Skipping non-configured token dir: ${entry}`);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
71
210
|
const tokenPath = path.join(tokensDir, entry, "oauth-token.json");
|
|
72
211
|
if (!fs.existsSync(tokenPath))
|
|
73
212
|
continue;
|
package/index.d.ts
CHANGED
|
@@ -43,6 +43,25 @@ export declare function getStorageInfo(): {
|
|
|
43
43
|
configDir?: string;
|
|
44
44
|
cloudError?: string;
|
|
45
45
|
};
|
|
46
|
+
/** Canonical form of an email address — applies provider-specific
|
|
47
|
+
* equivalences so two superficially-different strings that resolve to the
|
|
48
|
+
* SAME mailbox produce the same canonical key. Today: Gmail (and
|
|
49
|
+
* googlemail.com) ignores dots in the local-part and treats everything
|
|
50
|
+
* from `+` onward as a tag — `Bob.Frankston+work@Gmail.com`,
|
|
51
|
+
* `bobfrankston@gmail.com`, and `BOBFRANKSTON@GOOGLEMAIL.COM` all collapse
|
|
52
|
+
* to `bobfrankston@gmail.com`. Other domains: lowercase only. Used by the
|
|
53
|
+
* OAuth token cache so a Gmail account doesn't end up with two parallel
|
|
54
|
+
* token directories (one with dots, one without) racing each other and
|
|
55
|
+
* triggering full reauth (Bob 2026-05-25).
|
|
56
|
+
*
|
|
57
|
+
* Output is suitable for direct comparison; for filesystem dir names use
|
|
58
|
+
* `tokenDirName(email)` which applies the `[@.]/_` sanitization on top. */
|
|
59
|
+
export declare function canonicalEmail(email: string): string;
|
|
60
|
+
/** Compute the OAuth token-cache directory name for an email address.
|
|
61
|
+
* Canonicalizes first (so Gmail dot/`+tag`/case variants share one dir),
|
|
62
|
+
* then sanitizes `@`/`.` to `_`. Use everywhere a token cache path is
|
|
63
|
+
* derived from an email so all code paths land on the same directory. */
|
|
64
|
+
export declare function tokenDirName(email: string): string;
|
|
46
65
|
/** Fill in provider defaults for an account based on email domain.
|
|
47
66
|
* Exported so mailx-service's leanAccountsJsonc helper can reuse the same
|
|
48
67
|
* canonicalization rules that loadAccounts uses. */
|
package/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"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"}
|
package/index.js
CHANGED
|
@@ -489,6 +489,43 @@ const PROVIDERS = {
|
|
|
489
489
|
smtp: { host: "smtp.mail.me.com", port: 587, tls: true, auth: "password" },
|
|
490
490
|
},
|
|
491
491
|
};
|
|
492
|
+
/** Canonical form of an email address — applies provider-specific
|
|
493
|
+
* equivalences so two superficially-different strings that resolve to the
|
|
494
|
+
* SAME mailbox produce the same canonical key. Today: Gmail (and
|
|
495
|
+
* googlemail.com) ignores dots in the local-part and treats everything
|
|
496
|
+
* from `+` onward as a tag — `Bob.Frankston+work@Gmail.com`,
|
|
497
|
+
* `bobfrankston@gmail.com`, and `BOBFRANKSTON@GOOGLEMAIL.COM` all collapse
|
|
498
|
+
* to `bobfrankston@gmail.com`. Other domains: lowercase only. Used by the
|
|
499
|
+
* OAuth token cache so a Gmail account doesn't end up with two parallel
|
|
500
|
+
* token directories (one with dots, one without) racing each other and
|
|
501
|
+
* triggering full reauth (Bob 2026-05-25).
|
|
502
|
+
*
|
|
503
|
+
* Output is suitable for direct comparison; for filesystem dir names use
|
|
504
|
+
* `tokenDirName(email)` which applies the `[@.]/_` sanitization on top. */
|
|
505
|
+
export function canonicalEmail(email) {
|
|
506
|
+
const s = (email || "").trim().toLowerCase();
|
|
507
|
+
const at = s.indexOf("@");
|
|
508
|
+
if (at < 0)
|
|
509
|
+
return s;
|
|
510
|
+
let local = s.slice(0, at);
|
|
511
|
+
const domain = s.slice(at + 1);
|
|
512
|
+
const isGmail = domain === "gmail.com" || domain === "googlemail.com";
|
|
513
|
+
if (isGmail) {
|
|
514
|
+
const plus = local.indexOf("+");
|
|
515
|
+
if (plus >= 0)
|
|
516
|
+
local = local.slice(0, plus);
|
|
517
|
+
local = local.replace(/\./g, "");
|
|
518
|
+
return `${local}@gmail.com`;
|
|
519
|
+
}
|
|
520
|
+
return `${local}@${domain}`;
|
|
521
|
+
}
|
|
522
|
+
/** Compute the OAuth token-cache directory name for an email address.
|
|
523
|
+
* Canonicalizes first (so Gmail dot/`+tag`/case variants share one dir),
|
|
524
|
+
* then sanitizes `@`/`.` to `_`. Use everywhere a token cache path is
|
|
525
|
+
* derived from an email so all code paths land on the same directory. */
|
|
526
|
+
export function tokenDirName(email) {
|
|
527
|
+
return canonicalEmail(email).replace(/[@.]/g, "_");
|
|
528
|
+
}
|
|
492
529
|
/** Fill in provider defaults for an account based on email domain.
|
|
493
530
|
* Exported so mailx-service's leanAccountsJsonc helper can reuse the same
|
|
494
531
|
* canonicalization rules that loadAccounts uses. */
|