@ouro.bot/cli 0.1.0-alpha.475 → 0.1.0-alpha.477
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/changelog.json +16 -0
- package/dist/heart/daemon/cli-exec.js +73 -12
- package/dist/heart/daemon/cli-help.js +8 -3
- package/dist/heart/daemon/cli-parse.js +14 -3
- package/dist/mailroom/blob-store.js +166 -13
- package/dist/mailroom/file-store.js +8 -1
- package/dist/mailroom/mbox-import.js +247 -45
- package/dist/mailroom/reader.js +31 -4
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.477",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Hosted Mailroom reads now use per-agent message index blobs, so MCP mail tools and Outlook stop crawling the entire hosted mailbox just to load recent mail.",
|
|
8
|
+
"Legacy hosted mailboxes can rebuild missing message indexes without reimporting mail, and duplicate or placement-update paths now repair index drift automatically.",
|
|
9
|
+
"Mail source filtering is now case-insensitive across blob and file mail stores, which keeps delegated-source reads resilient to older mixed-case source labels while the runtime and `ouro.bot` wrapper stay version-synced for the hosted-mail read repair."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"version": "0.1.0-alpha.476",
|
|
14
|
+
"changes": [
|
|
15
|
+
"`ouro mail import-mbox` now works against hosted Mailroom config by reading public registry coordinates from the owning agent vault instead of requiring local-only registry and store paths.",
|
|
16
|
+
"Delegated HEY archive imports now stream large local MBOX files into encrypted Mailroom storage with per-message dedupe, so multi-gigabyte hosted backfills can resume safely after interruption.",
|
|
17
|
+
"Agent Mail docs now teach linked HEY accounts as separate export and forwarding feeders even under one browser login, while keeping one delegated-source provenance lens for the agent."
|
|
18
|
+
]
|
|
19
|
+
},
|
|
4
20
|
{
|
|
5
21
|
"version": "0.1.0-alpha.475",
|
|
6
22
|
"changes": [
|
|
@@ -80,8 +80,8 @@ const ouro_version_manager_1 = require("../versioning/ouro-version-manager");
|
|
|
80
80
|
const update_checker_1 = require("../versioning/update-checker");
|
|
81
81
|
const sync_1 = require("../sync");
|
|
82
82
|
const core_1 = require("../../mailroom/core");
|
|
83
|
-
const file_store_1 = require("../../mailroom/file-store");
|
|
84
83
|
const mbox_import_1 = require("../../mailroom/mbox-import");
|
|
84
|
+
const reader_1 = require("../../mailroom/reader");
|
|
85
85
|
const cli_parse_1 = require("./cli-parse");
|
|
86
86
|
const cli_parse_2 = require("./cli-parse");
|
|
87
87
|
const cli_help_1 = require("./cli-help");
|
|
@@ -3409,27 +3409,42 @@ async function executeMailImportMbox(command, deps) {
|
|
|
3409
3409
|
progress.end();
|
|
3410
3410
|
throw new Error(`cannot read Mailroom config from ${runtime.itemPath}: ${runtime.error}`);
|
|
3411
3411
|
}
|
|
3412
|
-
const mailroom = runtime.config.mailroom;
|
|
3413
|
-
if (!mailroom
|
|
3412
|
+
const mailroom = (0, reader_1.parseMailroomConfig)(runtime.config.mailroom);
|
|
3413
|
+
if (!mailroom) {
|
|
3414
3414
|
progress.end();
|
|
3415
3415
|
throw new Error(`missing mailroom config for ${command.agent}; agent-runnable repair: 'ouro connect mail --agent ${command.agent}'`);
|
|
3416
3416
|
}
|
|
3417
|
-
const
|
|
3418
|
-
|
|
3419
|
-
|
|
3417
|
+
const hosted = Boolean(mailroom.azureAccountUrl);
|
|
3418
|
+
if (hosted) {
|
|
3419
|
+
const registryAzureAccountUrl = mailroom.registryAzureAccountUrl?.trim() ?? "";
|
|
3420
|
+
const registryBlob = mailroom.registryBlob?.trim() ?? "";
|
|
3421
|
+
if (!registryAzureAccountUrl || !registryBlob) {
|
|
3422
|
+
progress.end();
|
|
3423
|
+
throw new Error(`mailroom config for ${command.agent} is missing hosted registry coordinates; agent-runnable repair: 'ouro connect mail --agent ${command.agent}'`);
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
else {
|
|
3427
|
+
const registryPath = mailroom.registryPath?.trim() ?? "";
|
|
3428
|
+
const storePath = mailroom.storePath?.trim() ?? "";
|
|
3429
|
+
if (!registryPath || !storePath) {
|
|
3430
|
+
progress.end();
|
|
3431
|
+
throw new Error(`mailroom config for ${command.agent} is missing registryPath/storePath; agent-runnable repair: 'ouro connect mail --agent ${command.agent}'`);
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
const resolved = (0, reader_1.resolveMailroomReader)(command.agent);
|
|
3435
|
+
if (!resolved.ok) {
|
|
3420
3436
|
progress.end();
|
|
3421
|
-
throw new Error(
|
|
3437
|
+
throw new Error(resolved.error);
|
|
3422
3438
|
}
|
|
3423
3439
|
progress.updateDetail("reading registry and MBOX");
|
|
3424
|
-
const registry =
|
|
3440
|
+
const registry = await (0, reader_1.readMailroomRegistry)(resolved.config);
|
|
3425
3441
|
const filePath = path.resolve(command.filePath);
|
|
3426
|
-
const rawMbox = fs.readFileSync(filePath);
|
|
3427
3442
|
progress.updateDetail("importing delegated mail");
|
|
3428
|
-
const result = await (0, mbox_import_1.
|
|
3443
|
+
const result = await (0, mbox_import_1.importMboxFileToStore)({
|
|
3429
3444
|
registry,
|
|
3430
|
-
store:
|
|
3445
|
+
store: resolved.store,
|
|
3431
3446
|
agentId: command.agent,
|
|
3432
|
-
|
|
3447
|
+
filePath,
|
|
3433
3448
|
ownerEmail: command.ownerEmail,
|
|
3434
3449
|
source: command.source,
|
|
3435
3450
|
});
|
|
@@ -3454,6 +3469,49 @@ async function executeMailImportMbox(command, deps) {
|
|
|
3454
3469
|
throw error;
|
|
3455
3470
|
}
|
|
3456
3471
|
}
|
|
3472
|
+
async function executeMailBackfillIndexes(command, deps) {
|
|
3473
|
+
const progress = createHumanCommandProgress(deps, "mail index repair");
|
|
3474
|
+
try {
|
|
3475
|
+
progress.startPhase("resolving Mailroom store");
|
|
3476
|
+
const resolved = (0, reader_1.resolveMailroomReader)(command.agent);
|
|
3477
|
+
if (!resolved.ok) {
|
|
3478
|
+
progress.end();
|
|
3479
|
+
throw new Error(resolved.error);
|
|
3480
|
+
}
|
|
3481
|
+
if (resolved.storeKind !== "azure-blob") {
|
|
3482
|
+
progress.completePhase("resolving Mailroom store", "not needed");
|
|
3483
|
+
progress.end();
|
|
3484
|
+
const message = [
|
|
3485
|
+
`Hosted mail index backfill not needed for ${command.agent}`,
|
|
3486
|
+
`store: ${resolved.storeLabel}`,
|
|
3487
|
+
"This agent is using the local file Mailroom store, so recent mail reads do not depend on hosted blob indexes.",
|
|
3488
|
+
].join("\n");
|
|
3489
|
+
deps.writeStdout(message);
|
|
3490
|
+
return message;
|
|
3491
|
+
}
|
|
3492
|
+
const store = resolved.store;
|
|
3493
|
+
if (typeof store.backfillMessageIndexes !== "function") {
|
|
3494
|
+
progress.end();
|
|
3495
|
+
throw new Error(`hosted Mailroom store for ${command.agent} does not expose index backfill`);
|
|
3496
|
+
}
|
|
3497
|
+
progress.updateDetail("backfilling hosted message indexes");
|
|
3498
|
+
const indexed = await store.backfillMessageIndexes(command.agent);
|
|
3499
|
+
progress.completePhase("resolving Mailroom store", "backfilled");
|
|
3500
|
+
progress.end();
|
|
3501
|
+
const message = [
|
|
3502
|
+
`Backfilled hosted mail indexes for ${command.agent}`,
|
|
3503
|
+
`store: ${resolved.storeLabel}`,
|
|
3504
|
+
`indexed: ${indexed}`,
|
|
3505
|
+
"Safe to rerun. Existing index entries are rewritten in place.",
|
|
3506
|
+
].join("\n");
|
|
3507
|
+
deps.writeStdout(message);
|
|
3508
|
+
return message;
|
|
3509
|
+
}
|
|
3510
|
+
catch (error) {
|
|
3511
|
+
progress.end();
|
|
3512
|
+
throw error;
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3457
3515
|
async function executeConnectProviders(agent, deps) {
|
|
3458
3516
|
const promptInput = deps.promptInput;
|
|
3459
3517
|
if (!promptInput) {
|
|
@@ -4874,6 +4932,9 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
4874
4932
|
if (command.kind === "mail.import-mbox") {
|
|
4875
4933
|
return executeMailImportMbox(command, deps);
|
|
4876
4934
|
}
|
|
4935
|
+
if (command.kind === "mail.backfill-indexes") {
|
|
4936
|
+
return executeMailBackfillIndexes(command, deps);
|
|
4937
|
+
}
|
|
4877
4938
|
if (command.kind === "daemon.up") {
|
|
4878
4939
|
// ── dev mode cleanup: delete dev-config.json so the wrapper stops dispatching to dev repo ──
|
|
4879
4940
|
/* v8 ignore start -- dev-config cleanup: requires real filesystem state @preserve */
|
|
@@ -179,10 +179,10 @@ exports.COMMAND_REGISTRY = {
|
|
|
179
179
|
},
|
|
180
180
|
mail: {
|
|
181
181
|
category: "Auth",
|
|
182
|
-
description: "Import delegated mail
|
|
183
|
-
usage: "ouro mail import-mbox
|
|
182
|
+
description: "Import delegated mail and repair hosted Mailroom mailbox indexes",
|
|
183
|
+
usage: "ouro mail <import-mbox|backfill-indexes> [--agent <name>]",
|
|
184
184
|
example: "ouro mail import-mbox --file ~/Downloads/hey.mbox --owner-email ari@mendelow.me --source hey --agent slugger",
|
|
185
|
-
subcommands: ["import-mbox"],
|
|
185
|
+
subcommands: ["import-mbox", "backfill-indexes"],
|
|
186
186
|
},
|
|
187
187
|
use: {
|
|
188
188
|
category: "Auth",
|
|
@@ -326,6 +326,11 @@ const SUBCOMMAND_HELP = {
|
|
|
326
326
|
usage: "ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>]",
|
|
327
327
|
example: "ouro mail import-mbox --file ~/Downloads/hey.mbox --owner-email ari@mendelow.me --source hey --agent slugger",
|
|
328
328
|
},
|
|
329
|
+
"mail backfill-indexes": {
|
|
330
|
+
description: "Rebuild hosted blob mailbox indexes for faster recent-mail reads after large legacy imports or drift repair.",
|
|
331
|
+
usage: "ouro mail backfill-indexes [--agent <name>]",
|
|
332
|
+
example: "ouro mail backfill-indexes --agent slugger",
|
|
333
|
+
},
|
|
329
334
|
"provider refresh": {
|
|
330
335
|
description: "Reload this agent's provider credentials from its vault into daemon memory",
|
|
331
336
|
usage: "ouro provider refresh [--agent <name>]",
|
|
@@ -87,6 +87,7 @@ function usage() {
|
|
|
87
87
|
" ouro account ensure [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]",
|
|
88
88
|
" ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail] [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]",
|
|
89
89
|
" ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>]",
|
|
90
|
+
" ouro mail backfill-indexes [--agent <name>]",
|
|
90
91
|
" ouro auth verify [--agent <name>] [--provider <provider>]",
|
|
91
92
|
" ouro auth switch [--agent <name>] --provider <provider>",
|
|
92
93
|
" ouro vault create [--agent <name>] --email <email> [--server <url>] [--store <store>]",
|
|
@@ -884,8 +885,18 @@ function parseConnectCommand(args) {
|
|
|
884
885
|
}
|
|
885
886
|
function parseMailCommand(args) {
|
|
886
887
|
const [sub, ...subArgs] = args;
|
|
888
|
+
const usageText = "Usage: ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>]\n ouro mail backfill-indexes [--agent <name>]";
|
|
889
|
+
if (sub === "backfill-indexes") {
|
|
890
|
+
const { agent, rest } = extractAgentFlag(subArgs);
|
|
891
|
+
if (rest.length > 0)
|
|
892
|
+
throw new Error(usageText);
|
|
893
|
+
return {
|
|
894
|
+
kind: "mail.backfill-indexes",
|
|
895
|
+
...(agent ? { agent } : {}),
|
|
896
|
+
};
|
|
897
|
+
}
|
|
887
898
|
if (sub !== "import-mbox") {
|
|
888
|
-
throw new Error(
|
|
899
|
+
throw new Error(usageText);
|
|
889
900
|
}
|
|
890
901
|
const { agent, rest } = extractAgentFlag(subArgs);
|
|
891
902
|
let filePath;
|
|
@@ -905,10 +916,10 @@ function parseMailCommand(args) {
|
|
|
905
916
|
source = rest[++i];
|
|
906
917
|
continue;
|
|
907
918
|
}
|
|
908
|
-
throw new Error(
|
|
919
|
+
throw new Error(usageText);
|
|
909
920
|
}
|
|
910
921
|
if (!filePath) {
|
|
911
|
-
throw new Error(
|
|
922
|
+
throw new Error(usageText);
|
|
912
923
|
}
|
|
913
924
|
return {
|
|
914
925
|
kind: "mail.import-mbox",
|
|
@@ -4,6 +4,12 @@ exports.AzureBlobMailroomStore = void 0;
|
|
|
4
4
|
exports.decryptBlobMessages = decryptBlobMessages;
|
|
5
5
|
const runtime_1 = require("../nerves/runtime");
|
|
6
6
|
const core_1 = require("./core");
|
|
7
|
+
const MESSAGE_INDEX_PREFIX = "message-index";
|
|
8
|
+
const MESSAGE_INDEX_SORT_MAX_MS = 9_999_999_999_999;
|
|
9
|
+
const MESSAGE_INDEX_SORT_WIDTH = 13;
|
|
10
|
+
const MESSAGE_INDEX_NO_SOURCE = "~";
|
|
11
|
+
const MESSAGE_INDEX_BACKFILL_CONCURRENCY = 16;
|
|
12
|
+
const MESSAGE_LIST_SCAN_CONCURRENCY = 32;
|
|
7
13
|
function compareNewestFirst(left, right) {
|
|
8
14
|
return Date.parse(right.receivedAt) - Date.parse(left.receivedAt);
|
|
9
15
|
}
|
|
@@ -18,6 +24,73 @@ async function downloadJson(blob) {
|
|
|
18
24
|
return null;
|
|
19
25
|
return JSON.parse((await blob.downloadToBuffer()).toString("utf-8"));
|
|
20
26
|
}
|
|
27
|
+
function encodeSourceToken(source) {
|
|
28
|
+
return source ? encodeURIComponent(source.toLowerCase()) : MESSAGE_INDEX_NO_SOURCE;
|
|
29
|
+
}
|
|
30
|
+
function decodeSourceToken(token) {
|
|
31
|
+
return token === MESSAGE_INDEX_NO_SOURCE ? undefined : decodeURIComponent(token);
|
|
32
|
+
}
|
|
33
|
+
function parseSortMs(receivedAt) {
|
|
34
|
+
const parsed = Date.parse(receivedAt);
|
|
35
|
+
if (!Number.isFinite(parsed))
|
|
36
|
+
return 0;
|
|
37
|
+
return Math.max(0, Math.min(MESSAGE_INDEX_SORT_MAX_MS, parsed));
|
|
38
|
+
}
|
|
39
|
+
function messageIndexPrefix(agentId) {
|
|
40
|
+
return `${MESSAGE_INDEX_PREFIX}/${agentId}/`;
|
|
41
|
+
}
|
|
42
|
+
function messageIndexBlobName(message) {
|
|
43
|
+
const sortKey = String(MESSAGE_INDEX_SORT_MAX_MS - parseSortMs(message.receivedAt)).padStart(MESSAGE_INDEX_SORT_WIDTH, "0");
|
|
44
|
+
return `${messageIndexPrefix(message.agentId)}${sortKey}__${message.compartmentKind}__${message.placement}__${encodeSourceToken(message.source)}__${message.id}.json`;
|
|
45
|
+
}
|
|
46
|
+
function messageIndexRecord(message) {
|
|
47
|
+
return {
|
|
48
|
+
schemaVersion: 1,
|
|
49
|
+
id: message.id,
|
|
50
|
+
agentId: message.agentId,
|
|
51
|
+
compartmentKind: message.compartmentKind,
|
|
52
|
+
placement: message.placement,
|
|
53
|
+
...(message.source ? { source: message.source } : {}),
|
|
54
|
+
receivedAt: message.receivedAt,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function parseMessageIndexBlobName(name) {
|
|
58
|
+
if (!name.startsWith(`${MESSAGE_INDEX_PREFIX}/`) || !name.endsWith(".json"))
|
|
59
|
+
return null;
|
|
60
|
+
const parts = name.split("/");
|
|
61
|
+
if (parts.length !== 3)
|
|
62
|
+
return null;
|
|
63
|
+
const agentId = parts[1];
|
|
64
|
+
const stem = parts[2].slice(0, -5);
|
|
65
|
+
const [sortKey, compartmentKind, placement, sourceToken, ...idParts] = stem.split("__");
|
|
66
|
+
if (!sortKey || !compartmentKind || !placement || !sourceToken || idParts.length === 0)
|
|
67
|
+
return null;
|
|
68
|
+
if (compartmentKind !== "native" && compartmentKind !== "delegated")
|
|
69
|
+
return null;
|
|
70
|
+
const receivedAtMs = MESSAGE_INDEX_SORT_MAX_MS - Number.parseInt(sortKey, 10);
|
|
71
|
+
return {
|
|
72
|
+
schemaVersion: 1,
|
|
73
|
+
id: idParts.join("__"),
|
|
74
|
+
agentId,
|
|
75
|
+
compartmentKind,
|
|
76
|
+
placement: placement,
|
|
77
|
+
...(decodeSourceToken(sourceToken) ? { source: decodeSourceToken(sourceToken) } : {}),
|
|
78
|
+
receivedAt: Number.isFinite(receivedAtMs) ? new Date(receivedAtMs).toISOString() : new Date(0).toISOString(),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function sourceMatchesFilter(source, filter) {
|
|
82
|
+
if (!filter)
|
|
83
|
+
return true;
|
|
84
|
+
if (!source)
|
|
85
|
+
return false;
|
|
86
|
+
return source.toLowerCase() === filter.toLowerCase();
|
|
87
|
+
}
|
|
88
|
+
function messageMatchesFilters(message, filters) {
|
|
89
|
+
return message.agentId === filters.agentId &&
|
|
90
|
+
(filters.placement ? message.placement === filters.placement : true) &&
|
|
91
|
+
(filters.compartmentKind ? message.compartmentKind === filters.compartmentKind : true) &&
|
|
92
|
+
sourceMatchesFilter(message.source, filters.source);
|
|
93
|
+
}
|
|
21
94
|
class AzureBlobMailroomStore {
|
|
22
95
|
serviceClient;
|
|
23
96
|
containerName;
|
|
@@ -44,6 +117,9 @@ class AzureBlobMailroomStore {
|
|
|
44
117
|
messageBlob(id) {
|
|
45
118
|
return this.container.getBlockBlobClient(`messages/${id}.json`);
|
|
46
119
|
}
|
|
120
|
+
messageIndexBlob(name) {
|
|
121
|
+
return this.container.getBlockBlobClient(name);
|
|
122
|
+
}
|
|
47
123
|
candidateBlob(id) {
|
|
48
124
|
return this.container.getBlockBlobClient(`candidates/${id}.json`);
|
|
49
125
|
}
|
|
@@ -59,11 +135,92 @@ class AzureBlobMailroomStore {
|
|
|
59
135
|
outboundBlob(id) {
|
|
60
136
|
return this.container.getBlockBlobClient(`outbound/${id}.json`);
|
|
61
137
|
}
|
|
138
|
+
async putMessageIndex(message) {
|
|
139
|
+
await this.messageIndexBlob(messageIndexBlobName(message)).uploadData(blobText(messageIndexRecord(message)));
|
|
140
|
+
}
|
|
141
|
+
async removeMessageIndex(message) {
|
|
142
|
+
await this.messageIndexBlob(messageIndexBlobName(message)).deleteIfExists();
|
|
143
|
+
}
|
|
144
|
+
async listMessagesLegacy(filters) {
|
|
145
|
+
const messageBlobNames = [];
|
|
146
|
+
for await (const item of this.container.listBlobsFlat({ prefix: "messages/" })) {
|
|
147
|
+
messageBlobNames.push(item.name);
|
|
148
|
+
}
|
|
149
|
+
const matches = [];
|
|
150
|
+
const limit = filters.limit ?? 20;
|
|
151
|
+
let nextIndex = 0;
|
|
152
|
+
const worker = async () => {
|
|
153
|
+
while (nextIndex < messageBlobNames.length) {
|
|
154
|
+
const current = messageBlobNames[nextIndex];
|
|
155
|
+
nextIndex += 1;
|
|
156
|
+
const message = await downloadJson(this.container.getBlockBlobClient(current));
|
|
157
|
+
if (!message || !messageMatchesFilters(message, filters))
|
|
158
|
+
continue;
|
|
159
|
+
matches.push(message);
|
|
160
|
+
matches.sort(compareNewestFirst);
|
|
161
|
+
if (matches.length > limit)
|
|
162
|
+
matches.length = limit;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
await Promise.all(Array.from({ length: Math.min(MESSAGE_LIST_SCAN_CONCURRENCY, Math.max(messageBlobNames.length, 1)) }, async () => worker()));
|
|
166
|
+
return matches.sort(compareNewestFirst).slice(0, limit);
|
|
167
|
+
}
|
|
168
|
+
async listMessagesFromIndexes(filters) {
|
|
169
|
+
const messageIds = [];
|
|
170
|
+
let sawIndex = false;
|
|
171
|
+
for await (const item of this.container.listBlobsFlat({ prefix: messageIndexPrefix(filters.agentId) })) {
|
|
172
|
+
sawIndex = true;
|
|
173
|
+
const parsed = parseMessageIndexBlobName(item.name);
|
|
174
|
+
if (!parsed || !messageMatchesFilters(parsed, filters))
|
|
175
|
+
continue;
|
|
176
|
+
messageIds.push(parsed.id);
|
|
177
|
+
if (messageIds.length >= (filters.limit ?? 20))
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
if (!sawIndex)
|
|
181
|
+
return null;
|
|
182
|
+
return (await Promise.all(messageIds.map(async (id) => downloadJson(this.messageBlob(id)))))
|
|
183
|
+
.filter((message) => message !== null)
|
|
184
|
+
.filter((message) => messageMatchesFilters(message, filters))
|
|
185
|
+
.sort(compareNewestFirst)
|
|
186
|
+
.slice(0, filters.limit ?? 20);
|
|
187
|
+
}
|
|
188
|
+
async backfillMessageIndexes(agentId) {
|
|
189
|
+
await this.ensureContainer();
|
|
190
|
+
const messageBlobNames = [];
|
|
191
|
+
for await (const item of this.container.listBlobsFlat({ prefix: "messages/" })) {
|
|
192
|
+
messageBlobNames.push(item.name);
|
|
193
|
+
}
|
|
194
|
+
let indexed = 0;
|
|
195
|
+
let nextIndex = 0;
|
|
196
|
+
const worker = async () => {
|
|
197
|
+
while (nextIndex < messageBlobNames.length) {
|
|
198
|
+
const current = messageBlobNames[nextIndex];
|
|
199
|
+
nextIndex += 1;
|
|
200
|
+
const message = await downloadJson(this.container.getBlockBlobClient(current));
|
|
201
|
+
if (!message)
|
|
202
|
+
continue;
|
|
203
|
+
if (agentId && message.agentId !== agentId)
|
|
204
|
+
continue;
|
|
205
|
+
await this.messageIndexBlob(messageIndexBlobName(message)).uploadData(blobText(messageIndexRecord(message)));
|
|
206
|
+
indexed += 1;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
await Promise.all(Array.from({ length: Math.min(MESSAGE_INDEX_BACKFILL_CONCURRENCY, Math.max(messageBlobNames.length, 1)) }, async () => worker()));
|
|
210
|
+
(0, runtime_1.emitNervesEvent)({
|
|
211
|
+
component: "senses",
|
|
212
|
+
event: "senses.mail_blob_index_backfilled",
|
|
213
|
+
message: "azure blob mailroom message indexes backfilled",
|
|
214
|
+
meta: { agentId: agentId ?? null, indexed },
|
|
215
|
+
});
|
|
216
|
+
return indexed;
|
|
217
|
+
}
|
|
62
218
|
async putRawMessage(input) {
|
|
63
219
|
await this.ensureContainer();
|
|
64
220
|
const { message, rawPayload, candidate } = await (0, core_1.buildStoredMailMessage)(input);
|
|
65
221
|
const existing = await downloadJson(this.messageBlob(message.id));
|
|
66
222
|
if (existing) {
|
|
223
|
+
await this.putMessageIndex(existing);
|
|
67
224
|
(0, runtime_1.emitNervesEvent)({
|
|
68
225
|
component: "senses",
|
|
69
226
|
event: "senses.mail_blob_store_dedupe",
|
|
@@ -74,6 +231,7 @@ class AzureBlobMailroomStore {
|
|
|
74
231
|
}
|
|
75
232
|
await this.rawBlob(message.rawObject).uploadData(blobText(rawPayload));
|
|
76
233
|
await this.messageBlob(message.id).uploadData(blobText(message));
|
|
234
|
+
await this.putMessageIndex(message);
|
|
77
235
|
if (candidate) {
|
|
78
236
|
await this.candidateBlob(candidate.id).uploadData(blobText(candidate));
|
|
79
237
|
}
|
|
@@ -98,24 +256,17 @@ class AzureBlobMailroomStore {
|
|
|
98
256
|
}
|
|
99
257
|
async listMessages(filters) {
|
|
100
258
|
await this.ensureContainer();
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
259
|
+
let filtered = await this.listMessagesFromIndexes(filters);
|
|
260
|
+
let source = "index";
|
|
261
|
+
if (filtered === null) {
|
|
262
|
+
filtered = await this.listMessagesLegacy(filters);
|
|
263
|
+
source = "legacy";
|
|
106
264
|
}
|
|
107
|
-
const filtered = messages
|
|
108
|
-
.filter((message) => message.agentId === filters.agentId)
|
|
109
|
-
.filter((message) => filters.placement ? message.placement === filters.placement : true)
|
|
110
|
-
.filter((message) => filters.compartmentKind ? message.compartmentKind === filters.compartmentKind : true)
|
|
111
|
-
.filter((message) => filters.source ? message.source === filters.source : true)
|
|
112
|
-
.sort(compareNewestFirst)
|
|
113
|
-
.slice(0, filters.limit ?? 20);
|
|
114
265
|
(0, runtime_1.emitNervesEvent)({
|
|
115
266
|
component: "senses",
|
|
116
267
|
event: "senses.mail_blob_store_messages_listed",
|
|
117
268
|
message: "azure blob mailroom store listed messages",
|
|
118
|
-
meta: { agentId: filters.agentId, count: filtered.length },
|
|
269
|
+
meta: { agentId: filters.agentId, count: filtered.length, source },
|
|
119
270
|
});
|
|
120
271
|
return filtered;
|
|
121
272
|
}
|
|
@@ -134,6 +285,8 @@ class AzureBlobMailroomStore {
|
|
|
134
285
|
}
|
|
135
286
|
const updated = { ...message, placement };
|
|
136
287
|
await blob.uploadData(blobText(updated));
|
|
288
|
+
await this.removeMessageIndex(message);
|
|
289
|
+
await this.putMessageIndex(updated);
|
|
137
290
|
(0, runtime_1.emitNervesEvent)({
|
|
138
291
|
component: "senses",
|
|
139
292
|
event: "senses.mail_blob_store_message_placement_updated",
|
|
@@ -61,6 +61,13 @@ function compareNewestFirst(left, right) {
|
|
|
61
61
|
function compareCandidatesNewestFirst(left, right) {
|
|
62
62
|
return Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt);
|
|
63
63
|
}
|
|
64
|
+
function sourceMatchesFilter(source, filter) {
|
|
65
|
+
if (!filter)
|
|
66
|
+
return true;
|
|
67
|
+
if (!source)
|
|
68
|
+
return false;
|
|
69
|
+
return source.toLowerCase() === filter.toLowerCase();
|
|
70
|
+
}
|
|
64
71
|
class FileMailroomStore {
|
|
65
72
|
rootDir;
|
|
66
73
|
constructor(options) {
|
|
@@ -157,7 +164,7 @@ class FileMailroomStore {
|
|
|
157
164
|
.filter((message) => message.agentId === filters.agentId)
|
|
158
165
|
.filter((message) => filters.placement ? message.placement === filters.placement : true)
|
|
159
166
|
.filter((message) => filters.compartmentKind ? message.compartmentKind === filters.compartmentKind : true)
|
|
160
|
-
.filter((message) =>
|
|
167
|
+
.filter((message) => sourceMatchesFilter(message.source, filters.source))
|
|
161
168
|
.sort(compareNewestFirst)
|
|
162
169
|
.slice(0, filters.limit ?? 20);
|
|
163
170
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -1,8 +1,42 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.splitMboxMessages = splitMboxMessages;
|
|
4
37
|
exports.importMboxToStore = importMboxToStore;
|
|
5
|
-
|
|
38
|
+
exports.importMboxFileToStore = importMboxFileToStore;
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
6
40
|
const runtime_1 = require("../nerves/runtime");
|
|
7
41
|
const core_1 = require("./core");
|
|
8
42
|
function splitMboxMessages(rawMbox) {
|
|
@@ -28,6 +62,65 @@ function splitMboxMessages(rawMbox) {
|
|
|
28
62
|
});
|
|
29
63
|
return messages;
|
|
30
64
|
}
|
|
65
|
+
function trimTrailingLineEnding(message) {
|
|
66
|
+
if (message.length >= 2 && message[message.length - 2] === 0x0d && message[message.length - 1] === 0x0a) {
|
|
67
|
+
return message.subarray(0, -2);
|
|
68
|
+
}
|
|
69
|
+
if (message.length >= 1 && message[message.length - 1] === 0x0a) {
|
|
70
|
+
return message.subarray(0, -1);
|
|
71
|
+
}
|
|
72
|
+
return message;
|
|
73
|
+
}
|
|
74
|
+
function isSeparatorLine(line) {
|
|
75
|
+
return line.length >= 5 &&
|
|
76
|
+
line[0] === 0x46 &&
|
|
77
|
+
line[1] === 0x72 &&
|
|
78
|
+
line[2] === 0x6f &&
|
|
79
|
+
line[3] === 0x6d &&
|
|
80
|
+
line[4] === 0x20;
|
|
81
|
+
}
|
|
82
|
+
async function* streamMboxMessagesFromFile(filePath) {
|
|
83
|
+
const stream = fs.createReadStream(filePath);
|
|
84
|
+
let pending = Buffer.alloc(0);
|
|
85
|
+
let currentParts = [];
|
|
86
|
+
const flushCurrent = () => {
|
|
87
|
+
if (currentParts.length === 0)
|
|
88
|
+
return null;
|
|
89
|
+
const message = trimTrailingLineEnding(Buffer.concat(currentParts));
|
|
90
|
+
currentParts = [];
|
|
91
|
+
return message;
|
|
92
|
+
};
|
|
93
|
+
for await (const chunk of stream) {
|
|
94
|
+
pending = pending.length > 0 ? Buffer.concat([pending, chunk]) : Buffer.from(chunk);
|
|
95
|
+
let newlineIndex = pending.indexOf(0x0a);
|
|
96
|
+
while (newlineIndex >= 0) {
|
|
97
|
+
const line = pending.subarray(0, newlineIndex + 1);
|
|
98
|
+
pending = pending.subarray(newlineIndex + 1);
|
|
99
|
+
if (isSeparatorLine(line)) {
|
|
100
|
+
const message = flushCurrent();
|
|
101
|
+
if (message && message.length > 0)
|
|
102
|
+
yield message;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
currentParts.push(Buffer.from(line));
|
|
106
|
+
}
|
|
107
|
+
newlineIndex = pending.indexOf(0x0a);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (pending.length > 0) {
|
|
111
|
+
if (isSeparatorLine(pending)) {
|
|
112
|
+
const message = flushCurrent();
|
|
113
|
+
if (message && message.length > 0)
|
|
114
|
+
yield message;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
currentParts.push(Buffer.from(pending));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const finalMessage = flushCurrent();
|
|
121
|
+
if (finalMessage && finalMessage.length > 0)
|
|
122
|
+
yield finalMessage;
|
|
123
|
+
}
|
|
31
124
|
function findSourceGrant(input) {
|
|
32
125
|
const ownerEmail = input.ownerEmail ? (0, core_1.normalizeMailAddress)(input.ownerEmail) : undefined;
|
|
33
126
|
const source = input.source?.trim().toLowerCase();
|
|
@@ -48,9 +141,9 @@ function findSourceGrant(input) {
|
|
|
48
141
|
}
|
|
49
142
|
return grants[0];
|
|
50
143
|
}
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
const
|
|
144
|
+
function parseMboxMessage(rawMessage, grant) {
|
|
145
|
+
const mailFrom = extractHeaderAddress(rawMessage, "from");
|
|
146
|
+
const messageDate = extractHeaderDate(rawMessage, "date");
|
|
54
147
|
return {
|
|
55
148
|
rawMessage,
|
|
56
149
|
envelope: {
|
|
@@ -58,7 +151,7 @@ async function parseMboxMessage(rawMessage, grant) {
|
|
|
58
151
|
rcptTo: [(0, core_1.normalizeMailAddress)(grant.aliasAddress)],
|
|
59
152
|
remoteAddress: "mbox-import",
|
|
60
153
|
},
|
|
61
|
-
...(
|
|
154
|
+
...(messageDate ? { messageDate } : {}),
|
|
62
155
|
};
|
|
63
156
|
}
|
|
64
157
|
function latestMessageDate(messages) {
|
|
@@ -69,6 +162,52 @@ function latestMessageDate(messages) {
|
|
|
69
162
|
return null;
|
|
70
163
|
return new Date(Math.max(...timestamps)).toISOString();
|
|
71
164
|
}
|
|
165
|
+
function readHeaderBlock(rawMessage) {
|
|
166
|
+
const crlfBoundary = rawMessage.indexOf("\r\n\r\n");
|
|
167
|
+
if (crlfBoundary >= 0) {
|
|
168
|
+
return rawMessage.subarray(0, crlfBoundary).toString("utf-8");
|
|
169
|
+
}
|
|
170
|
+
const lfBoundary = rawMessage.indexOf("\n\n");
|
|
171
|
+
if (lfBoundary >= 0) {
|
|
172
|
+
return rawMessage.subarray(0, lfBoundary).toString("utf-8");
|
|
173
|
+
}
|
|
174
|
+
return rawMessage.toString("utf-8");
|
|
175
|
+
}
|
|
176
|
+
function extractHeaderValue(rawMessage, headerName) {
|
|
177
|
+
const target = `${headerName.toLowerCase()}:`;
|
|
178
|
+
let currentHeader = "";
|
|
179
|
+
const unfoldedHeaders = [];
|
|
180
|
+
for (const line of readHeaderBlock(rawMessage).split(/\r?\n/)) {
|
|
181
|
+
if (/^[ \t]/.test(line) && currentHeader) {
|
|
182
|
+
currentHeader += ` ${line.trim()}`;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (currentHeader)
|
|
186
|
+
unfoldedHeaders.push(currentHeader);
|
|
187
|
+
currentHeader = line;
|
|
188
|
+
}
|
|
189
|
+
if (currentHeader)
|
|
190
|
+
unfoldedHeaders.push(currentHeader);
|
|
191
|
+
const match = unfoldedHeaders.find((line) => line.toLowerCase().startsWith(target));
|
|
192
|
+
return match ? match.slice(target.length).trim() : "";
|
|
193
|
+
}
|
|
194
|
+
function extractHeaderAddress(rawMessage, headerName) {
|
|
195
|
+
const value = extractHeaderValue(rawMessage, headerName);
|
|
196
|
+
if (!value)
|
|
197
|
+
return "";
|
|
198
|
+
const bracketed = value.match(/<([^>]+)>/);
|
|
199
|
+
if (bracketed?.[1])
|
|
200
|
+
return bracketed[1];
|
|
201
|
+
const plain = value.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i);
|
|
202
|
+
return plain?.[0] ?? "";
|
|
203
|
+
}
|
|
204
|
+
function extractHeaderDate(rawMessage, headerName) {
|
|
205
|
+
const value = extractHeaderValue(rawMessage, headerName);
|
|
206
|
+
if (!value)
|
|
207
|
+
return undefined;
|
|
208
|
+
const parsed = new Date(value);
|
|
209
|
+
return Number.isFinite(parsed.getTime()) ? parsed : undefined;
|
|
210
|
+
}
|
|
72
211
|
function historicalImportClassification(resolvedPlacement, sourceGrant) {
|
|
73
212
|
return {
|
|
74
213
|
placement: resolvedPlacement,
|
|
@@ -76,61 +215,124 @@ function historicalImportClassification(resolvedPlacement, sourceGrant) {
|
|
|
76
215
|
trustReason: `delegated source grant ${sourceGrant.source} historical mbox import`,
|
|
77
216
|
};
|
|
78
217
|
}
|
|
79
|
-
async function
|
|
80
|
-
const agentId = input.agentId.toLowerCase();
|
|
81
|
-
const sourceGrant = findSourceGrant({
|
|
82
|
-
registry: input.registry,
|
|
83
|
-
agentId,
|
|
84
|
-
ownerEmail: input.ownerEmail,
|
|
85
|
-
source: input.source,
|
|
86
|
-
});
|
|
87
|
-
const resolved = (0, core_1.resolveMailAddress)(input.registry, sourceGrant.aliasAddress);
|
|
88
|
-
/* v8 ignore start -- findSourceGrant and resolveMailAddress share the same registry; this is a corruption guard. @preserve */
|
|
89
|
-
if (!resolved) {
|
|
90
|
-
throw new Error(`Source grant alias ${sourceGrant.aliasAddress} is not resolvable`);
|
|
91
|
-
}
|
|
92
|
-
/* v8 ignore stop */
|
|
218
|
+
async function importParsedMessagesToStore(input) {
|
|
93
219
|
let imported = 0;
|
|
94
220
|
let duplicates = 0;
|
|
95
221
|
const messages = [];
|
|
96
|
-
|
|
97
|
-
const parsedMessages = await Promise.all(rawMessages.map((rawMessage) => parseMboxMessage(rawMessage, sourceGrant)));
|
|
222
|
+
let scanned = 0;
|
|
98
223
|
const importedAt = (input.importedAt ?? new Date()).toISOString();
|
|
99
|
-
const sourceFreshThrough =
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
224
|
+
const sourceFreshThrough = input.sourceFreshThrough ?? null;
|
|
225
|
+
const maxConcurrency = Math.max(1, input.maxConcurrency ?? 1);
|
|
226
|
+
const inFlight = new Set();
|
|
227
|
+
const enqueue = (parsedMessage) => {
|
|
228
|
+
const task = (async () => {
|
|
229
|
+
const result = await input.store.putRawMessage({
|
|
230
|
+
resolved: input.resolved,
|
|
231
|
+
envelope: parsedMessage.envelope,
|
|
232
|
+
rawMime: parsedMessage.rawMessage,
|
|
233
|
+
receivedAt: parsedMessage.messageDate ?? input.importedAt,
|
|
234
|
+
ingest: {
|
|
235
|
+
schemaVersion: 1,
|
|
236
|
+
kind: "mbox-import",
|
|
237
|
+
importedAt,
|
|
238
|
+
sourceFreshThrough,
|
|
239
|
+
attentionSuppressed: true,
|
|
240
|
+
},
|
|
241
|
+
classification: historicalImportClassification(input.resolved.defaultPlacement, input.sourceGrant),
|
|
242
|
+
});
|
|
243
|
+
if (input.collectMessages !== false)
|
|
244
|
+
messages.push(result.message);
|
|
245
|
+
if (result.created)
|
|
246
|
+
imported += 1;
|
|
247
|
+
else
|
|
248
|
+
duplicates += 1;
|
|
249
|
+
})().finally(() => {
|
|
250
|
+
inFlight.delete(task);
|
|
114
251
|
});
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
252
|
+
inFlight.add(task);
|
|
253
|
+
return task;
|
|
254
|
+
};
|
|
255
|
+
for await (const parsedMessage of input.parsedMessages) {
|
|
256
|
+
scanned += 1;
|
|
257
|
+
enqueue(parsedMessage);
|
|
258
|
+
if (inFlight.size >= maxConcurrency) {
|
|
259
|
+
await Promise.race(inFlight);
|
|
260
|
+
}
|
|
120
261
|
}
|
|
262
|
+
await Promise.all(inFlight);
|
|
121
263
|
(0, runtime_1.emitNervesEvent)({
|
|
122
264
|
component: "senses",
|
|
123
265
|
event: "senses.mail_mbox_imported",
|
|
124
266
|
message: "mbox mail imported",
|
|
125
|
-
meta: { agentId
|
|
267
|
+
meta: { agentId: input.sourceGrant.agentId.toLowerCase(), scanned, imported, duplicates, grantId: input.sourceGrant.grantId },
|
|
126
268
|
});
|
|
127
269
|
return {
|
|
128
|
-
agentId,
|
|
129
|
-
sourceGrant,
|
|
130
|
-
scanned
|
|
270
|
+
agentId: input.sourceGrant.agentId.toLowerCase(),
|
|
271
|
+
sourceGrant: input.sourceGrant,
|
|
272
|
+
scanned,
|
|
131
273
|
imported,
|
|
132
274
|
duplicates,
|
|
133
275
|
sourceFreshThrough,
|
|
134
276
|
messages,
|
|
135
277
|
};
|
|
136
278
|
}
|
|
279
|
+
async function scanMboxFileFreshness(filePath, sourceGrant) {
|
|
280
|
+
let latestTimestamp = Number.NEGATIVE_INFINITY;
|
|
281
|
+
for await (const rawMessage of streamMboxMessagesFromFile(filePath)) {
|
|
282
|
+
const parsedMessage = parseMboxMessage(rawMessage, sourceGrant);
|
|
283
|
+
if (parsedMessage.messageDate) {
|
|
284
|
+
latestTimestamp = Math.max(latestTimestamp, parsedMessage.messageDate.getTime());
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return Number.isFinite(latestTimestamp) ? new Date(latestTimestamp).toISOString() : null;
|
|
288
|
+
}
|
|
289
|
+
function resolveImportTarget(input) {
|
|
290
|
+
const agentId = input.agentId.toLowerCase();
|
|
291
|
+
const sourceGrant = findSourceGrant({
|
|
292
|
+
registry: input.registry,
|
|
293
|
+
agentId,
|
|
294
|
+
ownerEmail: input.ownerEmail,
|
|
295
|
+
source: input.source,
|
|
296
|
+
});
|
|
297
|
+
const resolved = (0, core_1.resolveMailAddress)(input.registry, sourceGrant.aliasAddress);
|
|
298
|
+
/* v8 ignore start -- findSourceGrant and resolveMailAddress share the same registry; this is a corruption guard. @preserve */
|
|
299
|
+
if (!resolved) {
|
|
300
|
+
throw new Error(`Source grant alias ${sourceGrant.aliasAddress} is not resolvable`);
|
|
301
|
+
}
|
|
302
|
+
/* v8 ignore stop */
|
|
303
|
+
return { agentId, sourceGrant, resolved };
|
|
304
|
+
}
|
|
305
|
+
async function importMboxToStore(input) {
|
|
306
|
+
const target = resolveImportTarget(input);
|
|
307
|
+
const rawMessages = splitMboxMessages(input.rawMbox);
|
|
308
|
+
const parsedMessages = rawMessages.map((rawMessage) => parseMboxMessage(rawMessage, target.sourceGrant));
|
|
309
|
+
const sourceFreshThrough = latestMessageDate(parsedMessages);
|
|
310
|
+
return importParsedMessagesToStore({
|
|
311
|
+
sourceGrant: target.sourceGrant,
|
|
312
|
+
resolved: target.resolved,
|
|
313
|
+
store: input.store,
|
|
314
|
+
parsedMessages,
|
|
315
|
+
importedAt: input.importedAt,
|
|
316
|
+
collectMessages: true,
|
|
317
|
+
sourceFreshThrough,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
async function importMboxFileToStore(input) {
|
|
321
|
+
const target = resolveImportTarget(input);
|
|
322
|
+
const sourceFreshThrough = await scanMboxFileFreshness(input.filePath, target.sourceGrant);
|
|
323
|
+
async function* parsedMessages() {
|
|
324
|
+
for await (const rawMessage of streamMboxMessagesFromFile(input.filePath)) {
|
|
325
|
+
yield parseMboxMessage(rawMessage, target.sourceGrant);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return importParsedMessagesToStore({
|
|
329
|
+
sourceGrant: target.sourceGrant,
|
|
330
|
+
resolved: target.resolved,
|
|
331
|
+
store: input.store,
|
|
332
|
+
parsedMessages: parsedMessages(),
|
|
333
|
+
importedAt: input.importedAt,
|
|
334
|
+
collectMessages: false,
|
|
335
|
+
sourceFreshThrough,
|
|
336
|
+
maxConcurrency: 8,
|
|
337
|
+
});
|
|
338
|
+
}
|
package/dist/mailroom/reader.js
CHANGED
|
@@ -34,7 +34,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.parseMailroomConfig = parseMailroomConfig;
|
|
37
|
+
exports.readMailroomRegistry = readMailroomRegistry;
|
|
37
38
|
exports.resolveMailroomReader = resolveMailroomReader;
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
38
40
|
const path = __importStar(require("node:path"));
|
|
39
41
|
const storage_blob_1 = require("@azure/storage-blob");
|
|
40
42
|
const identity_1 = require("@azure/identity");
|
|
@@ -71,6 +73,9 @@ function parseMailroomConfig(value) {
|
|
|
71
73
|
const storePath = textField(value, "storePath");
|
|
72
74
|
const azureAccountUrl = textField(value, "azureAccountUrl");
|
|
73
75
|
const azureContainer = textField(value, "azureContainer");
|
|
76
|
+
const registryAzureAccountUrl = textField(value, "registryAzureAccountUrl");
|
|
77
|
+
const registryContainer = textField(value, "registryContainer");
|
|
78
|
+
const registryBlob = textField(value, "registryBlob");
|
|
74
79
|
const azureManagedIdentityClientId = textField(value, "azureManagedIdentityClientId");
|
|
75
80
|
const smtpPort = numberField(value, "smtpPort");
|
|
76
81
|
const httpPort = numberField(value, "httpPort");
|
|
@@ -86,6 +91,9 @@ function parseMailroomConfig(value) {
|
|
|
86
91
|
...(storePath ? { storePath } : {}),
|
|
87
92
|
...(azureAccountUrl ? { azureAccountUrl } : {}),
|
|
88
93
|
...(azureContainer ? { azureContainer } : {}),
|
|
94
|
+
...(registryAzureAccountUrl ? { registryAzureAccountUrl } : {}),
|
|
95
|
+
...(registryContainer ? { registryContainer } : {}),
|
|
96
|
+
...(registryBlob ? { registryBlob } : {}),
|
|
89
97
|
...(azureManagedIdentityClientId ? { azureManagedIdentityClientId } : {}),
|
|
90
98
|
...(smtpPort !== undefined ? { smtpPort } : {}),
|
|
91
99
|
...(httpPort !== undefined ? { httpPort } : {}),
|
|
@@ -96,15 +104,17 @@ function parseMailroomConfig(value) {
|
|
|
96
104
|
privateKeys,
|
|
97
105
|
};
|
|
98
106
|
}
|
|
107
|
+
function createBlobCredential(config) {
|
|
108
|
+
return config.azureManagedIdentityClientId
|
|
109
|
+
? new identity_1.DefaultAzureCredential({ managedIdentityClientId: config.azureManagedIdentityClientId })
|
|
110
|
+
: new identity_1.DefaultAzureCredential();
|
|
111
|
+
}
|
|
99
112
|
function createMailroomStore(config, agentName) {
|
|
100
113
|
if (config.azureAccountUrl) {
|
|
101
|
-
const credential = config.azureManagedIdentityClientId
|
|
102
|
-
? new identity_1.DefaultAzureCredential({ managedIdentityClientId: config.azureManagedIdentityClientId })
|
|
103
|
-
: new identity_1.DefaultAzureCredential();
|
|
104
114
|
const containerName = config.azureContainer ?? "mailroom";
|
|
105
115
|
return {
|
|
106
116
|
store: new blob_store_1.AzureBlobMailroomStore({
|
|
107
|
-
serviceClient: new storage_blob_1.BlobServiceClient(config.azureAccountUrl,
|
|
117
|
+
serviceClient: new storage_blob_1.BlobServiceClient(config.azureAccountUrl, createBlobCredential(config)),
|
|
108
118
|
containerName,
|
|
109
119
|
}),
|
|
110
120
|
storeKind: "azure-blob",
|
|
@@ -118,6 +128,23 @@ function createMailroomStore(config, agentName) {
|
|
|
118
128
|
storeLabel: storePath,
|
|
119
129
|
};
|
|
120
130
|
}
|
|
131
|
+
async function readMailroomRegistry(config) {
|
|
132
|
+
if (config.registryPath) {
|
|
133
|
+
return JSON.parse(fs.readFileSync(config.registryPath, "utf-8"));
|
|
134
|
+
}
|
|
135
|
+
const registryAzureAccountUrl = config.registryAzureAccountUrl ?? config.azureAccountUrl;
|
|
136
|
+
const registryContainer = config.registryContainer ?? config.azureContainer ?? "mailroom";
|
|
137
|
+
const registryBlob = config.registryBlob;
|
|
138
|
+
if (!registryAzureAccountUrl || !registryBlob) {
|
|
139
|
+
throw new Error("mailroom config is missing registryPath or hosted registry coordinates");
|
|
140
|
+
}
|
|
141
|
+
const serviceClient = new storage_blob_1.BlobServiceClient(registryAzureAccountUrl, createBlobCredential(config));
|
|
142
|
+
const blobClient = serviceClient.getContainerClient(registryContainer).getBlockBlobClient(registryBlob);
|
|
143
|
+
if (!await blobClient.exists()) {
|
|
144
|
+
throw new Error(`mailroom registry blob not found: ${registryAzureAccountUrl}/${registryContainer}/${registryBlob}`);
|
|
145
|
+
}
|
|
146
|
+
return JSON.parse((await blobClient.downloadToBuffer()).toString("utf-8"));
|
|
147
|
+
}
|
|
121
148
|
function resolveMailroomReader(agentName = (0, identity_2.getAgentName)()) {
|
|
122
149
|
const runtime = (0, runtime_credentials_1.readRuntimeCredentialConfig)(agentName);
|
|
123
150
|
if (!runtime.ok) {
|