@agenticmail/core 0.2.26
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 +35 -0
- package/README.md +465 -0
- package/REFERENCE.md +1219 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/email-worker-template-BOJPKCVB.js +39 -0
- package/dist/index.d.ts +1365 -0
- package/dist/index.js +5816 -0
- package/package.json +69 -0
package/REFERENCE.md
ADDED
|
@@ -0,0 +1,1219 @@
|
|
|
1
|
+
# @agenticmail/core — Technical Reference
|
|
2
|
+
|
|
3
|
+
Complete API reference for developers and AI agents. Every exported class, function, type, constant, method signature, configuration option, database table, and detection rule.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Exports Overview](#exports-overview)
|
|
10
|
+
- [Configuration](#configuration)
|
|
11
|
+
- [Account Management](#account-management)
|
|
12
|
+
- [Mail Operations](#mail-operations)
|
|
13
|
+
- [Email Parsing](#email-parsing)
|
|
14
|
+
- [Inbox Watching](#inbox-watching)
|
|
15
|
+
- [Spam Filter](#spam-filter)
|
|
16
|
+
- [Outbound Guard](#outbound-guard)
|
|
17
|
+
- [Email Sanitizer](#email-sanitizer)
|
|
18
|
+
- [Gateway Manager](#gateway-manager)
|
|
19
|
+
- [Relay Gateway](#relay-gateway)
|
|
20
|
+
- [Cloudflare Client](#cloudflare-client)
|
|
21
|
+
- [Tunnel Manager](#tunnel-manager)
|
|
22
|
+
- [DNS Configurator](#dns-configurator)
|
|
23
|
+
- [Domain Purchaser](#domain-purchaser)
|
|
24
|
+
- [Relay Bridge](#relay-bridge)
|
|
25
|
+
- [Stalwart Admin](#stalwart-admin)
|
|
26
|
+
- [Domain Manager](#domain-manager)
|
|
27
|
+
- [Storage](#storage)
|
|
28
|
+
- [Search Index](#search-index)
|
|
29
|
+
- [Setup](#setup)
|
|
30
|
+
- [Database Schema](#database-schema)
|
|
31
|
+
- [Constants](#constants)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Exports Overview
|
|
36
|
+
|
|
37
|
+
87 items exported from the barrel (`src/index.ts`):
|
|
38
|
+
|
|
39
|
+
### Classes (17)
|
|
40
|
+
`AgenticMailClient`, `AccountManager`, `AgentDeletionService`, `MailSender`, `MailReceiver`, `InboxWatcher`, `GatewayManager`, `RelayGateway`, `CloudflareClient`, `TunnelManager`, `DNSConfigurator`, `DomainPurchaser`, `RelayBridge`, `StalwartAdmin`, `DomainManager`, `EmailSearchIndex`, `SetupManager`, `DependencyChecker`, `DependencyInstaller`
|
|
41
|
+
|
|
42
|
+
### Functions (9)
|
|
43
|
+
`resolveConfig`, `ensureDataDir`, `saveConfig`, `parseEmail`, `scoreEmail`, `isInternalEmail`, `scanOutboundEmail`, `buildInboundSecurityAdvisory`, `sanitizeEmail`, `getDatabase`, `closeDatabase`, `createTestDatabase`, `startRelayBridge`
|
|
44
|
+
|
|
45
|
+
### Types & Interfaces (55+)
|
|
46
|
+
`AgenticMailConfig`, `AgenticMailClientOptions`, `Agent`, `CreateAgentOptions`, `AgentRole`, `DeletionReport`, `DeletionSummary`, `ArchivedEmail`, `ArchiveAndDeleteOptions`, `SendMailOptions`, `SendResult`, `SendResultWithRaw`, `Attachment`, `EmailEnvelope`, `AddressInfo`, `ParsedEmail`, `ParsedAttachment`, `MailboxInfo`, `SearchCriteria`, `MailSenderOptions`, `MailReceiverOptions`, `FolderInfo`, `InboxWatcherOptions`, `InboxEvent`, `InboxNewEvent`, `InboxExpungeEvent`, `InboxFlagsEvent`, `WatcherOptions`, `SpamResult`, `SpamRuleMatch`, `SpamCategory`, `SanitizeResult`, `SanitizeDetection`, `OutboundScanResult`, `OutboundScanInput`, `OutboundWarning`, `OutboundCategory`, `Severity`, `SecurityAdvisory`, `AttachmentAdvisory`, `LinkAdvisory`, `GatewayMode`, `GatewayConfig`, `GatewayStatus`, `GatewayManagerOptions`, `LocalSmtpConfig`, `RelayConfig`, `RelayProvider`, `DomainModeConfig`, `PurchasedDomain`, `InboundEmail`, `DomainSearchResult`, `DomainPurchaseResult`, `DnsSetupResult`, `TunnelConfig`, `RelayBridgeOptions`, `StalwartAdminOptions`, `StalwartPrincipal`, `DomainInfo`, `DnsRecord`, `DomainSetupResult`, `SearchableEmail`, `DependencyStatus`, `InstallProgress`, `SetupConfig`, `SetupResult`
|
|
47
|
+
|
|
48
|
+
### Constants (5)
|
|
49
|
+
`AGENT_ROLES`, `DEFAULT_AGENT_ROLE`, `DEFAULT_AGENT_NAME`, `SPAM_THRESHOLD`, `WARNING_THRESHOLD`, `RELAY_PRESETS`
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Configuration
|
|
54
|
+
|
|
55
|
+
### `resolveConfig(overrides?: Partial<AgenticMailConfig>): AgenticMailConfig`
|
|
56
|
+
Loads configuration from environment variables, config file, and programmatic overrides (in that priority order).
|
|
57
|
+
|
|
58
|
+
### `ensureDataDir(config: AgenticMailConfig): void`
|
|
59
|
+
Creates the data directory (`~/.agenticmail/` by default) if it doesn't exist.
|
|
60
|
+
|
|
61
|
+
### `saveConfig(config: AgenticMailConfig): void`
|
|
62
|
+
Saves configuration to `{dataDir}/config.json` with file mode 0600.
|
|
63
|
+
|
|
64
|
+
### `AgenticMailConfig`
|
|
65
|
+
```typescript
|
|
66
|
+
interface AgenticMailConfig {
|
|
67
|
+
masterKey: string;
|
|
68
|
+
stalwart: { url: string; adminUser: string; adminPassword: string };
|
|
69
|
+
smtp: { host: string; port: number };
|
|
70
|
+
imap: { host: string; port: number };
|
|
71
|
+
api: { port: number; host: string };
|
|
72
|
+
gateway?: { mode: GatewayMode; relay?: RelayConfig; domain?: DomainModeConfig };
|
|
73
|
+
dataDir: string;
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Account Management
|
|
80
|
+
|
|
81
|
+
### `AccountManager`
|
|
82
|
+
```typescript
|
|
83
|
+
class AccountManager {
|
|
84
|
+
constructor(db: Database.Database, stalwart: StalwartAdmin)
|
|
85
|
+
|
|
86
|
+
create(options: CreateAgentOptions): Promise<Agent>
|
|
87
|
+
getById(id: string): Promise<Agent | null>
|
|
88
|
+
getByApiKey(apiKey: string): Promise<Agent | null>
|
|
89
|
+
getByName(name: string): Promise<Agent | null>
|
|
90
|
+
getByRole(role: AgentRole): Promise<Agent[]>
|
|
91
|
+
list(): Promise<Agent[]>
|
|
92
|
+
delete(id: string): Promise<boolean>
|
|
93
|
+
updateMetadata(id: string, metadata: Record<string, unknown>): Promise<Agent | null>
|
|
94
|
+
getCredentials(id: string): Promise<{ email; password; principal; smtpHost; smtpPort; imapHost; imapPort } | null>
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**`create()`** — Validates name against `/^[a-zA-Z0-9._-]+$/`, generates UUID + API key (`ak_` + 48 hex) + password (32 hex), creates Stalwart principal, inserts DB record. Rolls back Stalwart on DB failure.
|
|
99
|
+
|
|
100
|
+
**`updateMetadata()`** — Merge semantics. Preserves internal `_`-prefixed fields. Users cannot overwrite `_password` or `_gateway`.
|
|
101
|
+
|
|
102
|
+
**`getCredentials()`** — Returns hardcoded localhost SMTP(587)/IMAP(143) with password from `metadata._password`.
|
|
103
|
+
|
|
104
|
+
### Types
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
interface Agent {
|
|
108
|
+
id: string; // UUID
|
|
109
|
+
name: string; // email-safe unique name
|
|
110
|
+
email: string; // principal@domain
|
|
111
|
+
apiKey: string; // ak_... (48 hex chars)
|
|
112
|
+
stalwartPrincipal: string; // lowercase principal name
|
|
113
|
+
createdAt: string; // ISO timestamp
|
|
114
|
+
updatedAt: string; // ISO timestamp
|
|
115
|
+
metadata: Record<string, unknown>; // flexible JSON (internal fields: _password, _gateway)
|
|
116
|
+
role: AgentRole;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface CreateAgentOptions {
|
|
120
|
+
name: string; // required, must match /^[a-zA-Z0-9._-]+$/
|
|
121
|
+
domain?: string; // default: 'localhost'
|
|
122
|
+
password?: string; // auto-generated if omitted
|
|
123
|
+
metadata?: Record<string, unknown>;
|
|
124
|
+
gateway?: 'relay' | 'domain'; // stored in metadata._gateway
|
|
125
|
+
role?: AgentRole; // default: 'secretary'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
type AgentRole = 'secretary' | 'assistant' | 'researcher' | 'writer' | 'custom';
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### `AgentDeletionService`
|
|
132
|
+
```typescript
|
|
133
|
+
class AgentDeletionService {
|
|
134
|
+
constructor(db: Database.Database, accountManager: AccountManager, config: AgenticMailConfig)
|
|
135
|
+
|
|
136
|
+
archiveAndDelete(agentId: string, options?: ArchiveAndDeleteOptions): Promise<DeletionReport>
|
|
137
|
+
getReport(deletionId: string): DeletionReport | null
|
|
138
|
+
listReports(): DeletionSummary[]
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**`archiveAndDelete()`** — Prevents deleting last agent. Connects to IMAP, archives all emails (inbox, sent, custom folders, up to 10,000 per folder), builds summary with top 10 correspondents, saves JSON to `~/.agenticmail/deletions/{name}_{timestamp}.json`, inserts DB record, then deletes agent.
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
interface DeletionReport {
|
|
146
|
+
id: string; // del_{uuid}
|
|
147
|
+
agent: { id; name; email; role; createdAt };
|
|
148
|
+
deletedAt: string;
|
|
149
|
+
deletedBy: string;
|
|
150
|
+
reason?: string;
|
|
151
|
+
emails: {
|
|
152
|
+
inbox: ArchivedEmail[];
|
|
153
|
+
sent: ArchivedEmail[];
|
|
154
|
+
other: Record<string, ArchivedEmail[]>;
|
|
155
|
+
};
|
|
156
|
+
summary: {
|
|
157
|
+
totalEmails: number;
|
|
158
|
+
inboxCount: number;
|
|
159
|
+
sentCount: number;
|
|
160
|
+
otherCount: number;
|
|
161
|
+
folders: string[];
|
|
162
|
+
firstEmailDate?: string;
|
|
163
|
+
lastEmailDate?: string;
|
|
164
|
+
topCorrespondents: Array<{ address: string; count: number }>;
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Mail Operations
|
|
172
|
+
|
|
173
|
+
### `MailSender`
|
|
174
|
+
```typescript
|
|
175
|
+
class MailSender {
|
|
176
|
+
constructor(options: MailSenderOptions)
|
|
177
|
+
|
|
178
|
+
send(mail: SendMailOptions): Promise<SendResultWithRaw>
|
|
179
|
+
verify(): Promise<boolean>
|
|
180
|
+
close(): void
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Timeouts:** connectionTimeout=10s, greetingTimeout=10s, socketTimeout=15s. TLS: rejectUnauthorized=false.
|
|
185
|
+
|
|
186
|
+
**`send()`** — Builds RFC822 via MailComposer, returns messageId + envelope + raw Buffer. Supports `fromName` for display name in From header.
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
interface MailSenderOptions {
|
|
190
|
+
host: string;
|
|
191
|
+
port: number;
|
|
192
|
+
email: string; // From address
|
|
193
|
+
password: string;
|
|
194
|
+
authUser?: string; // defaults to email
|
|
195
|
+
secure?: boolean; // default: false
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
interface SendMailOptions {
|
|
199
|
+
to: string | string[];
|
|
200
|
+
subject: string;
|
|
201
|
+
text?: string;
|
|
202
|
+
html?: string;
|
|
203
|
+
cc?: string | string[];
|
|
204
|
+
bcc?: string | string[];
|
|
205
|
+
replyTo?: string;
|
|
206
|
+
inReplyTo?: string; // Message-ID for threading
|
|
207
|
+
references?: string[]; // ancestor Message-IDs
|
|
208
|
+
attachments?: Attachment[];
|
|
209
|
+
headers?: Record<string, string>;
|
|
210
|
+
fromName?: string; // display name in From header
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
interface Attachment {
|
|
214
|
+
filename: string;
|
|
215
|
+
content: Buffer | string;
|
|
216
|
+
contentType?: string;
|
|
217
|
+
encoding?: string; // e.g., 'base64'
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
interface SendResultWithRaw extends SendResult {
|
|
221
|
+
raw: Buffer; // RFC822 bytes for Sent folder
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### `MailReceiver`
|
|
226
|
+
```typescript
|
|
227
|
+
class MailReceiver {
|
|
228
|
+
constructor(options: MailReceiverOptions)
|
|
229
|
+
|
|
230
|
+
connect(): Promise<void>
|
|
231
|
+
disconnect(): Promise<void>
|
|
232
|
+
get usable(): boolean
|
|
233
|
+
|
|
234
|
+
// Listing
|
|
235
|
+
listEnvelopes(mailbox?: string, options?: { limit?: number; offset?: number }): Promise<EmailEnvelope[]>
|
|
236
|
+
getMailboxInfo(mailbox?: string): Promise<MailboxInfo>
|
|
237
|
+
|
|
238
|
+
// Reading
|
|
239
|
+
fetchMessage(uid: number, mailbox?: string): Promise<Buffer>
|
|
240
|
+
batchFetch(uids: number[], mailbox?: string): Promise<Map<number, Buffer>>
|
|
241
|
+
|
|
242
|
+
// Searching
|
|
243
|
+
search(criteria: SearchCriteria, mailbox?: string): Promise<number[]>
|
|
244
|
+
|
|
245
|
+
// Flags
|
|
246
|
+
markSeen(uid: number, mailbox?: string): Promise<void>
|
|
247
|
+
markUnseen(uid: number, mailbox?: string): Promise<void>
|
|
248
|
+
batchMarkSeen(uids: number[], mailbox?: string): Promise<void>
|
|
249
|
+
batchMarkUnseen(uids: number[], mailbox?: string): Promise<void>
|
|
250
|
+
|
|
251
|
+
// Delete & Move
|
|
252
|
+
deleteMessage(uid: number, mailbox?: string): Promise<void>
|
|
253
|
+
batchDelete(uids: number[], mailbox?: string): Promise<void>
|
|
254
|
+
moveMessage(uid: number, fromMailbox: string, toMailbox: string): Promise<void>
|
|
255
|
+
batchMove(uids: number[], fromMailbox: string, toMailbox: string): Promise<void>
|
|
256
|
+
|
|
257
|
+
// Folders
|
|
258
|
+
listFolders(): Promise<FolderInfo[]>
|
|
259
|
+
createFolder(path: string): Promise<void>
|
|
260
|
+
|
|
261
|
+
// Advanced
|
|
262
|
+
appendMessage(raw: Buffer, mailbox: string, flags?: string[]): Promise<void>
|
|
263
|
+
getImapClient(): ImapFlow
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**`listEnvelopes()`** — Pagination: default limit=20 (max 1000), offset=0. Returns newest first.
|
|
268
|
+
|
|
269
|
+
**`appendMessage()`** — Default flags: `['\\Seen']`. Attaches current Date.
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
interface EmailEnvelope {
|
|
273
|
+
uid: number;
|
|
274
|
+
seq: number;
|
|
275
|
+
messageId: string;
|
|
276
|
+
subject: string;
|
|
277
|
+
from: AddressInfo[];
|
|
278
|
+
to: AddressInfo[];
|
|
279
|
+
date: Date;
|
|
280
|
+
flags: Set<string>;
|
|
281
|
+
size: number;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
interface SearchCriteria {
|
|
285
|
+
from?: string;
|
|
286
|
+
to?: string;
|
|
287
|
+
subject?: string;
|
|
288
|
+
since?: Date;
|
|
289
|
+
before?: Date;
|
|
290
|
+
seen?: boolean;
|
|
291
|
+
text?: string; // body text search
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
interface FolderInfo {
|
|
295
|
+
path: string;
|
|
296
|
+
name: string;
|
|
297
|
+
specialUse?: string; // \\Sent, \\Drafts, \\Trash, etc.
|
|
298
|
+
flags: string[];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
interface MailboxInfo {
|
|
302
|
+
name: string;
|
|
303
|
+
exists: number;
|
|
304
|
+
recent: number;
|
|
305
|
+
unseen: number;
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Email Parsing
|
|
312
|
+
|
|
313
|
+
### `parseEmail(raw: Buffer | string): Promise<ParsedEmail>`
|
|
314
|
+
|
|
315
|
+
Uses mailparser's `simpleParser`. Special handling:
|
|
316
|
+
- **X-Original-From header**: If present and from address is `@localhost`, replaces with the original external sender (relay email detection).
|
|
317
|
+
- **References**: Normalizes single string to array.
|
|
318
|
+
- **Attachments**: Extracts filename (default 'unnamed'), contentType, size, content Buffer.
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
interface ParsedEmail {
|
|
322
|
+
messageId: string;
|
|
323
|
+
subject: string;
|
|
324
|
+
from: AddressInfo[];
|
|
325
|
+
to: AddressInfo[];
|
|
326
|
+
cc?: AddressInfo[];
|
|
327
|
+
replyTo?: AddressInfo[];
|
|
328
|
+
date: Date;
|
|
329
|
+
text?: string;
|
|
330
|
+
html?: string;
|
|
331
|
+
inReplyTo?: string;
|
|
332
|
+
references?: string[];
|
|
333
|
+
attachments: ParsedAttachment[];
|
|
334
|
+
headers: Map<string, string>;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
interface ParsedAttachment {
|
|
338
|
+
filename: string;
|
|
339
|
+
contentType: string;
|
|
340
|
+
size: number;
|
|
341
|
+
content: Buffer;
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## Inbox Watching
|
|
348
|
+
|
|
349
|
+
### `InboxWatcher`
|
|
350
|
+
```typescript
|
|
351
|
+
class InboxWatcher extends EventEmitter {
|
|
352
|
+
constructor(options: InboxWatcherOptions)
|
|
353
|
+
|
|
354
|
+
start(): Promise<void>
|
|
355
|
+
stop(): Promise<void>
|
|
356
|
+
isWatching(): boolean
|
|
357
|
+
|
|
358
|
+
// Events
|
|
359
|
+
on(event: 'new', listener: (e: InboxNewEvent) => void): this
|
|
360
|
+
on(event: 'expunge', listener: (e: InboxExpungeEvent) => void): this
|
|
361
|
+
on(event: 'flags', listener: (e: InboxFlagsEvent) => void): this
|
|
362
|
+
on(event: 'error', listener: (err: Error) => void): this
|
|
363
|
+
on(event: 'close', listener: () => void): this
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**`start()`** — Creates fresh ImapFlow client, connects, acquires mailbox lock (held for IDLE). When `exists` event fires (new messages), calculates range, fetches and parses all new messages, emits 'new' for each. `expunge` and `flags` events forwarded directly.
|
|
368
|
+
|
|
369
|
+
**`stop()`** — Removes all event listeners, releases lock, logs out. Idempotent.
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
interface InboxWatcherOptions {
|
|
373
|
+
host: string;
|
|
374
|
+
port: number;
|
|
375
|
+
email: string;
|
|
376
|
+
password: string;
|
|
377
|
+
secure?: boolean; // default: false
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
interface InboxNewEvent {
|
|
381
|
+
type: 'new';
|
|
382
|
+
uid: number;
|
|
383
|
+
message?: ParsedEmail; // present if autoFetch=true (default)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
interface InboxExpungeEvent {
|
|
387
|
+
type: 'expunge';
|
|
388
|
+
seq: number; // IMAP sequence number
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
interface InboxFlagsEvent {
|
|
392
|
+
type: 'flags';
|
|
393
|
+
uid: number;
|
|
394
|
+
flags: Set<string>;
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Spam Filter
|
|
401
|
+
|
|
402
|
+
### `scoreEmail(email: ParsedEmail): SpamResult`
|
|
403
|
+
|
|
404
|
+
Runs 47 rules across 9 categories. Each rule is try-catch wrapped. Concatenates subject + text + html for pattern testing.
|
|
405
|
+
|
|
406
|
+
### `isInternalEmail(email: ParsedEmail, localDomains?: string[]): boolean`
|
|
407
|
+
|
|
408
|
+
Returns true if from address is `@localhost` (or in localDomains). **Exception:** If from is `@localhost` but replyTo has an external domain, returns false (relay email detection).
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
interface SpamResult {
|
|
412
|
+
score: number; // 0-100+
|
|
413
|
+
isSpam: boolean; // score >= SPAM_THRESHOLD (40)
|
|
414
|
+
isWarning: boolean; // score >= WARNING_THRESHOLD (20) && < SPAM_THRESHOLD
|
|
415
|
+
matches: SpamRuleMatch[];
|
|
416
|
+
topCategory: SpamCategory | null; // category with highest total score
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
interface SpamRuleMatch {
|
|
420
|
+
ruleId: string;
|
|
421
|
+
category: SpamCategory;
|
|
422
|
+
score: number;
|
|
423
|
+
description: string;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
type SpamCategory =
|
|
427
|
+
| 'prompt_injection'
|
|
428
|
+
| 'social_engineering'
|
|
429
|
+
| 'data_exfiltration'
|
|
430
|
+
| 'phishing'
|
|
431
|
+
| 'header_anomaly'
|
|
432
|
+
| 'content_spam'
|
|
433
|
+
| 'link_analysis'
|
|
434
|
+
| 'authentication'
|
|
435
|
+
| 'attachment_risk';
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Complete Rule Inventory (47 rules)
|
|
439
|
+
|
|
440
|
+
| Rule ID | Category | Score | What it detects |
|
|
441
|
+
|---------|----------|-------|-----------------|
|
|
442
|
+
| pi_ignore_instructions | prompt_injection | 25 | "ignore previous/prior instructions" |
|
|
443
|
+
| pi_you_are_now | prompt_injection | 25 | "you are now a..." roleplay injection |
|
|
444
|
+
| pi_system_delimiter | prompt_injection | 20 | [SYSTEM], [INST], <<SYS>>, <\|im_start\|> |
|
|
445
|
+
| pi_new_instructions | prompt_injection | 20 | "new instructions:" / "override instructions:" |
|
|
446
|
+
| pi_act_as | prompt_injection | 15 | "act as a" / "pretend to be" |
|
|
447
|
+
| pi_do_not_mention | prompt_injection | 15 | "do not mention/tell/reveal that" |
|
|
448
|
+
| pi_invisible_unicode | prompt_injection | 20 | Tag chars (U+E0001-E007F), dense zero-width |
|
|
449
|
+
| pi_jailbreak | prompt_injection | 20 | "DAN", "jailbreak", "bypass safety" |
|
|
450
|
+
| pi_base64_injection | prompt_injection | 15 | 100+ char base64 blocks |
|
|
451
|
+
| pi_markdown_injection | prompt_injection | 10 | \`\`\`system, \`\`\`python exec |
|
|
452
|
+
| se_owner_impersonation | social_engineering | 20 | "your owner/admin asked/told/wants" |
|
|
453
|
+
| se_secret_request | social_engineering | 15 | "share your API key/password/secret" |
|
|
454
|
+
| se_impersonate_system | social_engineering | 15 | "this is a system/security automated message" |
|
|
455
|
+
| se_urgency_authority | social_engineering | 10 | urgency + authority/threat language combined |
|
|
456
|
+
| se_money_request | social_engineering | 15 | "send me $X", "wire transfer" |
|
|
457
|
+
| se_gift_card | social_engineering | 20 | "buy me gift cards", iTunes/Google Play |
|
|
458
|
+
| se_ceo_fraud | social_engineering | 15 | CEO/CFO/CTO + payment/wire/urgent |
|
|
459
|
+
| de_forward_all | data_exfiltration | 20 | "forward all/every emails" |
|
|
460
|
+
| de_search_credentials | data_exfiltration | 20 | "search inbox for password" |
|
|
461
|
+
| de_send_to_external | data_exfiltration | 15 | "send the/all to external@email" |
|
|
462
|
+
| de_dump_instructions | data_exfiltration | 15 | "reveal system prompt" / "dump instructions" |
|
|
463
|
+
| de_webhook_exfil | data_exfiltration | 15 | webhook/ngrok/pipedream/requestbin URLs |
|
|
464
|
+
| ph_spoofed_sender | phishing | 10 | brand name in From but mismatched domain |
|
|
465
|
+
| ph_credential_harvest | phishing | 15 | "verify your account" + links present |
|
|
466
|
+
| ph_suspicious_links | phishing | 10 | IP in URL, shorteners, 4+ subdomains |
|
|
467
|
+
| ph_data_uri | phishing | 15 | data:text/html or javascript: in hrefs |
|
|
468
|
+
| ph_homograph | phishing | 15 | punycode (xn--) or mixed Cyrillic+Latin domain |
|
|
469
|
+
| ph_mismatched_display_url | phishing | 10 | link text URL != href URL domain |
|
|
470
|
+
| ph_login_urgency | phishing | 10 | "click here/sign in" + urgency words |
|
|
471
|
+
| ph_unsubscribe_missing | phishing | 3 | 5+ links, no List-Unsubscribe header |
|
|
472
|
+
| auth_spf_fail | authentication | 15 | SPF fail/softfail in Authentication-Results |
|
|
473
|
+
| auth_dkim_fail | authentication | 15 | DKIM fail in Authentication-Results |
|
|
474
|
+
| auth_dmarc_fail | authentication | 20 | DMARC fail in Authentication-Results |
|
|
475
|
+
| auth_no_auth_results | authentication | 3 | missing Authentication-Results header |
|
|
476
|
+
| at_executable | attachment_risk | 25 | .exe/.bat/.cmd/.ps1/.sh/.dll/.scr/.vbs/.js/.msi/.com |
|
|
477
|
+
| at_double_extension | attachment_risk | 20 | .pdf.exe, .doc.bat, etc. |
|
|
478
|
+
| at_archive_carrier | attachment_risk | 15 | .zip/.rar/.7z/.tar.gz/.tgz |
|
|
479
|
+
| at_html_attachment | attachment_risk | 10 | .html/.htm/.svg |
|
|
480
|
+
| ha_missing_message_id | header_anomaly | 5 | no Message-ID header |
|
|
481
|
+
| ha_empty_from | header_anomaly | 10 | empty or missing From |
|
|
482
|
+
| ha_reply_to_mismatch | header_anomaly | 5 | Reply-To domain != From domain |
|
|
483
|
+
| cs_all_caps_subject | content_spam | 5 | subject >80% uppercase (min 10 chars) |
|
|
484
|
+
| cs_lottery_scam | content_spam | 25 | lottery/prize/"Nigerian prince" |
|
|
485
|
+
| cs_crypto_scam | content_spam | 10 | crypto investment/"guaranteed returns" |
|
|
486
|
+
| cs_excessive_punctuation | content_spam | 3 | 4+ !!!! or ???? in subject |
|
|
487
|
+
| cs_pharmacy_spam | content_spam | 15 | viagra/cialis/"online pharmacy" |
|
|
488
|
+
| cs_weight_loss | content_spam | 10 | "diet pill"/"lose 30 lbs" |
|
|
489
|
+
| cs_html_only_no_text | content_spam | 5 | HTML body but no plain text |
|
|
490
|
+
| cs_spam_word_density | content_spam | 10-20 | >5 spam words=10pts, >10 words=20pts |
|
|
491
|
+
| la_excessive_links | link_analysis | 5 | 10+ unique links |
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## Outbound Guard
|
|
496
|
+
|
|
497
|
+
### `scanOutboundEmail(input: OutboundScanInput): OutboundScanResult`
|
|
498
|
+
|
|
499
|
+
Skips all scanning if every recipient ends with `@localhost`. Strips HTML tags and decodes entities before scanning text. Scans attachment content for text-scannable types.
|
|
500
|
+
|
|
501
|
+
### `buildInboundSecurityAdvisory(attachments, spamMatches): SecurityAdvisory`
|
|
502
|
+
|
|
503
|
+
Analyzes attachments for risk (executables, archives, double extensions, HTML files) and extracts link warnings from spam matches.
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
interface OutboundScanInput {
|
|
507
|
+
to: string | string[];
|
|
508
|
+
subject?: string;
|
|
509
|
+
text?: string;
|
|
510
|
+
html?: string;
|
|
511
|
+
attachments?: Array<{
|
|
512
|
+
filename?: string;
|
|
513
|
+
contentType?: string;
|
|
514
|
+
content?: Buffer | string;
|
|
515
|
+
encoding?: string;
|
|
516
|
+
}>;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
interface OutboundScanResult {
|
|
520
|
+
warnings: OutboundWarning[];
|
|
521
|
+
hasHighSeverity: boolean;
|
|
522
|
+
hasMediumSeverity: boolean;
|
|
523
|
+
blocked: boolean; // true if ANY high-severity warning
|
|
524
|
+
summary: string;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
interface OutboundWarning {
|
|
528
|
+
category: OutboundCategory;
|
|
529
|
+
severity: Severity;
|
|
530
|
+
ruleId: string;
|
|
531
|
+
description: string;
|
|
532
|
+
match: string; // up to 80 chars of matched text
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
type OutboundCategory = 'pii' | 'credential' | 'system_internal' | 'owner_privacy' | 'attachment_risk';
|
|
536
|
+
type Severity = 'high' | 'medium';
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Complete Rule Inventory (34+ text rules + attachment rules)
|
|
540
|
+
|
|
541
|
+
#### PII Rules
|
|
542
|
+
|
|
543
|
+
| Rule ID | Severity | Pattern |
|
|
544
|
+
|---------|----------|---------|
|
|
545
|
+
| ob_ssn | HIGH | `\b\d{3}-\d{2}-\d{4}\b` |
|
|
546
|
+
| ob_ssn_obfuscated | HIGH | XXX.XX.XXXX, "ssn #123456789" variants |
|
|
547
|
+
| ob_credit_card | HIGH | `\b(?:\d{4}[-\s]?){3}\d{4}\b` |
|
|
548
|
+
| ob_phone | MEDIUM | US phone (optional +1, parens, dots) |
|
|
549
|
+
| ob_bank_routing | HIGH | routing/account number with 6-17 digits |
|
|
550
|
+
| ob_drivers_license | HIGH | driver's license + alphanumeric ID |
|
|
551
|
+
| ob_dob | MEDIUM | DOB/born on + date formats |
|
|
552
|
+
| ob_passport | HIGH | passport + 6-12 char ID |
|
|
553
|
+
| ob_tax_id | HIGH | EIN/TIN/tax id + XX-XXXXXXX |
|
|
554
|
+
| ob_itin | HIGH | ITIN + 9XX-XX-XXXX |
|
|
555
|
+
| ob_medicare | HIGH | medicare/medicaid + 8-14 char ID |
|
|
556
|
+
| ob_immigration | HIGH | A-number/alien number + 8-9 digits |
|
|
557
|
+
| ob_pin | MEDIUM | PIN/pin code = 4-8 digits |
|
|
558
|
+
| ob_security_qa | MEDIUM | security Q&A / mother's maiden name |
|
|
559
|
+
|
|
560
|
+
#### Financial Rules
|
|
561
|
+
|
|
562
|
+
| Rule ID | Severity | Pattern |
|
|
563
|
+
|---------|----------|---------|
|
|
564
|
+
| ob_iban | HIGH | Country code + 2 digits + alphanumeric blocks |
|
|
565
|
+
| ob_swift | MEDIUM | SWIFT/BIC code (6 alpha + 2 alphanum + optional 3) |
|
|
566
|
+
| ob_crypto_wallet | HIGH | Bitcoin (bc1...), Legacy (1.../3...), Ethereum (0x...) |
|
|
567
|
+
| ob_wire_transfer | HIGH | wire transfer terms + account details |
|
|
568
|
+
|
|
569
|
+
#### Credential Rules
|
|
570
|
+
|
|
571
|
+
| Rule ID | Severity | Pattern |
|
|
572
|
+
|---------|----------|---------|
|
|
573
|
+
| ob_api_key | HIGH | sk_/pk_/rk_/api_key_ + 20+ chars, sk-proj-... |
|
|
574
|
+
| ob_aws_key | HIGH | `AKIA[A-Z0-9]{16}` |
|
|
575
|
+
| ob_password_value | HIGH | p[a@4]ss(w[o0]rd)? := value |
|
|
576
|
+
| ob_private_key | HIGH | `-----BEGIN (RSA\|EC\|DSA\|OPENSSH) PRIVATE KEY-----` |
|
|
577
|
+
| ob_bearer_token | HIGH | `Bearer [a-zA-Z0-9_\-.]{20,}` |
|
|
578
|
+
| ob_connection_string | HIGH | mongodb/postgres/mysql/redis/amqp:// |
|
|
579
|
+
| ob_github_token | HIGH | ghp_/gho_/ghu_/ghs_/ghr_/github_pat_ + 20+ chars |
|
|
580
|
+
| ob_stripe_key | HIGH | sk_live_/pk_live_/rk_live_/sk_test_ + 20+ chars |
|
|
581
|
+
| ob_jwt | HIGH | eyJ...eyJ...eyJ... (3 base64url segments) |
|
|
582
|
+
| ob_webhook_url | HIGH | hooks.slack.com, discord webhooks, webhook.site |
|
|
583
|
+
| ob_env_block | HIGH | 3+ consecutive KEY=VALUE lines |
|
|
584
|
+
| ob_seed_phrase | HIGH | seed phrase/recovery phrase/mnemonic + content |
|
|
585
|
+
| ob_2fa_codes | HIGH | 2FA backup/recovery codes (series of 4-8 char codes) |
|
|
586
|
+
| ob_credential_pair | HIGH | username=X password=Y pairs |
|
|
587
|
+
| ob_oauth_token | HIGH | access_token/refresh_token/oauth_token = value |
|
|
588
|
+
| ob_vpn_creds | HIGH | VPN/OpenVPN/WireGuard + password/key/secret |
|
|
589
|
+
|
|
590
|
+
#### System Internal Rules
|
|
591
|
+
|
|
592
|
+
| Rule ID | Severity | Pattern |
|
|
593
|
+
|---------|----------|---------|
|
|
594
|
+
| ob_private_ip | MEDIUM | 10.x.x.x, 192.168.x.x, 172.16-31.x.x |
|
|
595
|
+
| ob_file_path | MEDIUM | /Users/, /home/, /etc/, C:\Users\ paths |
|
|
596
|
+
| ob_env_variable | MEDIUM | KEY_URL/KEY_SECRET/KEY_TOKEN = value |
|
|
597
|
+
|
|
598
|
+
#### Owner Privacy Rules
|
|
599
|
+
|
|
600
|
+
| Rule ID | Severity | Pattern |
|
|
601
|
+
|---------|----------|---------|
|
|
602
|
+
| ob_owner_info | HIGH | "owner's name/address/phone/email/ssn" |
|
|
603
|
+
| ob_personal_reveal | HIGH | "the person who runs/owns me is/lives/named" |
|
|
604
|
+
|
|
605
|
+
#### Attachment Rules
|
|
606
|
+
|
|
607
|
+
| Extension Type | Severity | Extensions |
|
|
608
|
+
|----------------|----------|------------|
|
|
609
|
+
| Sensitive files | HIGH | .pem, .key, .p12, .pfx, .env, .credentials, .keystore, .jks, .p8 |
|
|
610
|
+
| Data files | MEDIUM | .db, .sqlite, .sqlite3, .sql, .csv, .tsv, .json, .yml, .yaml, .conf, .config, .ini |
|
|
611
|
+
|
|
612
|
+
Text-scannable extensions (content scanned through all rules): .txt, .csv, .json, .xml, .yaml, .yml, .md, .log, .env, .conf, .config, .ini, .sql, .js, .ts, .py, .sh, .html, .htm, .css, .toml
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
## Email Sanitizer
|
|
617
|
+
|
|
618
|
+
### `sanitizeEmail(email: ParsedEmail): SanitizeResult`
|
|
619
|
+
|
|
620
|
+
```typescript
|
|
621
|
+
interface SanitizeResult {
|
|
622
|
+
text: string; // cleaned plain text
|
|
623
|
+
html: string; // cleaned HTML
|
|
624
|
+
detections: SanitizeDetection[];
|
|
625
|
+
wasModified: boolean;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
interface SanitizeDetection {
|
|
629
|
+
type: string; // detection identifier
|
|
630
|
+
description: string;
|
|
631
|
+
count: number;
|
|
632
|
+
}
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
**Invisible Unicode patterns removed:**
|
|
636
|
+
| Pattern | Chars |
|
|
637
|
+
|---------|-------|
|
|
638
|
+
| invisible_tags | U+E0001-E007F |
|
|
639
|
+
| zero_width | U+200B, U+200C, U+200D, U+FEFF (when 3+ consecutive) |
|
|
640
|
+
| bidi_control | U+202A-202E, U+2066-2069 |
|
|
641
|
+
| soft_hyphen | U+00AD |
|
|
642
|
+
| word_joiner | U+2060 |
|
|
643
|
+
|
|
644
|
+
**Hidden HTML patterns removed:**
|
|
645
|
+
| Pattern | What it catches |
|
|
646
|
+
|---------|----------------|
|
|
647
|
+
| hidden_css | display:none, visibility:hidden, font-size:0, opacity:0 |
|
|
648
|
+
| white_on_white | same foreground/background color (#fff/#ffffff/white) |
|
|
649
|
+
| offscreen | position:absolute/fixed + left/top: -9999+ |
|
|
650
|
+
| script_tags | `<script>...</script>` |
|
|
651
|
+
| data_uri | src/href/action with data:text/html or javascript: |
|
|
652
|
+
| suspicious_comment | HTML comments containing: ignore, system, instruction, prompt, inject |
|
|
653
|
+
| hidden_iframe | `<iframe>` with width/height=0 or display:none |
|
|
654
|
+
|
|
655
|
+
---
|
|
656
|
+
|
|
657
|
+
## Gateway Manager
|
|
658
|
+
|
|
659
|
+
### `GatewayManager`
|
|
660
|
+
```typescript
|
|
661
|
+
class GatewayManager {
|
|
662
|
+
constructor(options: GatewayManagerOptions)
|
|
663
|
+
|
|
664
|
+
// Setup
|
|
665
|
+
setupRelay(config: RelayConfig, options?: { createDefaultAgent?: boolean }): Promise<void>
|
|
666
|
+
setupDomain(options: {
|
|
667
|
+
cloudflareToken: string;
|
|
668
|
+
cloudflareAccountId: string;
|
|
669
|
+
domain?: string;
|
|
670
|
+
purchase?: { keywords: string[]; tlds?: string[] };
|
|
671
|
+
gmailRelay?: { email: string; appPassword: string };
|
|
672
|
+
outboundWorkerUrl?: string;
|
|
673
|
+
outboundSecret?: string;
|
|
674
|
+
}): Promise<{ domain; zoneId; tunnelId; dkimPublicKey; dnsRecords; outboundRelay?; nextSteps }>
|
|
675
|
+
|
|
676
|
+
// Email routing
|
|
677
|
+
routeOutbound(agentName: string, mail: SendMailOptions): Promise<SendResult | { pendingId: string }>
|
|
678
|
+
sendViaStalwart(agentName: string, mail: SendMailOptions): Promise<SendResult>
|
|
679
|
+
sendTestEmail(to: string): Promise<SendResult>
|
|
680
|
+
|
|
681
|
+
// Relay search & import
|
|
682
|
+
searchRelay(criteria: SearchCriteria): Promise<RelaySearchResult[]>
|
|
683
|
+
importRelayMessage(relayUid: number, agentName: string): Promise<void>
|
|
684
|
+
|
|
685
|
+
// Lifecycle
|
|
686
|
+
resume(): Promise<void>
|
|
687
|
+
getStatus(): GatewayStatus
|
|
688
|
+
getMode(): GatewayMode
|
|
689
|
+
getConfig(): GatewayConfig | null
|
|
690
|
+
getStalwart(): StalwartAdmin
|
|
691
|
+
|
|
692
|
+
// Deduplication
|
|
693
|
+
isAlreadyDelivered(messageId: string, agentName: string): boolean
|
|
694
|
+
recordDelivery(messageId: string, agentName: string): void
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
**`routeOutbound()`** — If all recipients are `@localhost`, routes locally. Otherwise routes through relay or Stalwart depending on mode.
|
|
699
|
+
|
|
700
|
+
**`sendViaStalwart()`** — Rewrites `@localhost` → `@domain` in sender address, submits to Stalwart SMTP (port 587).
|
|
701
|
+
|
|
702
|
+
**Inbound delivery (internal `deliverInboundLocally()`):**
|
|
703
|
+
- Authenticates as the target agent (Stalwart requires sender=auth user)
|
|
704
|
+
- Runs spam filter via `scoreEmail()`
|
|
705
|
+
- Detects approval reply emails (matches In-Reply-To against `pending_outbound.notification_message_id`)
|
|
706
|
+
- Approval patterns recognized: `approve[d]?`, `yes`, `send`, `go ahead`, `lgtm`, `ok`
|
|
707
|
+
- Rejection patterns recognized: `reject[ed]?`, `no`, `deny`, `don't send`, `cancel`, `block`
|
|
708
|
+
- Adds headers: `X-AgenticMail-Relay`, `X-Original-From`, `X-Original-Message-Id`
|
|
709
|
+
|
|
710
|
+
**Domain mode setup (17 steps) returns:**
|
|
711
|
+
```typescript
|
|
712
|
+
{
|
|
713
|
+
domain: string;
|
|
714
|
+
zoneId: string;
|
|
715
|
+
tunnelId: string;
|
|
716
|
+
dkimPublicKey: string;
|
|
717
|
+
dnsRecords: DnsRecord[];
|
|
718
|
+
outboundRelay?: { configured: boolean; smtpHost: string };
|
|
719
|
+
nextSteps: string[]; // e.g., Gmail "Send mail as" instructions
|
|
720
|
+
}
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
---
|
|
724
|
+
|
|
725
|
+
## Relay Gateway
|
|
726
|
+
|
|
727
|
+
### `RelayGateway`
|
|
728
|
+
```typescript
|
|
729
|
+
class RelayGateway {
|
|
730
|
+
constructor(options?: { onInboundMail?: (email: InboundEmail, agentName: string) => Promise<void>; defaultAgentName?: string })
|
|
731
|
+
|
|
732
|
+
setup(config: RelayConfig): Promise<void>
|
|
733
|
+
sendViaRelay(agentName: string, mail: SendMailOptions): Promise<SendResult>
|
|
734
|
+
startPolling(intervalMs?: number): void // default: 30000
|
|
735
|
+
stopPolling(): void
|
|
736
|
+
searchRelay(criteria: SearchCriteria, maxResults?: number): Promise<RelaySearchResult[]>
|
|
737
|
+
fetchRelayMessage(uid: number): Promise<InboundEmail>
|
|
738
|
+
setLastSeenUid(uid: number): void
|
|
739
|
+
trackSentMessage(messageId: string, agentName: string): void
|
|
740
|
+
isConfigured(): boolean
|
|
741
|
+
isPolling(): boolean
|
|
742
|
+
getConfig(): RelayConfig | null
|
|
743
|
+
shutdown(): Promise<void>
|
|
744
|
+
}
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
**Polling details:**
|
|
748
|
+
- Uses `setTimeout` (not `setInterval`) for natural backoff
|
|
749
|
+
- Backoff: `interval * 2^(failures-1)`, capped at 5 minutes
|
|
750
|
+
- Connection timeout: 30 seconds per poll
|
|
751
|
+
- First poll: scans UID range `uidNext-50` to `*`
|
|
752
|
+
- Subsequent: only `lastSeenUid+1` to `*`
|
|
753
|
+
- Never permanently stops — always reschedules
|
|
754
|
+
- Logs every 5 consecutive failures
|
|
755
|
+
|
|
756
|
+
**Agent routing priority:**
|
|
757
|
+
1. Sub-address in To/CC/Delivered-To/X-Original-To (`user+agent@domain`)
|
|
758
|
+
2. In-Reply-To matched against tracked sent messages
|
|
759
|
+
3. References chain (newest first)
|
|
760
|
+
4. Default agent fallback
|
|
761
|
+
|
|
762
|
+
**Sent message tracking:** Map capped at 10,000 entries.
|
|
763
|
+
|
|
764
|
+
```typescript
|
|
765
|
+
interface InboundEmail {
|
|
766
|
+
messageId: string;
|
|
767
|
+
from: string;
|
|
768
|
+
to: string[];
|
|
769
|
+
subject: string;
|
|
770
|
+
text?: string;
|
|
771
|
+
html?: string;
|
|
772
|
+
date: Date;
|
|
773
|
+
inReplyTo?: string;
|
|
774
|
+
references?: string[];
|
|
775
|
+
attachments: Array<{ filename: string; contentType: string; size: number; content: Buffer }>;
|
|
776
|
+
}
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
---
|
|
780
|
+
|
|
781
|
+
## Cloudflare Client
|
|
782
|
+
|
|
783
|
+
### `CloudflareClient`
|
|
784
|
+
```typescript
|
|
785
|
+
class CloudflareClient {
|
|
786
|
+
constructor(apiToken: string, accountId: string)
|
|
787
|
+
|
|
788
|
+
// Zones
|
|
789
|
+
listZones(): Promise<CloudflareZone[]>
|
|
790
|
+
getZone(domain: string): Promise<CloudflareZone>
|
|
791
|
+
createZone(domain: string): Promise<CloudflareZone>
|
|
792
|
+
|
|
793
|
+
// DNS
|
|
794
|
+
listDnsRecords(zoneId: string): Promise<CloudflareDnsRecord[]>
|
|
795
|
+
createDnsRecord(zoneId: string, record: { type; name; content; ttl?; priority?; proxied? }): Promise<CloudflareDnsRecord>
|
|
796
|
+
deleteDnsRecord(zoneId: string, recordId: string): Promise<void>
|
|
797
|
+
|
|
798
|
+
// Registrar
|
|
799
|
+
searchDomains(query: string): Promise<CloudflareDomainAvailability[]>
|
|
800
|
+
checkAvailability(domain: string): Promise<CloudflareDomainAvailability>
|
|
801
|
+
purchaseDomain(domain: string, autoRenew?: boolean): Promise<void>
|
|
802
|
+
listRegisteredDomains(): Promise<any[]>
|
|
803
|
+
|
|
804
|
+
// Tunnels
|
|
805
|
+
createTunnel(name: string): Promise<CloudflareTunnel>
|
|
806
|
+
getTunnel(tunnelId: string): Promise<CloudflareTunnel>
|
|
807
|
+
getTunnelToken(tunnelId: string): Promise<string>
|
|
808
|
+
createTunnelRoute(tunnelId: string, hostname: string, service: string, options?: { apiService?: string; apiPort?: number }): Promise<void>
|
|
809
|
+
deleteTunnel(tunnelId: string): Promise<void>
|
|
810
|
+
listTunnels(): Promise<CloudflareTunnel[]>
|
|
811
|
+
|
|
812
|
+
// Email Routing
|
|
813
|
+
enableEmailRouting(zoneId: string): Promise<void>
|
|
814
|
+
disableEmailRouting(zoneId: string): Promise<void>
|
|
815
|
+
getEmailRoutingStatus(zoneId: string): Promise<any>
|
|
816
|
+
setCatchAllWorkerRule(zoneId: string, workerName: string): Promise<void>
|
|
817
|
+
|
|
818
|
+
// Workers
|
|
819
|
+
deployEmailWorker(scriptName: string, scriptContent: string, envVars: Record<string, string>): Promise<void>
|
|
820
|
+
deleteWorker(scriptName: string): Promise<void>
|
|
821
|
+
}
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
**API base:** `https://api.cloudflare.com/client/v4`
|
|
825
|
+
|
|
826
|
+
**`createTunnel()`** — Reuses existing tunnel if name matches. Generates random 32-byte secret.
|
|
827
|
+
|
|
828
|
+
**`createTunnelRoute()`** — Creates ingress: `/api/agenticmail/*` → apiService (port 3100), `*` → primary service (port 8080), catch-all → 404.
|
|
829
|
+
|
|
830
|
+
**`deployEmailWorker()`** — Multipart form upload with ES module metadata and plain_text env var bindings. Compatibility date: 2024-01-01.
|
|
831
|
+
|
|
832
|
+
---
|
|
833
|
+
|
|
834
|
+
## Tunnel Manager
|
|
835
|
+
|
|
836
|
+
### `TunnelManager`
|
|
837
|
+
```typescript
|
|
838
|
+
class TunnelManager {
|
|
839
|
+
constructor(cf: CloudflareClient)
|
|
840
|
+
|
|
841
|
+
install(): Promise<string> // returns binary path
|
|
842
|
+
create(name: string): Promise<TunnelConfig>
|
|
843
|
+
start(tunnelToken: string): Promise<void>
|
|
844
|
+
createIngress(tunnelId: string, domain: string, smtpPort?: number, httpPort?: number, apiPort?: number): Promise<void>
|
|
845
|
+
stop(): Promise<void>
|
|
846
|
+
status(): { running: boolean; pid?: number }
|
|
847
|
+
healthCheck(tunnelId: string): Promise<boolean>
|
|
848
|
+
}
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
**`install()`** — Priority: managed binary (`~/.agenticmail/bin/cloudflared`) → system-wide (`which`) → download from GitHub releases. Platform detection: darwin-arm64, darwin-amd64, linux-arm64, linux-amd64. Atomic install: write .tmp, chmod 0755, rename.
|
|
852
|
+
|
|
853
|
+
**`start()`** — Spawns cloudflared with `--no-autoupdate`. Token passed via env var (not CLI arg). Waits up to 30 seconds for "Registered tunnel connection" or "Connection registered" in stdout/stderr.
|
|
854
|
+
|
|
855
|
+
**`stop()`** — SIGTERM with 5 second timeout.
|
|
856
|
+
|
|
857
|
+
---
|
|
858
|
+
|
|
859
|
+
## DNS Configurator
|
|
860
|
+
|
|
861
|
+
### `DNSConfigurator`
|
|
862
|
+
```typescript
|
|
863
|
+
class DNSConfigurator {
|
|
864
|
+
constructor(cf: CloudflareClient)
|
|
865
|
+
|
|
866
|
+
detectPublicIp(): Promise<string>
|
|
867
|
+
configureForEmail(domain: string, zoneId: string, options?: {
|
|
868
|
+
serverIp?: string;
|
|
869
|
+
dkimSelector?: string;
|
|
870
|
+
dkimPublicKey?: string;
|
|
871
|
+
}): Promise<DnsSetupResult>
|
|
872
|
+
configureForTunnel(domain: string, zoneId: string, tunnelId: string): Promise<DnsSetupResult>
|
|
873
|
+
verify(domain: string): Promise<{ mx: boolean; spf: boolean; dmarc: boolean }>
|
|
874
|
+
}
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
**`configureForEmail()`** records created:
|
|
878
|
+
- SPF TXT: `v=spf1 ip4:{serverIp} include:_spf.mx.cloudflare.net mx ~all`
|
|
879
|
+
- DMARC TXT: `v=DMARC1; p=quarantine; rua=mailto:dmarc@{domain}`
|
|
880
|
+
- DKIM TXT: `v=DKIM1; k=rsa; p={publicKey}` (if key provided)
|
|
881
|
+
- Preserves Cloudflare Email Routing `_dc-mx.*` MX records
|
|
882
|
+
- Removes conflicting foreign MX records
|
|
883
|
+
|
|
884
|
+
**`configureForTunnel()`** records created:
|
|
885
|
+
- CNAME: `{domain} → {tunnelId}.cfargotunnel.com` (proxied)
|
|
886
|
+
- CNAME: `mail.{domain} → {tunnelId}.cfargotunnel.com` (proxied)
|
|
887
|
+
- Removes conflicting A/AAAA/CNAME records
|
|
888
|
+
|
|
889
|
+
---
|
|
890
|
+
|
|
891
|
+
## Domain Purchaser
|
|
892
|
+
|
|
893
|
+
### `DomainPurchaser`
|
|
894
|
+
```typescript
|
|
895
|
+
class DomainPurchaser {
|
|
896
|
+
constructor(cf: CloudflareClient)
|
|
897
|
+
|
|
898
|
+
searchAvailable(keywords: string[], tlds?: string[]): Promise<DomainSearchResult[]>
|
|
899
|
+
purchase(domain: string, autoRenew?: boolean): Promise<void> // throws — use CF dashboard
|
|
900
|
+
getStatus(domain: string): Promise<any>
|
|
901
|
+
listRegistered(): Promise<any[]>
|
|
902
|
+
}
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
Default TLDs: `.com`, `.net`, `.io`, `.dev`
|
|
906
|
+
|
|
907
|
+
**Note:** `purchase()` throws an error because Cloudflare API tokens only get read access to registrar. Users must purchase via Cloudflare dashboard.
|
|
908
|
+
|
|
909
|
+
---
|
|
910
|
+
|
|
911
|
+
## Relay Bridge
|
|
912
|
+
|
|
913
|
+
### `RelayBridge`
|
|
914
|
+
```typescript
|
|
915
|
+
class RelayBridge {
|
|
916
|
+
constructor(options: RelayBridgeOptions)
|
|
917
|
+
|
|
918
|
+
start(): Promise<void>
|
|
919
|
+
stop(): Promise<void>
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function startRelayBridge(options: RelayBridgeOptions): Promise<RelayBridge>
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
Local HTTP-to-SMTP bridge on `127.0.0.1:{port}`. Validates `X-Relay-Secret` header. Accepts POST `/send` with JSON payload, submits to Stalwart SMTP for DKIM signing and delivery.
|
|
926
|
+
|
|
927
|
+
---
|
|
928
|
+
|
|
929
|
+
## Stalwart Admin
|
|
930
|
+
|
|
931
|
+
### `StalwartAdmin`
|
|
932
|
+
```typescript
|
|
933
|
+
class StalwartAdmin {
|
|
934
|
+
constructor(options: StalwartAdminOptions)
|
|
935
|
+
|
|
936
|
+
// Principals
|
|
937
|
+
createPrincipal(principal: StalwartPrincipal): Promise<void>
|
|
938
|
+
getPrincipal(name: string): Promise<StalwartPrincipal>
|
|
939
|
+
updatePrincipal(name: string, changes: Partial<StalwartPrincipal>): Promise<void>
|
|
940
|
+
addEmailAlias(name: string, email: string): Promise<void>
|
|
941
|
+
deletePrincipal(name: string): Promise<void>
|
|
942
|
+
listPrincipals(type?: string): Promise<string[]>
|
|
943
|
+
|
|
944
|
+
// Domains
|
|
945
|
+
ensureDomain(domain: string): Promise<void> // idempotent
|
|
946
|
+
|
|
947
|
+
// Health
|
|
948
|
+
healthCheck(): Promise<boolean> // 5s timeout
|
|
949
|
+
|
|
950
|
+
// Settings
|
|
951
|
+
getSetting(key: string): Promise<string | undefined>
|
|
952
|
+
getSettings(prefix: string): Promise<Record<string, string>>
|
|
953
|
+
updateSetting(key: string, value: string): Promise<void> // via stalwart-cli in Docker
|
|
954
|
+
|
|
955
|
+
// Config
|
|
956
|
+
setHostname(domain: string): Promise<void> // modifies stalwart.toml on host
|
|
957
|
+
|
|
958
|
+
// DKIM
|
|
959
|
+
createDkimSignature(domain: string, selector?: string): Promise<{ signatureId: string; publicKey: string }>
|
|
960
|
+
hasDkimSignature(domain: string): Promise<boolean>
|
|
961
|
+
|
|
962
|
+
// Outbound Relay
|
|
963
|
+
configureOutboundRelay(config: {
|
|
964
|
+
smtpHost: string;
|
|
965
|
+
smtpPort: number;
|
|
966
|
+
username: string;
|
|
967
|
+
password: string;
|
|
968
|
+
routeName?: string; // default: 'gmail'
|
|
969
|
+
}): Promise<void> // modifies stalwart.toml, restarts container
|
|
970
|
+
}
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
**Request timeout:** 15 seconds. Auth: HTTP Basic.
|
|
974
|
+
|
|
975
|
+
**`updateSetting()`** — Uses stalwart-cli inside Docker container. Deletes then adds config. Verifies by reading back.
|
|
976
|
+
|
|
977
|
+
**`createDkimSignature()`** — Idempotent. Signature ID: `agenticmail-{domain with dots→dashes}`. Default selector: `agenticmail`. Creates via `stalwart-cli dkim create rsa`. Sets signing rules in `auth.dkim.sign.*`. Returns base64 public key for DNS TXT record.
|
|
978
|
+
|
|
979
|
+
**`configureOutboundRelay()`** — Appends relay route and strategy to stalwart.toml. Routes: local domains → 'local', everything else → relay. Restarts container (15s wait).
|
|
980
|
+
|
|
981
|
+
```typescript
|
|
982
|
+
interface StalwartPrincipal {
|
|
983
|
+
type: 'individual' | 'group' | 'domain' | 'list' | 'apiKey';
|
|
984
|
+
name: string;
|
|
985
|
+
secrets?: string[];
|
|
986
|
+
emails?: string[];
|
|
987
|
+
description?: string;
|
|
988
|
+
quota?: number;
|
|
989
|
+
memberOf?: string[];
|
|
990
|
+
members?: string[];
|
|
991
|
+
roles?: string[];
|
|
992
|
+
}
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
---
|
|
996
|
+
|
|
997
|
+
## Domain Manager
|
|
998
|
+
|
|
999
|
+
### `DomainManager`
|
|
1000
|
+
```typescript
|
|
1001
|
+
class DomainManager {
|
|
1002
|
+
constructor(db: Database.Database, stalwart: StalwartAdmin)
|
|
1003
|
+
|
|
1004
|
+
setup(domain: string): Promise<DomainSetupResult>
|
|
1005
|
+
get(domain: string): Promise<DomainInfo | null>
|
|
1006
|
+
list(): Promise<DomainInfo[]>
|
|
1007
|
+
getDnsRecords(domain: string): Promise<DnsRecord[]>
|
|
1008
|
+
verify(domain: string): Promise<boolean> // checks MX records
|
|
1009
|
+
delete(domain: string): Promise<boolean>
|
|
1010
|
+
}
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
---
|
|
1014
|
+
|
|
1015
|
+
## Storage
|
|
1016
|
+
|
|
1017
|
+
### `getDatabase(config: AgenticMailConfig | string): Database.Database`
|
|
1018
|
+
|
|
1019
|
+
Singleton. Opens SQLite with WAL mode and foreign keys. Runs all pending migrations automatically.
|
|
1020
|
+
|
|
1021
|
+
### `closeDatabase(): void`
|
|
1022
|
+
|
|
1023
|
+
Closes and resets singleton.
|
|
1024
|
+
|
|
1025
|
+
### `createTestDatabase(): Database.Database`
|
|
1026
|
+
|
|
1027
|
+
In-memory database with all migrations applied. For testing.
|
|
1028
|
+
|
|
1029
|
+
---
|
|
1030
|
+
|
|
1031
|
+
## Search Index
|
|
1032
|
+
|
|
1033
|
+
### `EmailSearchIndex`
|
|
1034
|
+
```typescript
|
|
1035
|
+
class EmailSearchIndex {
|
|
1036
|
+
constructor(db: Database.Database)
|
|
1037
|
+
|
|
1038
|
+
index(email: SearchableEmail): void
|
|
1039
|
+
search(agentId: string, query: string, limit?: number): Array<{ uid: number; rank: number }>
|
|
1040
|
+
deleteByAgent(agentId: string): void
|
|
1041
|
+
}
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
**`search()`** — Wraps query in quotes (phrase search). Escapes internal quotes. Limit: 1-1000 (default 20). Returns empty on FTS5 syntax error. Ordered by FTS5 rank.
|
|
1045
|
+
|
|
1046
|
+
```typescript
|
|
1047
|
+
interface SearchableEmail {
|
|
1048
|
+
agentId: string;
|
|
1049
|
+
messageId: string;
|
|
1050
|
+
subject: string;
|
|
1051
|
+
fromAddress: string;
|
|
1052
|
+
toAddress: string;
|
|
1053
|
+
bodyText: string;
|
|
1054
|
+
receivedAt: Date;
|
|
1055
|
+
}
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
---
|
|
1059
|
+
|
|
1060
|
+
## Setup
|
|
1061
|
+
|
|
1062
|
+
### `SetupManager`
|
|
1063
|
+
```typescript
|
|
1064
|
+
class SetupManager {
|
|
1065
|
+
constructor(onProgress?: InstallProgress)
|
|
1066
|
+
|
|
1067
|
+
checkDependencies(): Promise<{ docker; stalwart; cloudflared }>
|
|
1068
|
+
installAll(composePath?: string): Promise<void>
|
|
1069
|
+
ensureDocker(): Promise<void>
|
|
1070
|
+
ensureStalwart(composePath?: string): Promise<void>
|
|
1071
|
+
ensureCloudflared(): Promise<void>
|
|
1072
|
+
getComposePath(): string
|
|
1073
|
+
initConfig(): Promise<SetupConfig>
|
|
1074
|
+
isInitialized(): boolean
|
|
1075
|
+
}
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
### `DependencyChecker`
|
|
1079
|
+
```typescript
|
|
1080
|
+
class DependencyChecker {
|
|
1081
|
+
checkAll(): Promise<DependencyStatus[]>
|
|
1082
|
+
checkDocker(): Promise<DependencyStatus>
|
|
1083
|
+
checkStalwart(): Promise<DependencyStatus>
|
|
1084
|
+
checkCloudflared(): Promise<DependencyStatus>
|
|
1085
|
+
}
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
### `DependencyInstaller`
|
|
1089
|
+
```typescript
|
|
1090
|
+
class DependencyInstaller {
|
|
1091
|
+
constructor(onProgress?: InstallProgress)
|
|
1092
|
+
|
|
1093
|
+
installDocker(): Promise<void> // Homebrew (macOS) or official script (Linux)
|
|
1094
|
+
startStalwart(composePath: string): Promise<void>
|
|
1095
|
+
installCloudflared(): Promise<void> // GitHub releases download
|
|
1096
|
+
installAll(composePath: string): Promise<void>
|
|
1097
|
+
}
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
---
|
|
1101
|
+
|
|
1102
|
+
## Database Schema
|
|
1103
|
+
|
|
1104
|
+
### Tables
|
|
1105
|
+
|
|
1106
|
+
```sql
|
|
1107
|
+
-- agents (migration 001 + 003 + 009)
|
|
1108
|
+
CREATE TABLE agents (
|
|
1109
|
+
id TEXT PRIMARY KEY,
|
|
1110
|
+
name TEXT UNIQUE NOT NULL,
|
|
1111
|
+
email TEXT UNIQUE NOT NULL,
|
|
1112
|
+
api_key TEXT UNIQUE NOT NULL,
|
|
1113
|
+
stalwart_principal TEXT NOT NULL,
|
|
1114
|
+
role TEXT DEFAULT 'secretary',
|
|
1115
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1116
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
1117
|
+
last_activity_at TEXT,
|
|
1118
|
+
persistent INTEGER DEFAULT 0,
|
|
1119
|
+
metadata TEXT DEFAULT '{}'
|
|
1120
|
+
);
|
|
1121
|
+
|
|
1122
|
+
-- pending_outbound (migration 012 + 013)
|
|
1123
|
+
CREATE TABLE pending_outbound (
|
|
1124
|
+
id TEXT PRIMARY KEY,
|
|
1125
|
+
agent_id TEXT NOT NULL,
|
|
1126
|
+
mail_options TEXT NOT NULL, -- JSON: to, subject, text, html, cc, bcc, etc.
|
|
1127
|
+
warnings TEXT NOT NULL, -- JSON array of OutboundWarning
|
|
1128
|
+
summary TEXT,
|
|
1129
|
+
status TEXT DEFAULT 'pending', -- pending | approved | rejected
|
|
1130
|
+
notification_message_id TEXT, -- Message-ID of owner notification email
|
|
1131
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1132
|
+
resolved_at TEXT,
|
|
1133
|
+
resolved_by TEXT, -- 'master' | 'owner-reply'
|
|
1134
|
+
error TEXT
|
|
1135
|
+
);
|
|
1136
|
+
CREATE INDEX idx_pending_agent_status ON pending_outbound(agent_id, status);
|
|
1137
|
+
CREATE INDEX idx_pending_notification ON pending_outbound(notification_message_id);
|
|
1138
|
+
|
|
1139
|
+
-- agent_tasks (migration 010)
|
|
1140
|
+
CREATE TABLE agent_tasks (
|
|
1141
|
+
id TEXT PRIMARY KEY,
|
|
1142
|
+
assigner_id TEXT,
|
|
1143
|
+
assignee_id TEXT,
|
|
1144
|
+
task_type TEXT DEFAULT 'generic',
|
|
1145
|
+
payload TEXT, -- JSON
|
|
1146
|
+
status TEXT DEFAULT 'pending', -- pending | claimed | completed | failed
|
|
1147
|
+
result TEXT,
|
|
1148
|
+
error TEXT,
|
|
1149
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1150
|
+
claimed_at TEXT,
|
|
1151
|
+
completed_at TEXT,
|
|
1152
|
+
expires_at TEXT
|
|
1153
|
+
);
|
|
1154
|
+
|
|
1155
|
+
-- gateway_config (migration 002)
|
|
1156
|
+
CREATE TABLE gateway_config (
|
|
1157
|
+
id TEXT PRIMARY KEY DEFAULT 'default',
|
|
1158
|
+
mode TEXT DEFAULT 'none', -- relay | domain | none
|
|
1159
|
+
config TEXT DEFAULT '{}', -- JSON: RelayConfig or DomainModeConfig
|
|
1160
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
1161
|
+
);
|
|
1162
|
+
|
|
1163
|
+
-- delivered_messages (migration 004)
|
|
1164
|
+
CREATE TABLE delivered_messages (
|
|
1165
|
+
message_id TEXT NOT NULL,
|
|
1166
|
+
agent_name TEXT NOT NULL,
|
|
1167
|
+
delivered_at TEXT DEFAULT (datetime('now')),
|
|
1168
|
+
PRIMARY KEY (message_id, agent_name)
|
|
1169
|
+
);
|
|
1170
|
+
|
|
1171
|
+
-- email_search (FTS5, migration 001)
|
|
1172
|
+
CREATE VIRTUAL TABLE email_search USING fts5(
|
|
1173
|
+
agent_id, message_id, subject, from_address, to_address, body_text, received_at
|
|
1174
|
+
);
|
|
1175
|
+
|
|
1176
|
+
-- Plus: domains, config, purchased_domains, contacts, drafts, signatures,
|
|
1177
|
+
-- templates, scheduled_emails, tags, message_tags, email_rules, agent_deletions, spam_log
|
|
1178
|
+
```
|
|
1179
|
+
|
|
1180
|
+
---
|
|
1181
|
+
|
|
1182
|
+
## Constants
|
|
1183
|
+
|
|
1184
|
+
| Constant | Value | Description |
|
|
1185
|
+
|----------|-------|-------------|
|
|
1186
|
+
| `SPAM_THRESHOLD` | 40 | Score >= 40 → classified as spam |
|
|
1187
|
+
| `WARNING_THRESHOLD` | 20 | Score 20-39 → warning flag |
|
|
1188
|
+
| `AGENT_ROLES` | `['secretary', 'assistant', 'researcher', 'writer', 'custom']` | Valid agent roles |
|
|
1189
|
+
| `DEFAULT_AGENT_ROLE` | `'secretary'` | Default role for new agents |
|
|
1190
|
+
| `DEFAULT_AGENT_NAME` | `'secretary'` | Default agent name |
|
|
1191
|
+
| `RELAY_PRESETS` | `{ gmail: {...}, outlook: {...} }` | SMTP/IMAP presets for Gmail and Outlook |
|
|
1192
|
+
|
|
1193
|
+
### Timeouts & Limits
|
|
1194
|
+
|
|
1195
|
+
| Setting | Value | Context |
|
|
1196
|
+
|---------|-------|---------|
|
|
1197
|
+
| SMTP connection timeout | 10,000ms | MailSender |
|
|
1198
|
+
| SMTP greeting timeout | 10,000ms | MailSender |
|
|
1199
|
+
| SMTP socket timeout | 15,000ms | MailSender |
|
|
1200
|
+
| Stalwart API request timeout | 15,000ms | StalwartAdmin |
|
|
1201
|
+
| Stalwart health check timeout | 5,000ms | StalwartAdmin |
|
|
1202
|
+
| Relay poll interval (initial) | 30,000ms | RelayGateway |
|
|
1203
|
+
| Relay poll backoff cap | 300,000ms (5 min) | RelayGateway |
|
|
1204
|
+
| Relay connection timeout | 30,000ms | RelayGateway |
|
|
1205
|
+
| Tunnel startup timeout | 30,000ms | TunnelManager |
|
|
1206
|
+
| Tunnel stop timeout | 5,000ms | TunnelManager |
|
|
1207
|
+
| Stalwart container wait | 30,000ms | DependencyInstaller |
|
|
1208
|
+
| Docker daemon wait | 60,000ms | DependencyInstaller |
|
|
1209
|
+
| Max emails per folder (archive) | 10,000 | AgentDeletionService |
|
|
1210
|
+
| Sent message tracking cap | 10,000 entries | RelayGateway |
|
|
1211
|
+
| IMAP list max limit | 1,000 | MailReceiver |
|
|
1212
|
+
| FTS5 search max limit | 1,000 | EmailSearchIndex |
|
|
1213
|
+
| Outbound match preview | 80 chars | scanOutboundEmail |
|
|
1214
|
+
|
|
1215
|
+
---
|
|
1216
|
+
|
|
1217
|
+
## License
|
|
1218
|
+
|
|
1219
|
+
[MIT](./LICENSE) - Ope Olatunji ([@ope-olatunji](https://github.com/ope-olatunji))
|