@ouro.bot/cli 0.1.0-alpha.450 → 0.1.0-alpha.451

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.
@@ -0,0 +1,230 @@
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
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.FileMailroomStore = void 0;
37
+ exports.ingestRawMailToStore = ingestRawMailToStore;
38
+ exports.decryptMessages = decryptMessages;
39
+ const fs = __importStar(require("node:fs"));
40
+ const path = __importStar(require("node:path"));
41
+ const runtime_1 = require("../nerves/runtime");
42
+ const core_1 = require("./core");
43
+ function ensureDir(dir) {
44
+ fs.mkdirSync(dir, { recursive: true });
45
+ }
46
+ function readJson(filePath) {
47
+ try {
48
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ function writeJson(filePath, value) {
55
+ ensureDir(path.dirname(filePath));
56
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
57
+ }
58
+ function compareNewestFirst(left, right) {
59
+ return Date.parse(right.receivedAt) - Date.parse(left.receivedAt);
60
+ }
61
+ class FileMailroomStore {
62
+ rootDir;
63
+ constructor(options) {
64
+ this.rootDir = options.rootDir;
65
+ ensureDir(this.messagesDir);
66
+ ensureDir(this.rawDir);
67
+ ensureDir(this.logsDir);
68
+ (0, runtime_1.emitNervesEvent)({
69
+ component: "senses",
70
+ event: "senses.mail_file_store_init",
71
+ message: "file mailroom store initialized",
72
+ meta: { rootDir: this.rootDir },
73
+ });
74
+ }
75
+ get messagesDir() {
76
+ return path.join(this.rootDir, "messages");
77
+ }
78
+ get rawDir() {
79
+ return path.join(this.rootDir, "raw");
80
+ }
81
+ get logsDir() {
82
+ return path.join(this.rootDir, "access-log");
83
+ }
84
+ messagePath(id) {
85
+ return path.join(this.messagesDir, `${id}.json`);
86
+ }
87
+ rawPath(objectName) {
88
+ return path.join(this.rootDir, objectName);
89
+ }
90
+ accessLogPath(agentId) {
91
+ return path.join(this.logsDir, `${agentId}.jsonl`);
92
+ }
93
+ async putRawMessage(input) {
94
+ const { message, rawPayload } = await (0, core_1.buildStoredMailMessage)(input);
95
+ const existing = readJson(this.messagePath(message.id));
96
+ if (existing) {
97
+ (0, runtime_1.emitNervesEvent)({
98
+ component: "senses",
99
+ event: "senses.mail_store_dedupe",
100
+ message: "mailroom store deduped existing message",
101
+ meta: { id: message.id, agentId: message.agentId },
102
+ });
103
+ return { created: false, message: existing };
104
+ }
105
+ writeJson(this.rawPath(message.rawObject), rawPayload);
106
+ writeJson(this.messagePath(message.id), message);
107
+ (0, runtime_1.emitNervesEvent)({
108
+ component: "senses",
109
+ event: "senses.mail_store_message_written",
110
+ message: "mailroom store wrote message",
111
+ meta: { id: message.id, agentId: message.agentId },
112
+ });
113
+ return { created: true, message };
114
+ }
115
+ async getMessage(id) {
116
+ const message = readJson(this.messagePath(id));
117
+ (0, runtime_1.emitNervesEvent)({
118
+ component: "senses",
119
+ event: "senses.mail_store_message_read",
120
+ message: "mailroom store read message",
121
+ meta: { id, found: message !== null },
122
+ });
123
+ return message;
124
+ }
125
+ async listMessages(filters) {
126
+ const messages = fs.readdirSync(this.messagesDir)
127
+ .filter((name) => name.endsWith(".json"))
128
+ .map((name) => readJson(path.join(this.messagesDir, name)))
129
+ .filter((message) => message !== null)
130
+ .filter((message) => message.agentId === filters.agentId)
131
+ .filter((message) => filters.placement ? message.placement === filters.placement : true)
132
+ .filter((message) => filters.compartmentKind ? message.compartmentKind === filters.compartmentKind : true)
133
+ .filter((message) => filters.source ? message.source === filters.source : true)
134
+ .sort(compareNewestFirst)
135
+ .slice(0, filters.limit ?? 20);
136
+ (0, runtime_1.emitNervesEvent)({
137
+ component: "senses",
138
+ event: "senses.mail_store_messages_listed",
139
+ message: "mailroom store listed messages",
140
+ meta: { agentId: filters.agentId, count: messages.length },
141
+ });
142
+ return messages;
143
+ }
144
+ async readRawPayload(objectName) {
145
+ const payload = readJson(this.rawPath(objectName));
146
+ (0, runtime_1.emitNervesEvent)({
147
+ component: "senses",
148
+ event: "senses.mail_store_raw_read",
149
+ message: "mailroom store read raw payload",
150
+ meta: { objectName, found: payload !== null },
151
+ });
152
+ return payload;
153
+ }
154
+ async recordAccess(entry) {
155
+ const complete = {
156
+ ...entry,
157
+ id: `access_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
158
+ accessedAt: new Date().toISOString(),
159
+ };
160
+ ensureDir(this.logsDir);
161
+ fs.appendFileSync(this.accessLogPath(entry.agentId), `${JSON.stringify(complete)}\n`, "utf-8");
162
+ (0, runtime_1.emitNervesEvent)({
163
+ component: "senses",
164
+ event: "senses.mail_access_recorded",
165
+ message: "mail access recorded",
166
+ meta: { agentId: entry.agentId, messageId: entry.messageId ?? null, tool: entry.tool },
167
+ });
168
+ return complete;
169
+ }
170
+ async listAccessLog(agentId) {
171
+ const filePath = this.accessLogPath(agentId);
172
+ if (!fs.existsSync(filePath)) {
173
+ (0, runtime_1.emitNervesEvent)({
174
+ component: "senses",
175
+ event: "senses.mail_access_log_listed",
176
+ message: "mail access log listed",
177
+ meta: { agentId, count: 0 },
178
+ });
179
+ return [];
180
+ }
181
+ const entries = fs.readFileSync(filePath, "utf-8")
182
+ .split(/\r?\n/)
183
+ .filter(Boolean)
184
+ .map((line) => JSON.parse(line));
185
+ (0, runtime_1.emitNervesEvent)({
186
+ component: "senses",
187
+ event: "senses.mail_access_log_listed",
188
+ message: "mail access log listed",
189
+ meta: { agentId, count: entries.length },
190
+ });
191
+ return entries;
192
+ }
193
+ }
194
+ exports.FileMailroomStore = FileMailroomStore;
195
+ async function ingestRawMailToStore(input) {
196
+ const { resolveMailAddress } = await Promise.resolve().then(() => __importStar(require("./core")));
197
+ const accepted = [];
198
+ const rejectedRecipients = [];
199
+ for (const recipient of input.envelope.rcptTo) {
200
+ const resolved = resolveMailAddress(input.registry, recipient);
201
+ if (!resolved) {
202
+ rejectedRecipients.push(recipient);
203
+ continue;
204
+ }
205
+ const result = await input.store.putRawMessage({
206
+ resolved,
207
+ envelope: input.envelope,
208
+ rawMime: input.rawMime,
209
+ receivedAt: input.receivedAt,
210
+ });
211
+ accepted.push(result.message);
212
+ }
213
+ (0, runtime_1.emitNervesEvent)({
214
+ component: "senses",
215
+ event: "senses.mail_ingest_complete",
216
+ message: "mail ingest completed",
217
+ meta: { accepted: accepted.length, rejected: rejectedRecipients.length },
218
+ });
219
+ return { accepted, rejectedRecipients };
220
+ }
221
+ function decryptMessages(messages, privateKeys) {
222
+ const decrypted = messages.map((message) => (0, core_1.decryptStoredMailMessage)(message, privateKeys));
223
+ (0, runtime_1.emitNervesEvent)({
224
+ component: "senses",
225
+ event: "senses.mail_messages_decrypted",
226
+ message: "mail messages decrypted",
227
+ meta: { count: decrypted.length },
228
+ });
229
+ return decrypted;
230
+ }
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.splitMboxMessages = splitMboxMessages;
4
+ exports.importMboxToStore = importMboxToStore;
5
+ const mailparser_1 = require("mailparser");
6
+ const runtime_1 = require("../nerves/runtime");
7
+ const core_1 = require("./core");
8
+ function splitMboxMessages(rawMbox) {
9
+ const text = rawMbox.toString("utf-8");
10
+ const separators = [...text.matchAll(/^From [^\r\n]*(?:\r?\n)/gm)];
11
+ if (separators.length === 0) {
12
+ const trimmed = text.trim();
13
+ return trimmed ? [Buffer.from(text, "utf-8")] : [];
14
+ }
15
+ const messages = separators
16
+ .map((match, index) => {
17
+ const start = match.index + match[0].length;
18
+ const end = index + 1 < separators.length ? separators[index + 1].index : text.length;
19
+ return text.slice(start, end).replace(/\r?\n$/, "");
20
+ })
21
+ .filter((message) => message.trim().length > 0)
22
+ .map((message) => Buffer.from(message, "utf-8"));
23
+ (0, runtime_1.emitNervesEvent)({
24
+ component: "senses",
25
+ event: "senses.mail_mbox_split",
26
+ message: "mbox payload split into messages",
27
+ meta: { messages: messages.length },
28
+ });
29
+ return messages;
30
+ }
31
+ function findSourceGrant(input) {
32
+ const ownerEmail = input.ownerEmail ? (0, core_1.normalizeMailAddress)(input.ownerEmail) : undefined;
33
+ const source = input.source?.trim().toLowerCase();
34
+ const grants = input.registry.sourceGrants.filter((grant) => {
35
+ if (!grant.enabled || grant.agentId !== input.agentId)
36
+ return false;
37
+ if (ownerEmail && (0, core_1.normalizeMailAddress)(grant.ownerEmail) !== ownerEmail)
38
+ return false;
39
+ if (source && grant.source.toLowerCase() !== source)
40
+ return false;
41
+ return true;
42
+ });
43
+ if (grants.length === 0) {
44
+ throw new Error(`No enabled Mailroom source grant found for ${input.agentId}${ownerEmail ? ` owner ${ownerEmail}` : ""}${source ? ` source ${source}` : ""}`);
45
+ }
46
+ if (grants.length > 1 && !ownerEmail && !source) {
47
+ throw new Error(`Multiple source grants found for ${input.agentId}; pass --owner-email or --source to choose one`);
48
+ }
49
+ return grants[0];
50
+ }
51
+ async function envelopeForMboxMessage(rawMessage, grant) {
52
+ const parsed = await (0, mailparser_1.simpleParser)(rawMessage);
53
+ const mailFrom = parsed.from?.value?.[0]?.address;
54
+ return {
55
+ mailFrom: mailFrom ? (0, core_1.normalizeMailAddress)(mailFrom) : "",
56
+ rcptTo: [(0, core_1.normalizeMailAddress)(grant.aliasAddress)],
57
+ remoteAddress: "mbox-import",
58
+ };
59
+ }
60
+ async function importMboxToStore(input) {
61
+ const agentId = input.agentId.toLowerCase();
62
+ const sourceGrant = findSourceGrant({
63
+ registry: input.registry,
64
+ agentId,
65
+ ownerEmail: input.ownerEmail,
66
+ source: input.source,
67
+ });
68
+ const resolved = (0, core_1.resolveMailAddress)(input.registry, sourceGrant.aliasAddress);
69
+ /* v8 ignore start -- findSourceGrant and resolveMailAddress share the same registry; this is a corruption guard. @preserve */
70
+ if (!resolved) {
71
+ throw new Error(`Source grant alias ${sourceGrant.aliasAddress} is not resolvable`);
72
+ }
73
+ /* v8 ignore stop */
74
+ let imported = 0;
75
+ let duplicates = 0;
76
+ const messages = [];
77
+ const rawMessages = splitMboxMessages(input.rawMbox);
78
+ for (const rawMessage of rawMessages) {
79
+ const result = await input.store.putRawMessage({
80
+ resolved,
81
+ envelope: await envelopeForMboxMessage(rawMessage, sourceGrant),
82
+ rawMime: rawMessage,
83
+ receivedAt: input.importedAt,
84
+ });
85
+ messages.push(result.message);
86
+ if (result.created)
87
+ imported += 1;
88
+ else
89
+ duplicates += 1;
90
+ }
91
+ (0, runtime_1.emitNervesEvent)({
92
+ component: "senses",
93
+ event: "senses.mail_mbox_imported",
94
+ message: "mbox mail imported",
95
+ meta: { agentId, scanned: rawMessages.length, imported, duplicates, grantId: sourceGrant.grantId },
96
+ });
97
+ return {
98
+ agentId,
99
+ sourceGrant,
100
+ scanned: rawMessages.length,
101
+ imported,
102
+ duplicates,
103
+ messages,
104
+ };
105
+ }
@@ -0,0 +1,150 @@
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
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.parseMailroomConfig = parseMailroomConfig;
37
+ exports.resolveMailroomReader = resolveMailroomReader;
38
+ const path = __importStar(require("node:path"));
39
+ const storage_blob_1 = require("@azure/storage-blob");
40
+ const identity_1 = require("@azure/identity");
41
+ const runtime_1 = require("../nerves/runtime");
42
+ const identity_2 = require("../heart/identity");
43
+ const runtime_credentials_1 = require("../heart/runtime-credentials");
44
+ const blob_store_1 = require("./blob-store");
45
+ const file_store_1 = require("./file-store");
46
+ function isRecord(value) {
47
+ return !!value && typeof value === "object" && !Array.isArray(value);
48
+ }
49
+ function textField(value, key) {
50
+ const raw = value[key];
51
+ return typeof raw === "string" && raw.trim() ? raw.trim() : undefined;
52
+ }
53
+ function parseMailroomConfig(value) {
54
+ if (!isRecord(value))
55
+ return null;
56
+ const mailboxAddress = textField(value, "mailboxAddress");
57
+ if (!mailboxAddress)
58
+ return null;
59
+ if (!isRecord(value.privateKeys))
60
+ return null;
61
+ const privateKeys = Object.fromEntries(Object.entries(value.privateKeys).filter((entry) => {
62
+ return typeof entry[0] === "string" && typeof entry[1] === "string" && entry[1].trim().length > 0;
63
+ }));
64
+ if (Object.keys(privateKeys).length === 0)
65
+ return null;
66
+ const storePath = textField(value, "storePath");
67
+ const azureAccountUrl = textField(value, "azureAccountUrl");
68
+ const azureContainer = textField(value, "azureContainer");
69
+ const azureManagedIdentityClientId = textField(value, "azureManagedIdentityClientId");
70
+ return {
71
+ mailboxAddress,
72
+ ...(storePath ? { storePath } : {}),
73
+ ...(azureAccountUrl ? { azureAccountUrl } : {}),
74
+ ...(azureContainer ? { azureContainer } : {}),
75
+ ...(azureManagedIdentityClientId ? { azureManagedIdentityClientId } : {}),
76
+ privateKeys,
77
+ };
78
+ }
79
+ function createMailroomStore(config, agentName) {
80
+ if (config.azureAccountUrl) {
81
+ const credential = config.azureManagedIdentityClientId
82
+ ? new identity_1.DefaultAzureCredential({ managedIdentityClientId: config.azureManagedIdentityClientId })
83
+ : new identity_1.DefaultAzureCredential();
84
+ const containerName = config.azureContainer ?? "mailroom";
85
+ return {
86
+ store: new blob_store_1.AzureBlobMailroomStore({
87
+ serviceClient: new storage_blob_1.BlobServiceClient(config.azureAccountUrl, credential),
88
+ containerName,
89
+ }),
90
+ storeKind: "azure-blob",
91
+ storeLabel: `${config.azureAccountUrl}/${containerName}`,
92
+ };
93
+ }
94
+ const storePath = config.storePath ?? path.join((0, identity_2.getAgentRoot)(agentName), "state", "mailroom");
95
+ return {
96
+ store: new file_store_1.FileMailroomStore({ rootDir: storePath }),
97
+ storeKind: "file",
98
+ storeLabel: storePath,
99
+ };
100
+ }
101
+ function resolveMailroomReader(agentName = (0, identity_2.getAgentName)()) {
102
+ const runtime = (0, runtime_credentials_1.readRuntimeCredentialConfig)(agentName);
103
+ if (!runtime.ok) {
104
+ const result = {
105
+ ok: false,
106
+ agentName,
107
+ reason: "auth-required",
108
+ error: `AUTH_REQUIRED:mailroom -- Mail is not available because ${runtime.itemPath} is ${runtime.reason}. Run 'ouro connect mail --agent ${agentName}' after the vault is unlocked.`,
109
+ };
110
+ (0, runtime_1.emitNervesEvent)({
111
+ component: "senses",
112
+ event: "senses.mail_reader_resolved",
113
+ message: "mailroom reader resolved",
114
+ meta: { agentName, status: result.reason },
115
+ });
116
+ return result;
117
+ }
118
+ const config = parseMailroomConfig(runtime.config.mailroom);
119
+ if (!config) {
120
+ const result = {
121
+ ok: false,
122
+ agentName,
123
+ reason: "misconfigured",
124
+ error: `AUTH_REQUIRED:mailroom -- Missing mailroom mailbox/private key config in vault runtime/config for ${agentName}.`,
125
+ };
126
+ (0, runtime_1.emitNervesEvent)({
127
+ component: "senses",
128
+ event: "senses.mail_reader_resolved",
129
+ message: "mailroom reader resolved",
130
+ meta: { agentName, status: result.reason },
131
+ });
132
+ return result;
133
+ }
134
+ const store = createMailroomStore(config, agentName);
135
+ const result = {
136
+ ok: true,
137
+ agentName,
138
+ config,
139
+ store: store.store,
140
+ storeKind: store.storeKind,
141
+ storeLabel: store.storeLabel,
142
+ };
143
+ (0, runtime_1.emitNervesEvent)({
144
+ component: "senses",
145
+ event: "senses.mail_reader_resolved",
146
+ message: "mailroom reader resolved",
147
+ meta: { agentName, status: "ready", mailboxAddress: config.mailboxAddress, storeKind: store.storeKind },
148
+ });
149
+ return result;
150
+ }
@@ -0,0 +1,176 @@
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
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createMailroomSmtpServer = createMailroomSmtpServer;
37
+ exports.createMailroomHealthServer = createMailroomHealthServer;
38
+ exports.startMailroomIngress = startMailroomIngress;
39
+ const http = __importStar(require("node:http"));
40
+ const smtp_server_1 = require("smtp-server");
41
+ const runtime_1 = require("../nerves/runtime");
42
+ const core_1 = require("./core");
43
+ const file_store_1 = require("./file-store");
44
+ function collectStream(stream, maxBytes) {
45
+ return new Promise((resolve, reject) => {
46
+ const chunks = [];
47
+ let total = 0;
48
+ stream.on("data", (chunk) => {
49
+ total += chunk.byteLength;
50
+ if (total > maxBytes) {
51
+ reject(new Error(`message exceeds max size ${maxBytes}`));
52
+ stream.destroy();
53
+ return;
54
+ }
55
+ chunks.push(chunk);
56
+ });
57
+ stream.on("error", reject);
58
+ stream.on("end", () => resolve(Buffer.concat(chunks)));
59
+ });
60
+ }
61
+ function sessionRecipients(session) {
62
+ /* v8 ignore next -- smtp-server supplies rcptTo during DATA; fallback is a defensive harness edge. @preserve */
63
+ return (session.envelope.rcptTo ?? []).map((address) => (0, core_1.normalizeMailAddress)(address.address));
64
+ }
65
+ function createMailroomSmtpServer(options) {
66
+ const maxMessageBytes = options.maxMessageBytes ?? 25 * 1024 * 1024;
67
+ const server = new smtp_server_1.SMTPServer({
68
+ disabledCommands: ["AUTH", "STARTTLS"],
69
+ logger: false,
70
+ onRcptTo(address, _session, callback) {
71
+ const normalized = (0, core_1.normalizeMailAddress)(address.address);
72
+ const resolved = (0, core_1.resolveMailAddress)(options.registry, normalized);
73
+ if (!resolved) {
74
+ (0, runtime_1.emitNervesEvent)({
75
+ component: "senses",
76
+ event: "senses.mail_smtp_recipient_rejected",
77
+ message: "smtp recipient rejected",
78
+ meta: { address: normalized },
79
+ });
80
+ const error = new Error(`unknown recipient ${normalized}`);
81
+ error.responseCode = 550;
82
+ callback(error);
83
+ return;
84
+ }
85
+ (0, runtime_1.emitNervesEvent)({
86
+ component: "senses",
87
+ event: "senses.mail_smtp_recipient_accepted",
88
+ message: "smtp recipient accepted",
89
+ meta: { address: normalized, agentId: resolved.agentId },
90
+ });
91
+ callback();
92
+ },
93
+ async onData(stream, session, callback) {
94
+ try {
95
+ const raw = await collectStream(stream, maxMessageBytes);
96
+ const mailFrom = session.envelope.mailFrom;
97
+ /* v8 ignore next -- smtp-server exposes normal and null senders as address objects; false/undefined are defensive direct-call states. @preserve */
98
+ const rawMailFrom = mailFrom === false ? "" : mailFrom?.address ?? "";
99
+ const envelope = {
100
+ mailFrom: rawMailFrom ? (0, core_1.normalizeMailAddress)(rawMailFrom) : "",
101
+ rcptTo: sessionRecipients(session),
102
+ /* v8 ignore next -- smtp-server network sessions carry remoteAddress; fallback is a defensive direct-call edge. @preserve */
103
+ ...(session.remoteAddress ? { remoteAddress: session.remoteAddress } : {}),
104
+ };
105
+ const result = await (0, file_store_1.ingestRawMailToStore)({
106
+ registry: options.registry,
107
+ store: options.store,
108
+ envelope,
109
+ rawMime: raw,
110
+ });
111
+ (0, runtime_1.emitNervesEvent)({
112
+ component: "senses",
113
+ event: "senses.mail_smtp_data_stored",
114
+ message: "smtp data stored",
115
+ meta: { accepted: result.accepted.length, rejected: result.rejectedRecipients.length },
116
+ });
117
+ callback();
118
+ }
119
+ catch (error) {
120
+ (0, runtime_1.emitNervesEvent)({
121
+ level: "error",
122
+ component: "senses",
123
+ event: "senses.mail_smtp_data_error",
124
+ message: "smtp data handling failed",
125
+ meta: { error: error instanceof Error ? error.message : String(error) },
126
+ });
127
+ callback(error instanceof Error ? error : new Error(String(error)));
128
+ }
129
+ },
130
+ });
131
+ (0, runtime_1.emitNervesEvent)({
132
+ component: "senses",
133
+ event: "senses.mail_smtp_server_created",
134
+ message: "mailroom smtp server created",
135
+ meta: { maxMessageBytes },
136
+ });
137
+ return server;
138
+ }
139
+ function createMailroomHealthServer(registry) {
140
+ const server = http.createServer((_request, response) => {
141
+ const body = JSON.stringify({
142
+ ok: true,
143
+ service: "ouro-mailroom",
144
+ domain: registry.domain,
145
+ mailboxes: registry.mailboxes.length,
146
+ sourceGrants: registry.sourceGrants.length,
147
+ });
148
+ response.writeHead(200, {
149
+ "content-type": "application/json",
150
+ "content-length": Buffer.byteLength(body),
151
+ });
152
+ response.end(body);
153
+ });
154
+ (0, runtime_1.emitNervesEvent)({
155
+ component: "senses",
156
+ event: "senses.mail_health_server_created",
157
+ message: "mailroom health server created",
158
+ meta: { domain: registry.domain },
159
+ });
160
+ return server;
161
+ }
162
+ function startMailroomIngress(options) {
163
+ const smtp = createMailroomSmtpServer(options);
164
+ const health = createMailroomHealthServer(options.registry);
165
+ /* v8 ignore next -- production/container entrypoints may omit host; unit tests bind loopback explicitly. @preserve */
166
+ const host = options.host ?? "0.0.0.0";
167
+ smtp.listen(options.smtpPort, host);
168
+ health.listen(options.httpPort, host);
169
+ (0, runtime_1.emitNervesEvent)({
170
+ component: "senses",
171
+ event: "senses.mail_ingress_started",
172
+ message: "mailroom ingress started",
173
+ meta: { smtpPort: options.smtpPort, httpPort: options.httpPort, host },
174
+ });
175
+ return { smtp, health };
176
+ }
@@ -46,6 +46,15 @@ const CHANNEL_CAPABILITIES = {
46
46
  supportsRichCards: false,
47
47
  maxMessageLength: Infinity,
48
48
  },
49
+ mail: {
50
+ channel: "mail",
51
+ senseType: "open",
52
+ availableIntegrations: [],
53
+ supportsMarkdown: false,
54
+ supportsStreaming: false,
55
+ supportsRichCards: false,
56
+ maxMessageLength: Infinity,
57
+ },
49
58
  inner: {
50
59
  channel: "inner",
51
60
  senseType: "internal",
@@ -7,7 +7,7 @@ exports.isIdentityProvider = isIdentityProvider;
7
7
  exports.isIntegration = isIntegration;
8
8
  exports.isTrustedLevel = isTrustedLevel;
9
9
  const runtime_1 = require("../../nerves/runtime");
10
- const IDENTITY_PROVIDERS = new Set(["aad", "local", "teams-conversation", "imessage-handle"]);
10
+ const IDENTITY_PROVIDERS = new Set(["aad", "local", "teams-conversation", "imessage-handle", "email-address"]);
11
11
  function isIdentityProvider(value) {
12
12
  (0, runtime_1.emitNervesEvent)({
13
13
  component: "friends",