@bobfrankston/mailx-settings 0.1.34 → 0.1.36
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 +17 -10
- package/cloud.d.ts.map +1 -1
- package/cloud.js +161 -11
- package/docs/annotation-registry.md +74 -0
- package/index.d.ts +1 -1
- package/index.d.ts.map +1 -1
- package/index.js +34 -20
- package/package.json +3 -3
package/cloud.d.ts
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
* which prevents conflicts with existing folders of the same name.
|
|
7
7
|
* All machines using the same OAuth client ID share the same folder.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* OneDrive (restored 2026-07-12): Microsoft Graph, PATH-addressed (no folder
|
|
10
|
+
* IDs — `root:/{path}:/content`), so the whole GDrive folder-ID
|
|
11
|
+
* discovery/validation/ownership machinery has no OneDrive counterpart.
|
|
12
|
+
* Needs ~/.rmfmail/microsoft-credentials.json (Azure public-client app;
|
|
13
|
+
* same registration the Outlook Graph mail provider uses — different
|
|
14
|
+
* scopes on the same token infrastructure).
|
|
15
15
|
*
|
|
16
16
|
* Dropbox (removed 2026-04-06): Never implemented — placeholder only.
|
|
17
17
|
* Would need Dropbox OAuth app, token management, and Dropbox API v2 calls.
|
|
@@ -46,7 +46,12 @@ export declare function gDriveValidateCachedFolder(folderId: string): Promise<bo
|
|
|
46
46
|
* orphaning the user's accounts/contacts/etc. Switched to the dotted name
|
|
47
47
|
* matching the actual folder convention. */
|
|
48
48
|
export declare function gDriveFindOrCreateFolder(): Promise<string | null>;
|
|
49
|
-
|
|
49
|
+
/** Fetch the authenticated Microsoft user's profile (setup-flow display). */
|
|
50
|
+
export declare function getMicrosoftProfile(): Promise<{
|
|
51
|
+
name?: string;
|
|
52
|
+
email?: string;
|
|
53
|
+
} | null>;
|
|
54
|
+
export type CloudProvider = "gdrive" | "google" | "onedrive" | "local";
|
|
50
55
|
export interface CloudFile {
|
|
51
56
|
/** Read a file. For gdrive, path is just the filename (folder ID is implicit). */
|
|
52
57
|
read(filePath: string): Promise<string | null>;
|
|
@@ -58,10 +63,12 @@ export interface CloudFile {
|
|
|
58
63
|
exists(filePath: string): Promise<boolean>;
|
|
59
64
|
}
|
|
60
65
|
/**
|
|
61
|
-
* Get a cloud file provider.
|
|
62
|
-
*
|
|
66
|
+
* Get a cloud file provider. The locator is provider-specific: gdrive takes
|
|
67
|
+
* the folder ID; onedrive takes the base PATH under the drive root (e.g.
|
|
68
|
+
* "home/.rmfmail"). Files are stored flat in that folder either way — the
|
|
69
|
+
* CloudFile contract is identical, only the FS API underneath differs.
|
|
63
70
|
*/
|
|
64
|
-
export declare function getCloudProvider(provider: string,
|
|
71
|
+
export declare function getCloudProvider(provider: string, locator?: string): CloudFile | null;
|
|
65
72
|
/** Fetch the authenticated Google user's profile (name + email) via the People API.
|
|
66
73
|
* Caller must supply an OAuth access token whose scopes include `contacts.readonly`
|
|
67
74
|
* or `userinfo.profile`. The Drive token (drive.file) does NOT have the right scope
|
package/cloud.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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;
|
|
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;AA8OD,6EAA6E;AAC7E,wBAAsB,mBAAmB,IAAI,OAAO,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAS7F;AAID,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAC;AAEvE,MAAM,WAAW,SAAS;IACtB,kFAAkF;IAClF,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC/C,8DAA8D;IAC9D,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACrD,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;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAoCrF;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
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
* which prevents conflicts with existing folders of the same name.
|
|
7
7
|
* All machines using the same OAuth client ID share the same folder.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* OneDrive (restored 2026-07-12): Microsoft Graph, PATH-addressed (no folder
|
|
10
|
+
* IDs — `root:/{path}:/content`), so the whole GDrive folder-ID
|
|
11
|
+
* discovery/validation/ownership machinery has no OneDrive counterpart.
|
|
12
|
+
* Needs ~/.rmfmail/microsoft-credentials.json (Azure public-client app;
|
|
13
|
+
* same registration the Outlook Graph mail provider uses — different
|
|
14
|
+
* scopes on the same token infrastructure).
|
|
15
15
|
*
|
|
16
16
|
* Dropbox (removed 2026-04-06): Never implemented — placeholder only.
|
|
17
17
|
* Would need Dropbox OAuth app, token management, and Dropbox API v2 calls.
|
|
@@ -568,14 +568,154 @@ async function gDriveWriteToFolder(folderId, fileName, content) {
|
|
|
568
568
|
}
|
|
569
569
|
}
|
|
570
570
|
}
|
|
571
|
+
// ── OneDrive (Microsoft Graph, path-addressed) ──
|
|
572
|
+
const MS_TOKEN_DIR = path.join(SETTINGS_DIR, "tokens", "microsoft");
|
|
573
|
+
// offline_access is a SCOPE on the Microsoft identity platform (unlike
|
|
574
|
+
// Google's access_type=offline query param) — required for a refresh token.
|
|
575
|
+
const ONEDRIVE_SCOPES = "https://graph.microsoft.com/Files.ReadWrite offline_access";
|
|
576
|
+
const GRAPH_ROOT = "https://graph.microsoft.com/v1.0/me/drive";
|
|
577
|
+
function findMicrosoftCredentials() {
|
|
578
|
+
const local = path.join(SETTINGS_DIR, "microsoft-credentials.json");
|
|
579
|
+
return fs.existsSync(local) ? local : null;
|
|
580
|
+
}
|
|
581
|
+
async function getOneDriveToken() {
|
|
582
|
+
const creds = findMicrosoftCredentials();
|
|
583
|
+
if (!creds) {
|
|
584
|
+
console.error(` [cloud] No Microsoft credentials — expected ${path.join(SETTINGS_DIR, "microsoft-credentials.json")} (Azure portal → App registrations → public/native client)`);
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
try {
|
|
588
|
+
const token = await authenticateOAuth(creds, {
|
|
589
|
+
scope: ONEDRIVE_SCOPES,
|
|
590
|
+
tokenDirectory: MS_TOKEN_DIR,
|
|
591
|
+
tokenFileName: "onedrive-token.json",
|
|
592
|
+
credentialsKey: "installed",
|
|
593
|
+
// Google-ism; Microsoft grants refresh tokens via the
|
|
594
|
+
// offline_access scope above instead.
|
|
595
|
+
includeOfflineAccess: false,
|
|
596
|
+
logFn: (type, message) => {
|
|
597
|
+
const p = " [oauth onedrive]";
|
|
598
|
+
if (type === "error")
|
|
599
|
+
console.error(`${p} ${message}`);
|
|
600
|
+
else
|
|
601
|
+
console.log(`${p} ${message}`);
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
if (!token?.access_token)
|
|
605
|
+
console.error(" [cloud] OneDrive auth: returned null — OAuth likely timed out");
|
|
606
|
+
return token?.access_token || null;
|
|
607
|
+
}
|
|
608
|
+
catch (e) {
|
|
609
|
+
console.error(` [cloud] OneDrive auth FAILED: ${e.message}`);
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
/** Graph item URL for `<basePath>/<fileName>` under the drive root. */
|
|
614
|
+
function graphContentUrl(basePath, fileName) {
|
|
615
|
+
const rel = [basePath.replace(/^\/+|\/+$/g, ""), fileName]
|
|
616
|
+
.filter(Boolean).join("/")
|
|
617
|
+
.split("/").map(encodeURIComponent).join("/");
|
|
618
|
+
return `${GRAPH_ROOT}/root:/${rel}:/content`;
|
|
619
|
+
}
|
|
620
|
+
/** Create every segment of basePath that doesn't exist yet. Graph's
|
|
621
|
+
* PUT :/content does NOT auto-create parent folders — a fresh account's
|
|
622
|
+
* first write would 404. conflictBehavior:fail + tolerating the
|
|
623
|
+
* nameAlreadyExists error makes this an idempotent walk. Once per
|
|
624
|
+
* process per path — after that the folders exist. */
|
|
625
|
+
const _ensuredPaths = new Set();
|
|
626
|
+
async function oneDriveEnsurePath(token, basePath) {
|
|
627
|
+
const clean = basePath.replace(/^\/+|\/+$/g, "");
|
|
628
|
+
if (!clean || _ensuredPaths.has(clean))
|
|
629
|
+
return;
|
|
630
|
+
let parent = "";
|
|
631
|
+
for (const seg of clean.split("/")) {
|
|
632
|
+
const url = parent
|
|
633
|
+
? `${GRAPH_ROOT}/root:/${parent.split("/").map(encodeURIComponent).join("/")}:/children`
|
|
634
|
+
: `${GRAPH_ROOT}/root/children`;
|
|
635
|
+
const r = await fetch(url, {
|
|
636
|
+
method: "POST",
|
|
637
|
+
headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
|
|
638
|
+
body: JSON.stringify({ name: seg, folder: {}, "@microsoft.graph.conflictBehavior": "fail" }),
|
|
639
|
+
});
|
|
640
|
+
// 201 created, 409 nameAlreadyExists — both mean the segment exists now.
|
|
641
|
+
if (!r.ok && r.status !== 409) {
|
|
642
|
+
throw new Error(`OneDrive folder create "${seg}" failed: ${r.status} ${await r.text().catch(() => "")}`);
|
|
643
|
+
}
|
|
644
|
+
parent = parent ? `${parent}/${seg}` : seg;
|
|
645
|
+
}
|
|
646
|
+
_ensuredPaths.add(clean);
|
|
647
|
+
}
|
|
648
|
+
async function oneDriveRead(basePath, fileName) {
|
|
649
|
+
const token = await getOneDriveToken();
|
|
650
|
+
if (!token)
|
|
651
|
+
return null;
|
|
652
|
+
const r = await fetch(graphContentUrl(basePath, fileName), { headers: { "Authorization": `Bearer ${token}` } });
|
|
653
|
+
if (r.status === 404)
|
|
654
|
+
return null;
|
|
655
|
+
if (!r.ok) {
|
|
656
|
+
console.error(` [cloud] OneDrive read ${fileName}: ${r.status}`);
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
return await r.text();
|
|
660
|
+
}
|
|
661
|
+
async function oneDriveReadBinary(basePath, fileName) {
|
|
662
|
+
const token = await getOneDriveToken();
|
|
663
|
+
if (!token)
|
|
664
|
+
return null;
|
|
665
|
+
const r = await fetch(graphContentUrl(basePath, fileName), { headers: { "Authorization": `Bearer ${token}` } });
|
|
666
|
+
if (r.status === 404)
|
|
667
|
+
return null;
|
|
668
|
+
if (!r.ok) {
|
|
669
|
+
console.error(` [cloud] OneDrive readBinary ${fileName}: ${r.status}`);
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
return Buffer.from(await r.arrayBuffer()).toString("base64");
|
|
673
|
+
}
|
|
674
|
+
async function oneDriveWrite(basePath, fileName, content) {
|
|
675
|
+
const token = await getOneDriveToken();
|
|
676
|
+
if (!token)
|
|
677
|
+
throw new Error("OneDrive auth failed — cannot write");
|
|
678
|
+
const put = () => fetch(graphContentUrl(basePath, fileName), {
|
|
679
|
+
method: "PUT",
|
|
680
|
+
headers: { "Authorization": `Bearer ${token}`, "Content-Type": "text/plain" },
|
|
681
|
+
body: content,
|
|
682
|
+
});
|
|
683
|
+
let r = await put();
|
|
684
|
+
if (r.status === 404 || r.status === 409) {
|
|
685
|
+
// Parent path missing (fresh account) — create it and retry once.
|
|
686
|
+
await oneDriveEnsurePath(token, basePath);
|
|
687
|
+
r = await put();
|
|
688
|
+
}
|
|
689
|
+
if (!r.ok)
|
|
690
|
+
throw new Error(`OneDrive write ${fileName} failed: ${r.status} ${await r.text().catch(() => "")}`);
|
|
691
|
+
}
|
|
692
|
+
/** Fetch the authenticated Microsoft user's profile (setup-flow display). */
|
|
693
|
+
export async function getMicrosoftProfile() {
|
|
694
|
+
const token = await getOneDriveToken();
|
|
695
|
+
if (!token)
|
|
696
|
+
return null;
|
|
697
|
+
try {
|
|
698
|
+
const r = await fetch("https://graph.microsoft.com/v1.0/me", { headers: { "Authorization": `Bearer ${token}` } });
|
|
699
|
+
if (!r.ok)
|
|
700
|
+
return null;
|
|
701
|
+
const me = await r.json();
|
|
702
|
+
return { name: me.displayName, email: me.mail || me.userPrincipalName };
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
571
708
|
/**
|
|
572
|
-
* Get a cloud file provider.
|
|
573
|
-
*
|
|
709
|
+
* Get a cloud file provider. The locator is provider-specific: gdrive takes
|
|
710
|
+
* the folder ID; onedrive takes the base PATH under the drive root (e.g.
|
|
711
|
+
* "home/.rmfmail"). Files are stored flat in that folder either way — the
|
|
712
|
+
* CloudFile contract is identical, only the FS API underneath differs.
|
|
574
713
|
*/
|
|
575
|
-
export function getCloudProvider(provider,
|
|
714
|
+
export function getCloudProvider(provider, locator) {
|
|
576
715
|
switch (provider) {
|
|
577
716
|
case "google":
|
|
578
|
-
case "gdrive":
|
|
717
|
+
case "gdrive": {
|
|
718
|
+
const folderId = locator;
|
|
579
719
|
if (!folderId) {
|
|
580
720
|
console.error(" [cloud] gdrive requires a folder ID — run initCloudConfig first");
|
|
581
721
|
return null;
|
|
@@ -586,6 +726,16 @@ export function getCloudProvider(provider, folderId) {
|
|
|
586
726
|
write: (fileName, content) => gDriveWriteToFolder(folderId, fileName, content),
|
|
587
727
|
exists: async (fileName) => (await gDriveReadFromFolder(folderId, fileName)) !== null,
|
|
588
728
|
};
|
|
729
|
+
}
|
|
730
|
+
case "onedrive": {
|
|
731
|
+
const basePath = locator || "home/.rmfmail";
|
|
732
|
+
return {
|
|
733
|
+
read: (fileName) => oneDriveRead(basePath, fileName),
|
|
734
|
+
readBinary: (fileName) => oneDriveReadBinary(basePath, fileName),
|
|
735
|
+
write: (fileName, content) => oneDriveWrite(basePath, fileName, content),
|
|
736
|
+
exists: async (fileName) => (await oneDriveRead(basePath, fileName)) !== null,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
589
739
|
case "local":
|
|
590
740
|
return {
|
|
591
741
|
read: async (p) => { try {
|
|
@@ -604,7 +754,7 @@ export function getCloudProvider(provider, folderId) {
|
|
|
604
754
|
exists: async (p) => fs.existsSync(p),
|
|
605
755
|
};
|
|
606
756
|
default:
|
|
607
|
-
console.error(` [cloud] Provider "${provider}" not supported —
|
|
757
|
+
console.error(` [cloud] Provider "${provider}" not supported — gdrive and onedrive are available`);
|
|
608
758
|
return null;
|
|
609
759
|
}
|
|
610
760
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Annotation registry — companion annotator for imail (design sketch)
|
|
2
|
+
|
|
3
|
+
*Status: DRAFT 2026-07-16 — captured from discussion; Bob thinking it over. TODO item C155.*
|
|
4
|
+
|
|
5
|
+
## Goal
|
|
6
|
+
|
|
7
|
+
A companion app annotates each letter **before** sorting. imail's sorter (and any
|
|
8
|
+
other consumer — rmfmail UI, agents, future tools) reads the annotations and acts.
|
|
9
|
+
The mechanism should be general — not private to imail the way its `ai/` cache is.
|
|
10
|
+
|
|
11
|
+
## Decision so far: keywords on the message + a shared JSON registry
|
|
12
|
+
|
|
13
|
+
Two-layer design, no shared database required:
|
|
14
|
+
|
|
15
|
+
1. **Data plane — tags on the message itself.** IMAP keywords (core RFC 3501,
|
|
16
|
+
not Dovecot-specific: any server advertising `\*` in PERMANENTFLAGS). The
|
|
17
|
+
message carries its own annotations; they survive folder moves; IMAP STORE is
|
|
18
|
+
atomic per message so concurrency is the server's problem, already solved.
|
|
19
|
+
Provider mapping for the same concept:
|
|
20
|
+
| Provider | Mechanism |
|
|
21
|
+
|---|---|
|
|
22
|
+
| Dovecot / Cyrus / Fastmail / iCloud / Yahoo IMAP | user keywords (atoms) |
|
|
23
|
+
| Gmail | API labels (Gmail IMAP ignores custom keywords) |
|
|
24
|
+
| Outlook / Graph | categories |
|
|
25
|
+
| JMAP | keywords (unified with IMAP's by design) |
|
|
26
|
+
|
|
27
|
+
2. **Vocabulary plane — one shared JSON registry** (`annotations.jsonc`, draft
|
|
28
|
+
next to this doc) with MCP-tool-style entries per keyword: `label`,
|
|
29
|
+
`description`, `whenToApply`, `exclusiveGroup`, `setBy`, `sortAction`.
|
|
30
|
+
The registry does triple duty:
|
|
31
|
+
- the AI annotator's prompt is generated from `description` / `whenToApply`
|
|
32
|
+
(same way MCP tool descriptions steer an agent);
|
|
33
|
+
- the sorter maps keyword → action from `sortAction`;
|
|
34
|
+
- UIs (rmfmail chips, Thunderbird tags) get display names from `label`.
|
|
35
|
+
Adding a category = editing one file. No schema migration, no API change.
|
|
36
|
+
|
|
37
|
+
## Constraints / cautions
|
|
38
|
+
|
|
39
|
+
- **Keyword budget**: Dovecot on maildir caps keywords ~26/mailbox (filename
|
|
40
|
+
letters). Registry stays small and curated; `exclusiveGroup` keeps categories
|
|
41
|
+
mutually exclusive instead of accreting.
|
|
42
|
+
- **Atoms only**: no spaces, ASCII, prefix everything `im_` so the vocabulary
|
|
43
|
+
can't collide with `$Junk`, `$Forwarded`, etc.
|
|
44
|
+
- **Tags, not key-value**: scores get bucketed (`im_spam_high`); anything
|
|
45
|
+
non-enumerable (extracted dates, reasoning text, summaries) does NOT fit.
|
|
46
|
+
If rich values are needed later, a sidecar store comes back *only for that
|
|
47
|
+
residue* — the keyword layer stays as the interoperable spine. (imail's
|
|
48
|
+
`.ics` writer is precedent: rich results can also just be files.)
|
|
49
|
+
- **Registry distribution**: share it the way accounts.jsonc is shared — GDrive
|
|
50
|
+
via API with a local cache, never a file on a network path. Slots into
|
|
51
|
+
rmfmail's existing config-editor/watch machinery; imail (self-contained by
|
|
52
|
+
policy) keeps its own cached copy.
|
|
53
|
+
- **Shared SQLite over SMB is ruled out** (WAL needs shared memory; SMB locking
|
|
54
|
+
corrupts). If a shared rich store is ever needed: a small annotation service
|
|
55
|
+
owning WAL SQLite behind HTTP/MCP (the rmfmail-daemon pattern), or libSQL
|
|
56
|
+
embedded replicas for multi-machine.
|
|
57
|
+
|
|
58
|
+
## Fit with existing TODO items
|
|
59
|
+
|
|
60
|
+
- **Q113 / Q113-AI** (per-message metadata shape): this design answers the
|
|
61
|
+
cross-app half — message-attached keywords + registry. A local
|
|
62
|
+
`message_meta` table remains a possible rmfmail-internal cache/residue store.
|
|
63
|
+
- **Q103-AI / C39 / P19** (rules engine + screener): `sortAction` in the
|
|
64
|
+
registry is a candidate rule shape; imail experiments feed this directly.
|
|
65
|
+
|
|
66
|
+
## Open questions for Bob
|
|
67
|
+
|
|
68
|
+
1. Starter vocabulary — which categories actually earn one of the ~26 slots?
|
|
69
|
+
2. Does the annotator mirror keywords → Gmail labels / Graph categories from
|
|
70
|
+
day one, or Dovecot-only first (bobma is the hot path)?
|
|
71
|
+
3. Where does the annotator run — IDLE-driven daemon on new mail (before
|
|
72
|
+
imail's scheduled pass), or a stage inside imail's own loop?
|
|
73
|
+
4. Should rmfmail render `im_*` keywords as chips in the message list (it
|
|
74
|
+
already syncs them in `flags_json` — display-only change)?
|
package/index.d.ts
CHANGED
|
@@ -231,7 +231,7 @@ export declare function initLocalConfig(sharedDir?: string, storePath?: string):
|
|
|
231
231
|
* Finds or creates the app-owned ".rmfmail" folder via Drive API and stores its ID.
|
|
232
232
|
* No mount scanning — API only. Existing settings at other paths (e.g., home/.rmfmail
|
|
233
233
|
* from Desktop sync) must be migrated manually or via config.jsonc importPath. */
|
|
234
|
-
export declare function initCloudConfig(provider?: "gdrive"): Promise<void>;
|
|
234
|
+
export declare function initCloudConfig(provider?: "gdrive" | "onedrive"): Promise<void>;
|
|
235
235
|
declare const DEFAULT_SETTINGS: MailxSettings;
|
|
236
236
|
/** Get historyDays for an account: per-account override > system override > shared default */
|
|
237
237
|
export declare function getHistoryDays(accountId?: string): number;
|
package/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AAyG5G,QAAA,MAAM,SAAS,QAA4E,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"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"}
|
package/index.js
CHANGED
|
@@ -146,6 +146,12 @@ const LOCAL_DIR = path.join(process.env.USERPROFILE || process.env.HOME || ".",
|
|
|
146
146
|
const LOCAL_CONFIG_PATH = path.join(LOCAL_DIR, "config.jsonc");
|
|
147
147
|
const LEGACY_CONFIG_PATH = path.join(LOCAL_DIR, "config.json");
|
|
148
148
|
const DEFAULT_STORE_PATH = path.join(LOCAL_DIR, "mailxstore");
|
|
149
|
+
/** Provider-specific locator for getCloudProvider: gdrive wants the cached
|
|
150
|
+
* folder ID, onedrive wants the base path. One helper so the call sites
|
|
151
|
+
* can't drift apart. */
|
|
152
|
+
function cloudLocator(cfg) {
|
|
153
|
+
return cfg.provider === "onedrive" ? cfg.path : cfg.folderId;
|
|
154
|
+
}
|
|
149
155
|
/** Resolve a path from config — relative to ~/.mailx/, ~ expands to home */
|
|
150
156
|
function resolvePath(p) {
|
|
151
157
|
if (!p)
|
|
@@ -174,7 +180,7 @@ function readLocalConfig() {
|
|
|
174
180
|
/** Resolve provider config to a filesystem path (checks for local Google Drive mount) */
|
|
175
181
|
function resolveProvider(cfg) {
|
|
176
182
|
// Cloud providers use API only — no filesystem mount scanning
|
|
177
|
-
if (cfg.provider === "gdrive" || cfg.provider === "google")
|
|
183
|
+
if (cfg.provider === "gdrive" || cfg.provider === "google" || cfg.provider === "onedrive")
|
|
178
184
|
return undefined;
|
|
179
185
|
if (cfg.provider === "local")
|
|
180
186
|
return resolvePath(cfg.path);
|
|
@@ -248,8 +254,8 @@ export async function cloudRead(filename) {
|
|
|
248
254
|
clearFolderIdInConfig();
|
|
249
255
|
}
|
|
250
256
|
}
|
|
251
|
-
// Ensure we have a folder ID
|
|
252
|
-
if (!pendingCloudConfig.folderId) {
|
|
257
|
+
// Ensure we have a folder ID (gdrive only — onedrive is path-addressed)
|
|
258
|
+
if (pendingCloudConfig.provider !== "onedrive" && !pendingCloudConfig.folderId) {
|
|
253
259
|
pendingCloudConfig.folderId = await gDriveFindOrCreateFolder() || undefined;
|
|
254
260
|
if (pendingCloudConfig.folderId)
|
|
255
261
|
saveFolderIdToConfig(pendingCloudConfig.folderId);
|
|
@@ -258,7 +264,7 @@ export async function cloudRead(filename) {
|
|
|
258
264
|
return null;
|
|
259
265
|
}
|
|
260
266
|
}
|
|
261
|
-
const provider = getCloudProvider(pendingCloudConfig.provider, pendingCloudConfig
|
|
267
|
+
const provider = getCloudProvider(pendingCloudConfig.provider, cloudLocator(pendingCloudConfig));
|
|
262
268
|
if (!provider) {
|
|
263
269
|
setCloudError(`No cloud provider for ${pendingCloudConfig.provider}`, { op: "read", filename });
|
|
264
270
|
return null;
|
|
@@ -296,14 +302,14 @@ export async function cloudRead(filename) {
|
|
|
296
302
|
export async function cloudReadBinary(filename) {
|
|
297
303
|
if (!pendingCloudConfig)
|
|
298
304
|
return null;
|
|
299
|
-
if (!pendingCloudConfig.folderId) {
|
|
305
|
+
if (pendingCloudConfig.provider !== "onedrive" && !pendingCloudConfig.folderId) {
|
|
300
306
|
pendingCloudConfig.folderId = await gDriveFindOrCreateFolder() || undefined;
|
|
301
307
|
if (pendingCloudConfig.folderId)
|
|
302
308
|
saveFolderIdToConfig(pendingCloudConfig.folderId);
|
|
303
309
|
if (!pendingCloudConfig.folderId)
|
|
304
310
|
return null;
|
|
305
311
|
}
|
|
306
|
-
const provider = getCloudProvider(pendingCloudConfig.provider, pendingCloudConfig
|
|
312
|
+
const provider = getCloudProvider(pendingCloudConfig.provider, cloudLocator(pendingCloudConfig));
|
|
307
313
|
if (!provider?.readBinary)
|
|
308
314
|
return null;
|
|
309
315
|
try {
|
|
@@ -327,8 +333,8 @@ export async function cloudWrite(filename, content) {
|
|
|
327
333
|
setCloudError(err, { op: "write", filename });
|
|
328
334
|
throw new Error(err);
|
|
329
335
|
}
|
|
330
|
-
// Ensure we have a folder ID
|
|
331
|
-
if (!pendingCloudConfig.folderId) {
|
|
336
|
+
// Ensure we have a folder ID (gdrive only — onedrive is path-addressed)
|
|
337
|
+
if (pendingCloudConfig.provider !== "onedrive" && !pendingCloudConfig.folderId) {
|
|
332
338
|
pendingCloudConfig.folderId = await gDriveFindOrCreateFolder() || undefined;
|
|
333
339
|
if (pendingCloudConfig.folderId)
|
|
334
340
|
saveFolderIdToConfig(pendingCloudConfig.folderId);
|
|
@@ -338,7 +344,7 @@ export async function cloudWrite(filename, content) {
|
|
|
338
344
|
throw new Error(err);
|
|
339
345
|
}
|
|
340
346
|
}
|
|
341
|
-
const provider = getCloudProvider(pendingCloudConfig.provider, pendingCloudConfig
|
|
347
|
+
const provider = getCloudProvider(pendingCloudConfig.provider, cloudLocator(pendingCloudConfig));
|
|
342
348
|
if (!provider) {
|
|
343
349
|
const err = `No cloud provider for ${pendingCloudConfig.provider}`;
|
|
344
350
|
setCloudError(err, { op: "write", filename });
|
|
@@ -403,8 +409,8 @@ export function getStorageInfo() {
|
|
|
403
409
|
}
|
|
404
410
|
// Provider-based entry — check for API mode (folderId) first, then mount
|
|
405
411
|
const name = (entry.provider === "gdrive" || entry.provider === "google") ? "gdrive" : entry.provider;
|
|
406
|
-
if (entry.folderId) {
|
|
407
|
-
// Has folder ID → API mode (don't scan filesystem for mounts)
|
|
412
|
+
if (entry.folderId || entry.provider === "onedrive") {
|
|
413
|
+
// Has folder ID (or is path-addressed onedrive) → API mode (don't scan filesystem for mounts)
|
|
408
414
|
return { provider: name, mode: "api", cloudPath: entry.path, folderName: entry.path, folderId: entry.folderId, configDir, cloudError: lastCloudError || undefined };
|
|
409
415
|
}
|
|
410
416
|
const resolved = resolveSharedEntry(entry);
|
|
@@ -1385,17 +1391,25 @@ export async function initCloudConfig(provider = "gdrive") {
|
|
|
1385
1391
|
const existing = readLocalConfig();
|
|
1386
1392
|
if (existing.sharedDir)
|
|
1387
1393
|
return; // Already configured
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
+
let sharedDir;
|
|
1395
|
+
if (provider === "onedrive") {
|
|
1396
|
+
// Path-addressed — no folder discovery; the first write creates the
|
|
1397
|
+
// folder chain (oneDriveEnsurePath). Same layout convention as Drive.
|
|
1398
|
+
sharedDir = { provider, path: "home/.rmfmail" };
|
|
1399
|
+
}
|
|
1400
|
+
else {
|
|
1401
|
+
// Find or create the settings folder via Drive API. Stored path is the
|
|
1402
|
+
// canonical post-rebrand location; the folderId is what actually drives
|
|
1403
|
+
// subsequent reads/writes, so even legacy users with the old folder name
|
|
1404
|
+
// work fine since gDriveFindOrCreateFolder resolves whichever exists.
|
|
1405
|
+
const folderId = await gDriveFindOrCreateFolder();
|
|
1406
|
+
sharedDir = { provider, path: "home/.rmfmail", folderId: folderId || undefined };
|
|
1407
|
+
}
|
|
1394
1408
|
const config = { ...existing, sharedDir, storePath: existing.storePath || DEFAULT_STORE_PATH };
|
|
1395
1409
|
fs.mkdirSync(LOCAL_DIR, { recursive: true });
|
|
1396
1410
|
atomicWrite(LOCAL_CONFIG_PATH, config);
|
|
1397
1411
|
pendingCloudConfig = sharedDir;
|
|
1398
|
-
console.log(` Initialized cloud config: ${provider} (folder ID: ${folderId
|
|
1412
|
+
console.log(` Initialized cloud config: ${provider} (${sharedDir.folderId ? `folder ID: ${sharedDir.folderId}` : sharedDir.path})`);
|
|
1399
1413
|
}
|
|
1400
1414
|
const DEFAULT_SETTINGS = {
|
|
1401
1415
|
accounts: [],
|
|
@@ -1437,9 +1451,9 @@ export { DEFAULT_SETTINGS, DEFAULT_ALLOWLIST, DEFAULT_PREFERENCES, DEFAULT_AUTOC
|
|
|
1437
1451
|
* bearing — the app works without them.
|
|
1438
1452
|
*/
|
|
1439
1453
|
export async function deployDocs(appVersion) {
|
|
1440
|
-
if (!pendingCloudConfig
|
|
1454
|
+
if (!pendingCloudConfig || (pendingCloudConfig.provider !== "onedrive" && !pendingCloudConfig.folderId))
|
|
1441
1455
|
return;
|
|
1442
|
-
const provider = getCloudProvider(pendingCloudConfig.provider, pendingCloudConfig
|
|
1456
|
+
const provider = getCloudProvider(pendingCloudConfig.provider, cloudLocator(pendingCloudConfig));
|
|
1443
1457
|
if (!provider)
|
|
1444
1458
|
return;
|
|
1445
1459
|
// Resolve the source docs dir. In dev the workspace has `app/docs/`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-settings",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.36",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"license": "ISC",
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@bobfrankston/mailx-types": "^0.1.
|
|
20
|
+
"@bobfrankston/mailx-types": "^0.1.25",
|
|
21
21
|
"jsonc-parser": "^3.3.1"
|
|
22
22
|
},
|
|
23
23
|
"repository": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
".transformedSnapshot": {
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@bobfrankston/mailx-types": "^0.1.
|
|
36
|
+
"@bobfrankston/mailx-types": "^0.1.25",
|
|
37
37
|
"jsonc-parser": "^3.3.1"
|
|
38
38
|
}
|
|
39
39
|
}
|