@diskd-ai/email-client-mcp 0.1.0
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/LICENSE +155 -0
- package/README.md +110 -0
- package/dist/config/loader.d.ts +12 -0
- package/dist/config/loader.js +42 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +512 -0
- package/dist/config/schema.js +66 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/domain/errors.d.ts +40 -0
- package/dist/domain/errors.js +46 -0
- package/dist/domain/errors.js.map +1 -0
- package/dist/domain/result.d.ts +30 -0
- package/dist/domain/result.js +12 -0
- package/dist/domain/result.js.map +1 -0
- package/dist/imap/fetch.d.ts +44 -0
- package/dist/imap/fetch.js +127 -0
- package/dist/imap/fetch.js.map +1 -0
- package/dist/imap/mapper.d.ts +70 -0
- package/dist/imap/mapper.js +140 -0
- package/dist/imap/mapper.js.map +1 -0
- package/dist/imap/pool.d.ts +26 -0
- package/dist/imap/pool.js +92 -0
- package/dist/imap/pool.js.map +1 -0
- package/dist/imap/virtualFolders.d.ts +11 -0
- package/dist/imap/virtualFolders.js +20 -0
- package/dist/imap/virtualFolders.js.map +1 -0
- package/dist/sdk/diskdClient.d.ts +21 -0
- package/dist/sdk/diskdClient.js +39 -0
- package/dist/sdk/diskdClient.js.map +1 -0
- package/dist/server.d.ts +17 -0
- package/dist/server.js +180 -0
- package/dist/server.js.map +1 -0
- package/dist/store/conventions.d.ts +31 -0
- package/dist/store/conventions.js +53 -0
- package/dist/store/conventions.js.map +1 -0
- package/dist/store/driveStore.d.ts +36 -0
- package/dist/store/driveStore.js +95 -0
- package/dist/store/driveStore.js.map +1 -0
- package/dist/store/payloadTypes.d.ts +62 -0
- package/dist/store/payloadTypes.js +11 -0
- package/dist/store/payloadTypes.js.map +1 -0
- package/dist/sync/backoff.d.ts +18 -0
- package/dist/sync/backoff.js +24 -0
- package/dist/sync/backoff.js.map +1 -0
- package/dist/sync/sync.d.ts +85 -0
- package/dist/sync/sync.js +383 -0
- package/dist/sync/sync.js.map +1 -0
- package/dist/sync/watcher.d.ts +43 -0
- package/dist/sync/watcher.js +141 -0
- package/dist/sync/watcher.js.map +1 -0
- package/dist/tools/bulkAction.d.ts +41 -0
- package/dist/tools/bulkAction.js +82 -0
- package/dist/tools/bulkAction.js.map +1 -0
- package/dist/tools/findMailboxFolder.d.ts +33 -0
- package/dist/tools/findMailboxFolder.js +45 -0
- package/dist/tools/findMailboxFolder.js.map +1 -0
- package/dist/tools/getEmail.d.ts +41 -0
- package/dist/tools/getEmail.js +74 -0
- package/dist/tools/getEmail.js.map +1 -0
- package/dist/tools/getEmails.d.ts +38 -0
- package/dist/tools/getEmails.js +72 -0
- package/dist/tools/getEmails.js.map +1 -0
- package/dist/tools/getWatcherStatus.d.ts +9 -0
- package/dist/tools/getWatcherStatus.js +8 -0
- package/dist/tools/getWatcherStatus.js.map +1 -0
- package/dist/tools/listAccounts.d.ts +19 -0
- package/dist/tools/listAccounts.js +18 -0
- package/dist/tools/listAccounts.js.map +1 -0
- package/dist/tools/listEmails.d.ts +70 -0
- package/dist/tools/listEmails.js +101 -0
- package/dist/tools/listEmails.js.map +1 -0
- package/dist/tools/listMailboxFolder.d.ts +33 -0
- package/dist/tools/listMailboxFolder.js +60 -0
- package/dist/tools/listMailboxFolder.js.map +1 -0
- package/dist/tools/moveEmail.d.ts +36 -0
- package/dist/tools/moveEmail.js +52 -0
- package/dist/tools/moveEmail.js.map +1 -0
- package/dist/tools/registry.d.ts +17 -0
- package/dist/tools/registry.js +64 -0
- package/dist/tools/registry.js.map +1 -0
- package/package.json +46 -0
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* email-client-mcp stdio entry. Composition root.
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities:
|
|
6
|
+
* 1. Parse CLI args (default subcommand: stdio).
|
|
7
|
+
* 2. Load + validate the TOML config.
|
|
8
|
+
* 3. Build @diskd/sdk client and Drive store.
|
|
9
|
+
* 4. Build IMAP pool + sync deps.
|
|
10
|
+
* 5. Register MCP tools, attach stdio transport.
|
|
11
|
+
* 6. Start the watcher AFTER the MCP `initialized` notification so we
|
|
12
|
+
* do not race with the client handshake -- per the email-mcp pattern.
|
|
13
|
+
* 7. Graceful shutdown on SIGINT/SIGTERM.
|
|
14
|
+
*
|
|
15
|
+
* Logs go to stderr only (stdout is reserved for MCP JSON-RPC).
|
|
16
|
+
*/
|
|
17
|
+
export {};
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* email-client-mcp stdio entry. Composition root.
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities:
|
|
6
|
+
* 1. Parse CLI args (default subcommand: stdio).
|
|
7
|
+
* 2. Load + validate the TOML config.
|
|
8
|
+
* 3. Build @diskd/sdk client and Drive store.
|
|
9
|
+
* 4. Build IMAP pool + sync deps.
|
|
10
|
+
* 5. Register MCP tools, attach stdio transport.
|
|
11
|
+
* 6. Start the watcher AFTER the MCP `initialized` notification so we
|
|
12
|
+
* do not race with the client handshake -- per the email-mcp pattern.
|
|
13
|
+
* 7. Graceful shutdown on SIGINT/SIGTERM.
|
|
14
|
+
*
|
|
15
|
+
* Logs go to stderr only (stdout is reserved for MCP JSON-RPC).
|
|
16
|
+
*/
|
|
17
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
18
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
|
+
import { loadConfig } from "./config/loader.js";
|
|
20
|
+
import { errorMessage } from "./domain/errors.js";
|
|
21
|
+
import { Err, Ok } from "./domain/result.js";
|
|
22
|
+
import { fetchEnvelopesUidRange, fetchUidRange, folderStatus, withMailboxLock, } from "./imap/fetch.js";
|
|
23
|
+
import { buildImapPool } from "./imap/pool.js";
|
|
24
|
+
import { buildDiskd } from "./sdk/diskdClient.js";
|
|
25
|
+
import { buildDriveStore } from "./store/driveStore.js";
|
|
26
|
+
import { buildWatcher } from "./sync/watcher.js";
|
|
27
|
+
import { registerTools } from "./tools/registry.js";
|
|
28
|
+
const log = (msg, extra) => {
|
|
29
|
+
const payload = extra === undefined ? msg : `${msg} ${JSON.stringify(extra)}`;
|
|
30
|
+
process.stderr.write(`[email-client-mcp] ${payload}\n`);
|
|
31
|
+
};
|
|
32
|
+
const parseSubcommand = (argv) => {
|
|
33
|
+
// npx will pass the bin name as argv[1]; the user-supplied subcommand
|
|
34
|
+
// is at argv[2] (or absent).
|
|
35
|
+
const sub = argv[2];
|
|
36
|
+
if (sub === undefined || sub === "stdio")
|
|
37
|
+
return "stdio";
|
|
38
|
+
return "unknown";
|
|
39
|
+
};
|
|
40
|
+
const main = async () => {
|
|
41
|
+
const sub = parseSubcommand(process.argv);
|
|
42
|
+
if (sub === "unknown") {
|
|
43
|
+
log("usage: email-client-mcp [stdio]");
|
|
44
|
+
process.exit(2);
|
|
45
|
+
}
|
|
46
|
+
const configPath = process.env.EMAIL_CLIENT_MCP_CONFIG;
|
|
47
|
+
const cfg = await loadConfig(configPath);
|
|
48
|
+
if (cfg.tag === "Err") {
|
|
49
|
+
log("config error", { error: errorMessage(cfg.error) });
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
const diskd = buildDiskd(cfg.value.sdk, process.env.MCP_HUB_WORKSPACE_ID);
|
|
53
|
+
if (diskd.tag === "Err") {
|
|
54
|
+
log("sdk init failed", { error: errorMessage(diskd.error) });
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
log("sdk ready", { workspaceId: diskd.value.workspaceId });
|
|
58
|
+
const driveStore = buildDriveStore(diskd.value.messagesStore);
|
|
59
|
+
const pool = buildImapPool(cfg.value.accounts);
|
|
60
|
+
const syncDeps = {
|
|
61
|
+
drive: driveStore,
|
|
62
|
+
imap: {
|
|
63
|
+
listFolders: async (accountId) => {
|
|
64
|
+
const c = await pool.forAccount(accountId);
|
|
65
|
+
if (c.tag === "Err")
|
|
66
|
+
return c;
|
|
67
|
+
try {
|
|
68
|
+
const list = await c.value.list();
|
|
69
|
+
return Ok(list.map((f) => ({
|
|
70
|
+
path: f.path,
|
|
71
|
+
specialUse: (f.specialUse ?? null),
|
|
72
|
+
})));
|
|
73
|
+
}
|
|
74
|
+
catch (cause) {
|
|
75
|
+
return Err({
|
|
76
|
+
kind: "ImapError",
|
|
77
|
+
accountId,
|
|
78
|
+
message: `list failed: ${cause?.message ?? String(cause)}`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
folderStatus: async (accountId, path) => {
|
|
83
|
+
const c = await pool.forAccount(accountId);
|
|
84
|
+
if (c.tag === "Err")
|
|
85
|
+
return c;
|
|
86
|
+
try {
|
|
87
|
+
const s = await folderStatus(c.value, path);
|
|
88
|
+
return Ok(s);
|
|
89
|
+
}
|
|
90
|
+
catch (cause) {
|
|
91
|
+
return Err({
|
|
92
|
+
kind: "ImapError",
|
|
93
|
+
accountId,
|
|
94
|
+
message: `status ${path}: ${cause?.message ?? String(cause)}`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
fetchRange: async function* (accountId, path, fromUid, toUid) {
|
|
99
|
+
const c = await pool.forAccount(accountId);
|
|
100
|
+
if (c.tag === "Err")
|
|
101
|
+
throw new Error(errorMessage(c.error));
|
|
102
|
+
// Buffer under-lock, yield after release. Batch sizes are
|
|
103
|
+
// bounded (sync uses 50) so the cost is acceptable.
|
|
104
|
+
const buf = await withMailboxLock(c.value, path, async () => {
|
|
105
|
+
const out = [];
|
|
106
|
+
for await (const m of fetchUidRange(c.value, fromUid, toUid))
|
|
107
|
+
out.push(m);
|
|
108
|
+
return out;
|
|
109
|
+
});
|
|
110
|
+
for (const m of buf)
|
|
111
|
+
yield m;
|
|
112
|
+
},
|
|
113
|
+
fetchEnvelopesRange: async function* (accountId, path, fromUid, toUid) {
|
|
114
|
+
const c = await pool.forAccount(accountId);
|
|
115
|
+
if (c.tag === "Err")
|
|
116
|
+
throw new Error(errorMessage(c.error));
|
|
117
|
+
const buf = await withMailboxLock(c.value, path, async () => {
|
|
118
|
+
const out = [];
|
|
119
|
+
for await (const m of fetchEnvelopesUidRange(c.value, fromUid, toUid))
|
|
120
|
+
out.push(m);
|
|
121
|
+
return out;
|
|
122
|
+
});
|
|
123
|
+
for (const m of buf)
|
|
124
|
+
yield m;
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
now: () => new Date(),
|
|
128
|
+
};
|
|
129
|
+
const watcher = buildWatcher(syncDeps, cfg.value.accounts, cfg.value.watcher, log);
|
|
130
|
+
const server = new McpServer({
|
|
131
|
+
name: "email-client-mcp",
|
|
132
|
+
version: "0.1.0",
|
|
133
|
+
});
|
|
134
|
+
registerTools(server, {
|
|
135
|
+
accounts: cfg.value.accounts,
|
|
136
|
+
imapPool: pool,
|
|
137
|
+
watcher,
|
|
138
|
+
});
|
|
139
|
+
const transport = new StdioServerTransport();
|
|
140
|
+
await server.connect(transport);
|
|
141
|
+
log("mcp ready", { accounts: cfg.value.accounts.length });
|
|
142
|
+
// Start watcher after MCP transport is ready. The interval fires
|
|
143
|
+
// immediately on the first tick (see watcher.start), then on every
|
|
144
|
+
// clamped interval thereafter.
|
|
145
|
+
if (cfg.value.watcher.enabled) {
|
|
146
|
+
watcher.start();
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
log("watcher.disabled-by-config");
|
|
150
|
+
}
|
|
151
|
+
const shutdown = async (signal) => {
|
|
152
|
+
log("shutdown", { signal });
|
|
153
|
+
try {
|
|
154
|
+
await watcher.stop();
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
log("watcher stop error", { error: String(e) });
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
await pool.closeAll();
|
|
161
|
+
}
|
|
162
|
+
catch (e) {
|
|
163
|
+
log("pool close error", { error: String(e) });
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
await server.close();
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
log("server close error", { error: String(e) });
|
|
170
|
+
}
|
|
171
|
+
process.exit(0);
|
|
172
|
+
};
|
|
173
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
174
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
175
|
+
};
|
|
176
|
+
main().catch((err) => {
|
|
177
|
+
log("fatal", { error: err?.message ?? String(err) });
|
|
178
|
+
process.exit(1);
|
|
179
|
+
});
|
|
180
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers that map account/folder/UID identity onto the
|
|
3
|
+
* messagesStore namespace. Concentrating these so a future change
|
|
4
|
+
* (e.g. a different mailbox-id scheme) only edits one file.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Sanitize an account id for use as a Drive mailbox id (slug subset
|
|
8
|
+
* `[a-z0-9-]{1,64}`). Lowercase, replace anything else with `-`, collapse
|
|
9
|
+
* runs, trim leading/trailing dashes, truncate. Empty input -> 'mailbox'.
|
|
10
|
+
*/
|
|
11
|
+
export declare const sanitizeMailboxId: (raw: string) => string;
|
|
12
|
+
/**
|
|
13
|
+
* Caller-supplied folderId for a given IMAP mailbox path. We keep the
|
|
14
|
+
* IMAP path verbatim so `find_mailbox_folder` can round-trip it.
|
|
15
|
+
*/
|
|
16
|
+
export declare const folderIdFromImapPath: (imapPath: string) => string;
|
|
17
|
+
/**
|
|
18
|
+
* Build the messagesStore externalId for a message. `${uidValidity}:${uid}`
|
|
19
|
+
* makes the id stable across sync runs and disjoint across UIDVALIDITY
|
|
20
|
+
* generations.
|
|
21
|
+
*/
|
|
22
|
+
export declare const externalIdFor: (uidValidity: number, uid: number) => string;
|
|
23
|
+
/**
|
|
24
|
+
* Parse an externalId back into its components. Returns null on a
|
|
25
|
+
* malformed input -- callers should treat that as "not produced by us".
|
|
26
|
+
*/
|
|
27
|
+
export declare const parseExternalId: (externalId: string) => {
|
|
28
|
+
uidValidity: number;
|
|
29
|
+
uid: number;
|
|
30
|
+
} | null;
|
|
31
|
+
export declare const isValidMailboxId: (id: string) => boolean;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers that map account/folder/UID identity onto the
|
|
3
|
+
* messagesStore namespace. Concentrating these so a future change
|
|
4
|
+
* (e.g. a different mailbox-id scheme) only edits one file.
|
|
5
|
+
*/
|
|
6
|
+
const MAILBOX_ID_RE = /^[a-z0-9-]{1,64}$/;
|
|
7
|
+
/**
|
|
8
|
+
* Sanitize an account id for use as a Drive mailbox id (slug subset
|
|
9
|
+
* `[a-z0-9-]{1,64}`). Lowercase, replace anything else with `-`, collapse
|
|
10
|
+
* runs, trim leading/trailing dashes, truncate. Empty input -> 'mailbox'.
|
|
11
|
+
*/
|
|
12
|
+
export const sanitizeMailboxId = (raw) => {
|
|
13
|
+
const lowered = raw
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
16
|
+
.replace(/-+/g, "-")
|
|
17
|
+
.replace(/^-|-$/g, "");
|
|
18
|
+
const truncated = lowered.slice(0, 64);
|
|
19
|
+
return truncated.length === 0 ? "mailbox" : truncated;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Caller-supplied folderId for a given IMAP mailbox path. We keep the
|
|
23
|
+
* IMAP path verbatim so `find_mailbox_folder` can round-trip it.
|
|
24
|
+
*/
|
|
25
|
+
export const folderIdFromImapPath = (imapPath) => imapPath;
|
|
26
|
+
/**
|
|
27
|
+
* Build the messagesStore externalId for a message. `${uidValidity}:${uid}`
|
|
28
|
+
* makes the id stable across sync runs and disjoint across UIDVALIDITY
|
|
29
|
+
* generations.
|
|
30
|
+
*/
|
|
31
|
+
export const externalIdFor = (uidValidity, uid) => `${uidValidity}:${uid}`;
|
|
32
|
+
/**
|
|
33
|
+
* Parse an externalId back into its components. Returns null on a
|
|
34
|
+
* malformed input -- callers should treat that as "not produced by us".
|
|
35
|
+
*/
|
|
36
|
+
export const parseExternalId = (externalId) => {
|
|
37
|
+
const parts = externalId.split(":");
|
|
38
|
+
if (parts.length !== 2)
|
|
39
|
+
return null;
|
|
40
|
+
const uvRaw = parts[0];
|
|
41
|
+
const uidRaw = parts[1];
|
|
42
|
+
if (uvRaw === undefined || uidRaw === undefined)
|
|
43
|
+
return null;
|
|
44
|
+
const uv = Number.parseInt(uvRaw, 10);
|
|
45
|
+
const uid = Number.parseInt(uidRaw, 10);
|
|
46
|
+
if (!Number.isFinite(uv) || !Number.isFinite(uid))
|
|
47
|
+
return null;
|
|
48
|
+
if (uv < 0 || uid < 0)
|
|
49
|
+
return null;
|
|
50
|
+
return { uidValidity: uv, uid };
|
|
51
|
+
};
|
|
52
|
+
export const isValidMailboxId = (id) => MAILBOX_ID_RE.test(id);
|
|
53
|
+
//# sourceMappingURL=conventions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conventions.js","sourceRoot":"","sources":["../../src/store/conventions.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,aAAa,GAAG,mBAAmB,CAAC;AAE1C;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,GAAW,EAAU,EAAE;IACvD,MAAM,OAAO,GAAG,GAAG;SAChB,WAAW,EAAE;SACb,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC;SAC5B,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACzB,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACvC,OAAO,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;AACxD,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,QAAgB,EAAU,EAAE,CAAC,QAAQ,CAAC;AAE3E;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,WAAmB,EAAE,GAAW,EAAU,EAAE,CAAC,GAAG,WAAW,IAAI,GAAG,EAAE,CAAC;AAEnG;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAC7B,UAAkB,EAC2B,EAAE;IAC/C,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACpC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IACvB,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IACxB,IAAI,KAAK,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAC7D,MAAM,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACtC,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACxC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/D,IAAI,EAAE,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,OAAO,EAAE,WAAW,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC;AAClC,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,EAAU,EAAW,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter over `diskd.os.messagesStore`. Wraps each call in
|
|
3
|
+
* try/catch -> typed `DriveError` so the sync core stays pure-ish
|
|
4
|
+
* (Result-returning) and we can compose retries with backoff.
|
|
5
|
+
*
|
|
6
|
+
* One mailbox per account: ensureMailbox is idempotent; the underlying
|
|
7
|
+
* `createMailbox` errors (e.g. ALREADY_EXISTS) are absorbed -- callers
|
|
8
|
+
* do not need to track creation state.
|
|
9
|
+
*/
|
|
10
|
+
import type { diskd as DiskdNs } from "@diskd/sdk";
|
|
11
|
+
import { type DriveError } from "../domain/errors.js";
|
|
12
|
+
import { type Result } from "../domain/result.js";
|
|
13
|
+
import type { StoredEmailPayload, SyncState } from "./payloadTypes.js";
|
|
14
|
+
type MessagesStore = ReturnType<typeof DiskdNs.os.messagesStore>;
|
|
15
|
+
export type DriveStore = {
|
|
16
|
+
/** Ensure mailbox exists and SQLite schema is bootstrapped. */
|
|
17
|
+
readonly ensureMailbox: (mailboxId: string, displayName: string) => Promise<Result<DriveError, void>>;
|
|
18
|
+
readonly listMailboxes: () => Promise<Result<DriveError, readonly string[]>>;
|
|
19
|
+
readonly upsertFolder: (mailboxId: string, folderId: string, displayName: string, metadata: SyncState) => Promise<Result<DriveError, void>>;
|
|
20
|
+
readonly getFolder: (mailboxId: string, folderId: string) => Promise<Result<DriveError, {
|
|
21
|
+
metadata: Readonly<Record<string, unknown>>;
|
|
22
|
+
} | null>>;
|
|
23
|
+
readonly listFolders: (mailboxId: string) => Promise<Result<DriveError, readonly {
|
|
24
|
+
folderId: string;
|
|
25
|
+
messageCount: number;
|
|
26
|
+
}[]>>;
|
|
27
|
+
readonly deleteFolder: (mailboxId: string, folderId: string) => Promise<Result<DriveError, {
|
|
28
|
+
deletedMessageCount: number;
|
|
29
|
+
}>>;
|
|
30
|
+
readonly upsertMessages: (mailboxId: string, folderId: string, payloads: readonly StoredEmailPayload[], externalIds: readonly string[]) => Promise<Result<DriveError, {
|
|
31
|
+
inserted: number;
|
|
32
|
+
updated: number;
|
|
33
|
+
}>>;
|
|
34
|
+
};
|
|
35
|
+
export declare const buildDriveStore: (store: MessagesStore) => DriveStore;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter over `diskd.os.messagesStore`. Wraps each call in
|
|
3
|
+
* try/catch -> typed `DriveError` so the sync core stays pure-ish
|
|
4
|
+
* (Result-returning) and we can compose retries with backoff.
|
|
5
|
+
*
|
|
6
|
+
* One mailbox per account: ensureMailbox is idempotent; the underlying
|
|
7
|
+
* `createMailbox` errors (e.g. ALREADY_EXISTS) are absorbed -- callers
|
|
8
|
+
* do not need to track creation state.
|
|
9
|
+
*/
|
|
10
|
+
import { driveError } from "../domain/errors.js";
|
|
11
|
+
import { Err, Ok } from "../domain/result.js";
|
|
12
|
+
import { isValidMailboxId } from "./conventions.js";
|
|
13
|
+
const wrap = async (what, fn) => {
|
|
14
|
+
try {
|
|
15
|
+
return Ok(await fn());
|
|
16
|
+
}
|
|
17
|
+
catch (cause) {
|
|
18
|
+
return Err(driveError(`${what} failed: ${cause?.message ?? String(cause)}`, cause));
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const folderScoped = (store, mailboxId, folderId) => store.mailbox({ mailboxId }).folder({ folderId });
|
|
22
|
+
export const buildDriveStore = (store) => ({
|
|
23
|
+
async ensureMailbox(mailboxId, displayName) {
|
|
24
|
+
if (!isValidMailboxId(mailboxId)) {
|
|
25
|
+
return Err(driveError(`mailboxId '${mailboxId}' is not a valid slug`));
|
|
26
|
+
}
|
|
27
|
+
const list = await wrap("listMailboxes", () => store.listMailboxes());
|
|
28
|
+
if (list.tag === "Err")
|
|
29
|
+
return list;
|
|
30
|
+
const exists = list.value.some((m) => m.mailboxId === mailboxId);
|
|
31
|
+
if (!exists) {
|
|
32
|
+
const create = await wrap("createMailbox", () => store.createMailbox({ mailboxId, displayName }));
|
|
33
|
+
if (create.tag === "Err")
|
|
34
|
+
return create;
|
|
35
|
+
}
|
|
36
|
+
const init = await wrap("mailbox.init", () => store.mailbox({ mailboxId }).init());
|
|
37
|
+
if (init.tag === "Err")
|
|
38
|
+
return init;
|
|
39
|
+
return Ok(undefined);
|
|
40
|
+
},
|
|
41
|
+
async listMailboxes() {
|
|
42
|
+
const r = await wrap("listMailboxes", () => store.listMailboxes());
|
|
43
|
+
if (r.tag === "Err")
|
|
44
|
+
return r;
|
|
45
|
+
return Ok(r.value.map((m) => m.mailboxId));
|
|
46
|
+
},
|
|
47
|
+
async upsertFolder(mailboxId, folderId, displayName, metadata) {
|
|
48
|
+
const r = await wrap("upsertFolder", () => store.mailbox({ mailboxId }).upsertFolder({
|
|
49
|
+
folderId,
|
|
50
|
+
displayName,
|
|
51
|
+
metadata: metadata,
|
|
52
|
+
}));
|
|
53
|
+
if (r.tag === "Err")
|
|
54
|
+
return r;
|
|
55
|
+
return Ok(undefined);
|
|
56
|
+
},
|
|
57
|
+
async getFolder(mailboxId, folderId) {
|
|
58
|
+
try {
|
|
59
|
+
const summary = await folderScoped(store, mailboxId, folderId).get();
|
|
60
|
+
return Ok({ metadata: summary.metadata });
|
|
61
|
+
}
|
|
62
|
+
catch (cause) {
|
|
63
|
+
const msg = cause?.message ?? String(cause);
|
|
64
|
+
if (/not found|FOLDER_NOT_FOUND|404/i.test(msg)) {
|
|
65
|
+
return Ok(null);
|
|
66
|
+
}
|
|
67
|
+
return Err(driveError(`folder.get failed: ${msg}`, cause));
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
async listFolders(mailboxId) {
|
|
71
|
+
const r = await wrap("listFolders", () => store.mailbox({ mailboxId }).listFolders());
|
|
72
|
+
if (r.tag === "Err")
|
|
73
|
+
return r;
|
|
74
|
+
return Ok(r.value.map((f) => ({ folderId: f.folderId, messageCount: f.messageCount })));
|
|
75
|
+
},
|
|
76
|
+
async deleteFolder(mailboxId, folderId) {
|
|
77
|
+
const r = await wrap("folder.delete", () => folderScoped(store, mailboxId, folderId).delete());
|
|
78
|
+
if (r.tag === "Err")
|
|
79
|
+
return r;
|
|
80
|
+
return Ok({ deletedMessageCount: r.value.deletedMessageCount });
|
|
81
|
+
},
|
|
82
|
+
async upsertMessages(mailboxId, folderId, payloads, externalIds) {
|
|
83
|
+
if (payloads.length === 0)
|
|
84
|
+
return Ok({ inserted: 0, updated: 0 });
|
|
85
|
+
const items = payloads.map((p, i) => ({
|
|
86
|
+
externalId: externalIds[i],
|
|
87
|
+
payload: p,
|
|
88
|
+
}));
|
|
89
|
+
const r = await wrap("folder.upsertBatch", () => folderScoped(store, mailboxId, folderId).upsertBatch({ items, autoCommit: false }));
|
|
90
|
+
if (r.tag === "Err")
|
|
91
|
+
return r;
|
|
92
|
+
return Ok({ inserted: r.value.inserted, updated: r.value.updated });
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
//# sourceMappingURL=driveStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"driveStore.js","sourceRoot":"","sources":["../../src/store/driveStore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drive `messagesStore` payload shape -- opaque JSON to Drive but owned
|
|
3
|
+
* by this server. Keep stable: external consumers (search, agents)
|
|
4
|
+
* can rely on these fields landing in the store mirror.
|
|
5
|
+
*
|
|
6
|
+
* `externalId` (the messagesStore primary key for a message) is built
|
|
7
|
+
* from `${uidValidity}:${uid}` so a UIDVALIDITY rollover never
|
|
8
|
+
* collides with the previous generation.
|
|
9
|
+
*/
|
|
10
|
+
export type EmailAddress = {
|
|
11
|
+
readonly name: string | null;
|
|
12
|
+
readonly address: string;
|
|
13
|
+
};
|
|
14
|
+
export type StoredAttachment = {
|
|
15
|
+
readonly filename: string;
|
|
16
|
+
readonly contentType: string;
|
|
17
|
+
readonly sizeBytes: number;
|
|
18
|
+
readonly partId: string;
|
|
19
|
+
};
|
|
20
|
+
export type StoredEmailPayload = {
|
|
21
|
+
readonly accountId: string;
|
|
22
|
+
readonly mailbox: string;
|
|
23
|
+
readonly uid: number;
|
|
24
|
+
readonly uidValidity: number;
|
|
25
|
+
readonly messageId: string | null;
|
|
26
|
+
readonly inReplyTo: string | null;
|
|
27
|
+
readonly references: readonly string[];
|
|
28
|
+
readonly from: EmailAddress | null;
|
|
29
|
+
readonly to: readonly EmailAddress[];
|
|
30
|
+
readonly cc: readonly EmailAddress[];
|
|
31
|
+
readonly subject: string;
|
|
32
|
+
readonly date: string;
|
|
33
|
+
readonly flags: readonly string[];
|
|
34
|
+
readonly labels: readonly string[];
|
|
35
|
+
readonly hasAttachments: boolean;
|
|
36
|
+
readonly attachments: readonly StoredAttachment[];
|
|
37
|
+
readonly snippet: string;
|
|
38
|
+
readonly bodyText: string | null;
|
|
39
|
+
readonly bodyHtml: string | null;
|
|
40
|
+
readonly truncated: boolean;
|
|
41
|
+
readonly fetchedAt: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Folder metadata used by the watcher to drive idempotent, restart-safe
|
|
45
|
+
* sync. Stored as opaque JSON via `mailbox.folder(...).upsert({metadata})`.
|
|
46
|
+
*
|
|
47
|
+
* - `uidValidity`: snapshot of last seen UIDVALIDITY; mismatch triggers
|
|
48
|
+
* a full folder drop+resync.
|
|
49
|
+
* - `uidNext`: snapshot of last seen UIDNEXT; the head of the sync window.
|
|
50
|
+
* - `lastSyncedUid`: highest UID confirmed written via successful upsert.
|
|
51
|
+
* `runSyncOnce` advances this only after a batch upsert returns Ok.
|
|
52
|
+
* - `lastSyncStartedAt` / `lastSyncFinishedAt` / `lastSyncError`:
|
|
53
|
+
* diagnostic snapshot exposed via `get_watcher_status`.
|
|
54
|
+
*/
|
|
55
|
+
export type SyncState = {
|
|
56
|
+
readonly uidValidity: number;
|
|
57
|
+
readonly uidNext: number;
|
|
58
|
+
readonly lastSyncedUid: number;
|
|
59
|
+
readonly lastSyncStartedAt: string | null;
|
|
60
|
+
readonly lastSyncFinishedAt: string | null;
|
|
61
|
+
readonly lastSyncError: string | null;
|
|
62
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drive `messagesStore` payload shape -- opaque JSON to Drive but owned
|
|
3
|
+
* by this server. Keep stable: external consumers (search, agents)
|
|
4
|
+
* can rely on these fields landing in the store mirror.
|
|
5
|
+
*
|
|
6
|
+
* `externalId` (the messagesStore primary key for a message) is built
|
|
7
|
+
* from `${uidValidity}:${uid}` so a UIDVALIDITY rollover never
|
|
8
|
+
* collides with the previous generation.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=payloadTypes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payloadTypes.js","sourceRoot":"","sources":["../../src/store/payloadTypes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure exponential-backoff helper. `nextDelay(prev)` returns the next
|
|
3
|
+
* delay in ms, capped at MAX_BACKOFF_MS. The watcher uses this when an
|
|
4
|
+
* IMAP or Drive operation fails inside a tick, so each retry waits a
|
|
5
|
+
* little longer up to the cap.
|
|
6
|
+
*
|
|
7
|
+
* Pure; no setTimeout. Callers compose the delay with their own timers.
|
|
8
|
+
*/
|
|
9
|
+
export declare const INITIAL_BACKOFF_MS = 1000;
|
|
10
|
+
export declare const MAX_BACKOFF_MS = 60000;
|
|
11
|
+
export declare const BACKOFF_FACTOR = 2;
|
|
12
|
+
export declare const nextBackoff: (prevMs: number | null) => number;
|
|
13
|
+
/**
|
|
14
|
+
* Clamp a configured tick interval to the spec floor of 60s. Used by
|
|
15
|
+
* the watcher; not the backoff helper itself.
|
|
16
|
+
*/
|
|
17
|
+
export declare const MIN_TICK_INTERVAL_MS = 60000;
|
|
18
|
+
export declare const clampTickInterval: (ms: number) => number;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure exponential-backoff helper. `nextDelay(prev)` returns the next
|
|
3
|
+
* delay in ms, capped at MAX_BACKOFF_MS. The watcher uses this when an
|
|
4
|
+
* IMAP or Drive operation fails inside a tick, so each retry waits a
|
|
5
|
+
* little longer up to the cap.
|
|
6
|
+
*
|
|
7
|
+
* Pure; no setTimeout. Callers compose the delay with their own timers.
|
|
8
|
+
*/
|
|
9
|
+
export const INITIAL_BACKOFF_MS = 1_000;
|
|
10
|
+
export const MAX_BACKOFF_MS = 60_000;
|
|
11
|
+
export const BACKOFF_FACTOR = 2;
|
|
12
|
+
export const nextBackoff = (prevMs) => {
|
|
13
|
+
if (prevMs === null)
|
|
14
|
+
return INITIAL_BACKOFF_MS;
|
|
15
|
+
const next = prevMs * BACKOFF_FACTOR;
|
|
16
|
+
return next > MAX_BACKOFF_MS ? MAX_BACKOFF_MS : next;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Clamp a configured tick interval to the spec floor of 60s. Used by
|
|
20
|
+
* the watcher; not the backoff helper itself.
|
|
21
|
+
*/
|
|
22
|
+
export const MIN_TICK_INTERVAL_MS = 60_000;
|
|
23
|
+
export const clampTickInterval = (ms) => ms < MIN_TICK_INTERVAL_MS ? MIN_TICK_INTERVAL_MS : ms;
|
|
24
|
+
//# sourceMappingURL=backoff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backoff.js","sourceRoot":"","sources":["../../src/sync/backoff.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,CAAC;AACxC,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC;AACrC,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC;AAEhC,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAAqB,EAAU,EAAE;IAC3D,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,kBAAkB,CAAC;IAC/C,MAAM,IAAI,GAAG,MAAM,GAAG,cAAc,CAAC;IACrC,OAAO,IAAI,GAAG,cAAc,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC;AACvD,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAC3C,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,EAAU,EAAU,EAAE,CACtD,EAAE,GAAG,oBAAoB,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,EAAE,CAAC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reliability core. `runSyncOnce(account)` performs one watcher
|
|
3
|
+
* pass for one account.
|
|
4
|
+
*
|
|
5
|
+
* Invariants enforced here:
|
|
6
|
+
* 1. `lastSyncedUid` is advanced ONLY after a successful upsertBatch.
|
|
7
|
+
* A crash mid-tick or a Drive 5xx leaves the checkpoint untouched,
|
|
8
|
+
* so the next tick re-fetches the unprocessed UIDs (idempotent
|
|
9
|
+
* because externalId is `${UIDVALIDITY}:${UID}`).
|
|
10
|
+
* 2. UIDVALIDITY mismatch -> drop+recreate folder, resync from UID 1.
|
|
11
|
+
* 3. Folder metadata is the source of truth across restarts. The
|
|
12
|
+
* watcher never relies on in-memory state to resume.
|
|
13
|
+
* 4. Folders deleted in IMAP are pruned from Drive at the end of a tick.
|
|
14
|
+
* 5. A bounded sliding-window flag reconciliation catches drift on
|
|
15
|
+
* already-synced UIDs without re-downloading bodies.
|
|
16
|
+
*
|
|
17
|
+
* The function takes injected dependencies (drive store, IMAP client
|
|
18
|
+
* factory, mapper) so unit tests can drive it without real I/O.
|
|
19
|
+
*/
|
|
20
|
+
import { type Account, type WatcherSettings } from "../config/schema.js";
|
|
21
|
+
import { type AppError, type ImapError } from "../domain/errors.js";
|
|
22
|
+
import { type Result } from "../domain/result.js";
|
|
23
|
+
import { type FetchedMessageLike } from "../imap/mapper.js";
|
|
24
|
+
import type { StoredEmailPayload, SyncState } from "../store/payloadTypes.js";
|
|
25
|
+
export type SyncDeps = {
|
|
26
|
+
readonly drive: {
|
|
27
|
+
readonly ensureMailbox: (mailboxId: string, displayName: string) => Promise<Result<AppError, void>>;
|
|
28
|
+
readonly upsertFolder: (mailboxId: string, folderId: string, displayName: string, metadata: SyncState) => Promise<Result<AppError, void>>;
|
|
29
|
+
readonly getFolder: (mailboxId: string, folderId: string) => Promise<Result<AppError, {
|
|
30
|
+
metadata: Readonly<Record<string, unknown>>;
|
|
31
|
+
} | null>>;
|
|
32
|
+
readonly listFolders: (mailboxId: string) => Promise<Result<AppError, readonly {
|
|
33
|
+
folderId: string;
|
|
34
|
+
messageCount: number;
|
|
35
|
+
}[]>>;
|
|
36
|
+
readonly deleteFolder: (mailboxId: string, folderId: string) => Promise<Result<AppError, {
|
|
37
|
+
deletedMessageCount: number;
|
|
38
|
+
}>>;
|
|
39
|
+
readonly upsertMessages: (mailboxId: string, folderId: string, payloads: readonly StoredEmailPayload[], externalIds: readonly string[]) => Promise<Result<AppError, {
|
|
40
|
+
inserted: number;
|
|
41
|
+
updated: number;
|
|
42
|
+
}>>;
|
|
43
|
+
};
|
|
44
|
+
readonly imap: {
|
|
45
|
+
readonly listFolders: (accountId: string) => Promise<Result<ImapError, readonly {
|
|
46
|
+
path: string;
|
|
47
|
+
specialUse: string | null;
|
|
48
|
+
}[]>>;
|
|
49
|
+
readonly folderStatus: (accountId: string, path: string) => Promise<Result<ImapError, {
|
|
50
|
+
uidValidity: number;
|
|
51
|
+
uidNext: number;
|
|
52
|
+
messages: number;
|
|
53
|
+
}>>;
|
|
54
|
+
readonly fetchRange: (accountId: string, path: string, fromUid: number, toUid: number) => AsyncIterable<{
|
|
55
|
+
readonly imapMessage: FetchedMessageLike;
|
|
56
|
+
readonly bodyText: string | null;
|
|
57
|
+
readonly bodyHtml: string | null;
|
|
58
|
+
}>;
|
|
59
|
+
readonly fetchEnvelopesRange: (accountId: string, path: string, fromUid: number, toUid: number) => AsyncIterable<FetchedMessageLike>;
|
|
60
|
+
};
|
|
61
|
+
readonly now: () => Date;
|
|
62
|
+
};
|
|
63
|
+
export type SyncFolderReport = {
|
|
64
|
+
readonly folderId: string;
|
|
65
|
+
readonly newMessages: number;
|
|
66
|
+
readonly reconciledFlags: number;
|
|
67
|
+
readonly uidValidityRolled: boolean;
|
|
68
|
+
readonly error: string | null;
|
|
69
|
+
};
|
|
70
|
+
export type SyncReport = {
|
|
71
|
+
readonly accountId: string;
|
|
72
|
+
readonly folders: readonly SyncFolderReport[];
|
|
73
|
+
readonly prunedFolders: number;
|
|
74
|
+
readonly startedAt: string;
|
|
75
|
+
readonly finishedAt: string;
|
|
76
|
+
readonly error: string | null;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Run one sync tick for one account. Sequentially walks all folders,
|
|
80
|
+
* then prunes Drive folders that no longer exist in IMAP.
|
|
81
|
+
*/
|
|
82
|
+
export declare const runSyncOnce: (deps: SyncDeps, account: Account, watcher: WatcherSettings) => Promise<SyncReport>;
|
|
83
|
+
export declare const __test__: {
|
|
84
|
+
Err: <E>(error: E) => Result<E, never>;
|
|
85
|
+
};
|