@agenticmail/api 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 +370 -0
- package/REFERENCE.md +1314 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3248 -0
- package/package.json +68 -0
package/REFERENCE.md
ADDED
|
@@ -0,0 +1,1314 @@
|
|
|
1
|
+
# @agenticmail/api — Technical Reference
|
|
2
|
+
|
|
3
|
+
Complete technical reference for the AgenticMail REST API server. All routes are prefixed with `/api/agenticmail` unless otherwise noted.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Exports
|
|
8
|
+
|
|
9
|
+
The package exports a single factory function:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { createApp } from '@agenticmail/api';
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### `createApp(options): Promise<Express>`
|
|
16
|
+
|
|
17
|
+
Creates and configures the Express application with all routes, middleware, and background services.
|
|
18
|
+
|
|
19
|
+
**Options:**
|
|
20
|
+
```typescript
|
|
21
|
+
{
|
|
22
|
+
masterKey: string;
|
|
23
|
+
smtp: { host: string; port: number };
|
|
24
|
+
imap: { host: string; port: number };
|
|
25
|
+
stalwart: { url: string; user: string; pass: string };
|
|
26
|
+
dataDir: string;
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Returns:** Configured Express app ready to `.listen()`.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Authentication
|
|
35
|
+
|
|
36
|
+
### Bearer Token Authentication
|
|
37
|
+
|
|
38
|
+
All endpoints (except `/health` and `/mail/inbound`) require:
|
|
39
|
+
```
|
|
40
|
+
Authorization: Bearer <token>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Token types:**
|
|
44
|
+
| Prefix | Type | Scope |
|
|
45
|
+
|--------|------|-------|
|
|
46
|
+
| `mk_` | Master key | Full admin access |
|
|
47
|
+
| `ak_` | Agent key | Scoped to owning agent |
|
|
48
|
+
|
|
49
|
+
### Timing-Safe Comparison
|
|
50
|
+
|
|
51
|
+
Key comparison uses SHA-256 hashing + `timingSafeEqual`:
|
|
52
|
+
```typescript
|
|
53
|
+
function safeEqual(a: string, b: string): boolean {
|
|
54
|
+
const ha = createHash('sha256').update(a).digest();
|
|
55
|
+
const hb = createHash('sha256').update(b).digest();
|
|
56
|
+
return timingSafeEqual(ha, hb);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
This prevents timing side-channel attacks against key validation.
|
|
60
|
+
|
|
61
|
+
### Activity Throttling
|
|
62
|
+
|
|
63
|
+
Agent activity (`last_activity_at`) is updated at most once per 60 seconds per agent via an in-memory timestamp cache. SQL: `UPDATE agents SET last_activity_at = datetime('now') WHERE id = ?`
|
|
64
|
+
|
|
65
|
+
### Auth Guard Functions
|
|
66
|
+
|
|
67
|
+
| Function | Behavior |
|
|
68
|
+
|----------|----------|
|
|
69
|
+
| `requireMaster()` | Returns 403 if `!req.isMaster` |
|
|
70
|
+
| `requireAuth()` | Returns 403 if `!req.agent && !req.isMaster` |
|
|
71
|
+
| `requireAgent()` | Returns 403 if `!req.agent` (master alone insufficient) |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Middleware Stack
|
|
76
|
+
|
|
77
|
+
Applied in this order:
|
|
78
|
+
|
|
79
|
+
1. **CORS** — `cors()` with default options (all origins)
|
|
80
|
+
2. **JSON Body Parser** — `express.json({ limit: '10mb' })`
|
|
81
|
+
3. **Global Rate Limiter** — 100 requests per IP per 60-second window
|
|
82
|
+
4. **Health routes** — mounted before auth (no auth required)
|
|
83
|
+
5. **Inbound webhook** — mounted before auth (uses `X-Inbound-Secret`)
|
|
84
|
+
6. **Auth middleware** — Bearer token validation
|
|
85
|
+
7. **Protected routes** — all remaining routes
|
|
86
|
+
8. **404 handler** — `{ error: 'Not found' }`
|
|
87
|
+
9. **Error handler** — categorizes errors by message pattern
|
|
88
|
+
|
|
89
|
+
### Error Handler
|
|
90
|
+
|
|
91
|
+
Maps errors to HTTP status codes:
|
|
92
|
+
|
|
93
|
+
| Status | Trigger |
|
|
94
|
+
|--------|---------|
|
|
95
|
+
| 400 | `SyntaxError` with `.status === 400` (malformed JSON) |
|
|
96
|
+
| 400 | Message contains "invalid", "required", or "must " |
|
|
97
|
+
| 404 | Message contains "not found" (but not "not found a") |
|
|
98
|
+
| 409 | Message contains "already exists" or "unique constraint" |
|
|
99
|
+
| Custom | `err.statusCode` property (if set) |
|
|
100
|
+
| 500 | Default fallback (response: `{ error: 'Internal server error' }`) |
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Routes: Health
|
|
105
|
+
|
|
106
|
+
### GET /health
|
|
107
|
+
|
|
108
|
+
**Auth:** None
|
|
109
|
+
|
|
110
|
+
**Response (200):**
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"status": "ok",
|
|
114
|
+
"services": { "api": "ok", "stalwart": "ok" },
|
|
115
|
+
"timestamp": "ISO-8601"
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Response (503):** Stalwart unreachable → `{ "status": "degraded", "services": { "api": "ok", "stalwart": "unreachable" } }`
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Routes: Mail Operations
|
|
124
|
+
|
|
125
|
+
### Connection Caching
|
|
126
|
+
|
|
127
|
+
| Parameter | Value |
|
|
128
|
+
|-----------|-------|
|
|
129
|
+
| `CACHE_TTL_MS` | 600,000 (10 minutes) |
|
|
130
|
+
| `MAX_CACHE_SIZE` | 100 entries |
|
|
131
|
+
| Eviction interval | 60 seconds |
|
|
132
|
+
| Sender cache key | `${stalwartPrincipal}:${fromEmail}` |
|
|
133
|
+
| Receiver cache key | `${stalwartPrincipal}` |
|
|
134
|
+
| Concurrent dedup | `receiverPending: Map<string, Promise<MailReceiver>>` |
|
|
135
|
+
| Draining flag | Prevents new connections during shutdown |
|
|
136
|
+
|
|
137
|
+
### POST /mail/send
|
|
138
|
+
|
|
139
|
+
**Auth:** Agent (`requireAgent`)
|
|
140
|
+
|
|
141
|
+
**Request Body:**
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"to": "string | string[]", // Required
|
|
145
|
+
"subject": "string", // Required
|
|
146
|
+
"text": "string", // Optional
|
|
147
|
+
"html": "string", // Optional
|
|
148
|
+
"cc": "string | string[]", // Optional
|
|
149
|
+
"bcc": "string | string[]", // Optional
|
|
150
|
+
"replyTo": "string", // Optional
|
|
151
|
+
"inReplyTo": "string", // Optional (Message-ID for threading)
|
|
152
|
+
"references": "string[]", // Optional (thread chain)
|
|
153
|
+
"attachments": [{ // Optional
|
|
154
|
+
"filename": "string",
|
|
155
|
+
"contentType": "string",
|
|
156
|
+
"content": "Buffer | string",
|
|
157
|
+
"encoding": "string"
|
|
158
|
+
}],
|
|
159
|
+
"allowSensitive": "boolean" // Optional (master bypass only)
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Outbound Guard Flow:**
|
|
164
|
+
1. Master key + `allowSensitive: true` → bypasses all scanning
|
|
165
|
+
2. Agent key → `scanOutboundEmail()` always runs regardless of `allowSensitive`
|
|
166
|
+
3. If blocked → stored in `pending_outbound`, notification sent to owner
|
|
167
|
+
4. If allowed (with warnings) → sent with `outboundWarnings` in response
|
|
168
|
+
5. If clean → sent normally
|
|
169
|
+
|
|
170
|
+
**Blocked Email Storage:**
|
|
171
|
+
```sql
|
|
172
|
+
INSERT INTO pending_outbound (id, agent_id, mail_options, warnings, summary, status, created_at)
|
|
173
|
+
VALUES (?, ?, ?, ?, ?, 'pending', datetime('now'))
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Notification Email:**
|
|
177
|
+
- To: `gatewayConfig.relay.email` (owner's relay address)
|
|
178
|
+
- Subject: `[Approval Required] Blocked email from "{agentName}" — "{subject}"`
|
|
179
|
+
- Body: Full email preview, all security warnings, pending ID, reply instructions
|
|
180
|
+
- Reply detection keywords: approve/yes/lgtm/go ahead/send/ok (approve), reject/no/deny/cancel/block (reject)
|
|
181
|
+
|
|
182
|
+
**Display Name Logic:**
|
|
183
|
+
- If `agent.metadata.ownerName`: `"${agent.name} from ${ownerName}"`
|
|
184
|
+
- Else: `agent.name`
|
|
185
|
+
|
|
186
|
+
**Routing Priority:**
|
|
187
|
+
1. Try `gatewayManager.routeOutbound(agentName, mailOpts)`
|
|
188
|
+
2. Fallback to local SMTP via `getSender()`
|
|
189
|
+
3. Best-effort save to Sent folder
|
|
190
|
+
|
|
191
|
+
**Response (200):**
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"sent": true,
|
|
195
|
+
"messageId": "string",
|
|
196
|
+
"timestamp": "ISO-8601",
|
|
197
|
+
"outboundWarnings": [...], // Optional
|
|
198
|
+
"outboundSummary": "string" // Optional
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Response (blocked):**
|
|
203
|
+
```json
|
|
204
|
+
{
|
|
205
|
+
"sent": false,
|
|
206
|
+
"blocked": true,
|
|
207
|
+
"pendingId": "uuid",
|
|
208
|
+
"warnings": [...],
|
|
209
|
+
"summary": "string"
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
### GET /mail/inbox
|
|
216
|
+
|
|
217
|
+
**Auth:** Agent
|
|
218
|
+
|
|
219
|
+
**Query Params:**
|
|
220
|
+
| Param | Default | Range |
|
|
221
|
+
|-------|---------|-------|
|
|
222
|
+
| `limit` | 20 | 1–200 |
|
|
223
|
+
| `offset` | 0 | 0+ |
|
|
224
|
+
|
|
225
|
+
**Response:**
|
|
226
|
+
```json
|
|
227
|
+
{
|
|
228
|
+
"messages": [{
|
|
229
|
+
"uid": 1,
|
|
230
|
+
"subject": "string",
|
|
231
|
+
"from": [{ "name": "string", "address": "string" }],
|
|
232
|
+
"to": [{ "name": "string", "address": "string" }],
|
|
233
|
+
"date": "ISO-8601",
|
|
234
|
+
"flags": ["\\Seen", "\\Flagged"],
|
|
235
|
+
"size": 1234
|
|
236
|
+
}],
|
|
237
|
+
"count": 20,
|
|
238
|
+
"total": 150
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
### GET /mail/digest
|
|
245
|
+
|
|
246
|
+
**Auth:** Agent
|
|
247
|
+
|
|
248
|
+
**Query Params:**
|
|
249
|
+
| Param | Default | Range |
|
|
250
|
+
|-------|---------|-------|
|
|
251
|
+
| `limit` | 20 | 1–50 |
|
|
252
|
+
| `offset` | 0 | 0+ |
|
|
253
|
+
| `previewLength` | 200 | 50–500 |
|
|
254
|
+
| `folder` | "INBOX" | any valid folder |
|
|
255
|
+
|
|
256
|
+
**Response:** Same as inbox but each message includes `"preview": "First N chars..."`.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
### GET /mail/messages/:uid
|
|
261
|
+
|
|
262
|
+
**Auth:** Agent
|
|
263
|
+
|
|
264
|
+
**Query Params:** `folder` (default: "INBOX")
|
|
265
|
+
|
|
266
|
+
**Response:**
|
|
267
|
+
```json
|
|
268
|
+
{
|
|
269
|
+
"uid": 1,
|
|
270
|
+
"subject": "string",
|
|
271
|
+
"from": [...],
|
|
272
|
+
"to": [...],
|
|
273
|
+
"cc": [...],
|
|
274
|
+
"date": "ISO-8601",
|
|
275
|
+
"messageId": "string",
|
|
276
|
+
"inReplyTo": "string",
|
|
277
|
+
"references": "string",
|
|
278
|
+
"text": "string",
|
|
279
|
+
"html": "string",
|
|
280
|
+
"attachments": [{
|
|
281
|
+
"filename": "string",
|
|
282
|
+
"contentType": "string",
|
|
283
|
+
"size": 1234
|
|
284
|
+
}],
|
|
285
|
+
"security": {
|
|
286
|
+
"internal": false,
|
|
287
|
+
"spamScore": 0,
|
|
288
|
+
"isSpam": false,
|
|
289
|
+
"isWarning": false,
|
|
290
|
+
"topCategory": "string | null",
|
|
291
|
+
"matches": ["ruleId", ...],
|
|
292
|
+
"sanitized": false,
|
|
293
|
+
"sanitizeDetections": [...]
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Spam scoring logic:**
|
|
299
|
+
- Internal emails (agent-to-agent on same system) → skip scoring, return `spamScore: 0`
|
|
300
|
+
- External emails → `scoreEmail(parsed)` + `sanitizeEmailContent()`
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
### GET /mail/messages/:uid/attachments/:index
|
|
305
|
+
|
|
306
|
+
**Auth:** Agent
|
|
307
|
+
|
|
308
|
+
**Path Params:** `uid` (integer), `index` (0-based)
|
|
309
|
+
|
|
310
|
+
**Query Params:** `folder` (default: "INBOX")
|
|
311
|
+
|
|
312
|
+
**Response:** Binary data with appropriate `Content-Type`, `Content-Disposition`, `Content-Length` headers.
|
|
313
|
+
|
|
314
|
+
**Status:** 404 if attachment not found.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
### GET /mail/messages/:uid/spam-score
|
|
319
|
+
|
|
320
|
+
**Auth:** Agent
|
|
321
|
+
|
|
322
|
+
**Response:**
|
|
323
|
+
```json
|
|
324
|
+
{
|
|
325
|
+
"score": 0,
|
|
326
|
+
"isSpam": false,
|
|
327
|
+
"isWarning": false,
|
|
328
|
+
"matches": [],
|
|
329
|
+
"topCategory": null,
|
|
330
|
+
"internal": true
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
### POST /mail/search
|
|
337
|
+
|
|
338
|
+
**Auth:** Agent
|
|
339
|
+
|
|
340
|
+
**Request Body:**
|
|
341
|
+
```json
|
|
342
|
+
{
|
|
343
|
+
"from": "string", // Optional
|
|
344
|
+
"to": "string", // Optional
|
|
345
|
+
"subject": "string", // Optional
|
|
346
|
+
"text": "string", // Optional (body text)
|
|
347
|
+
"since": "ISO-8601", // Optional
|
|
348
|
+
"before": "ISO-8601", // Optional
|
|
349
|
+
"seen": true, // Optional (true=read, false=unread)
|
|
350
|
+
"searchRelay": false // Optional (also search relay account)
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Response:**
|
|
355
|
+
```json
|
|
356
|
+
{
|
|
357
|
+
"uids": [1, 2, 3],
|
|
358
|
+
"relayResults": [{ // Only if searchRelay=true
|
|
359
|
+
"uid": 1,
|
|
360
|
+
"source": "relay",
|
|
361
|
+
"account": "email@gmail.com",
|
|
362
|
+
"messageId": "string",
|
|
363
|
+
"subject": "string",
|
|
364
|
+
"from": [...],
|
|
365
|
+
"to": [...],
|
|
366
|
+
"date": "ISO-8601",
|
|
367
|
+
"flags": [...]
|
|
368
|
+
}]
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
### POST /mail/import-relay
|
|
375
|
+
|
|
376
|
+
**Auth:** Agent
|
|
377
|
+
|
|
378
|
+
**Request Body:** `{ "uid": 123 }`
|
|
379
|
+
|
|
380
|
+
**Response:** `{ "ok": true, "message": "Email imported to local inbox." }`
|
|
381
|
+
|
|
382
|
+
**Status:** 400 if no gateway or import failed.
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
### DELETE /mail/messages/:uid
|
|
387
|
+
|
|
388
|
+
**Auth:** Agent
|
|
389
|
+
|
|
390
|
+
**Response:** 204 (no content)
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
### POST /mail/messages/:uid/seen
|
|
395
|
+
|
|
396
|
+
**Auth:** Agent
|
|
397
|
+
|
|
398
|
+
**Response:** `{ "ok": true }`
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
### POST /mail/messages/:uid/unseen
|
|
403
|
+
|
|
404
|
+
**Auth:** Agent
|
|
405
|
+
|
|
406
|
+
**Response:** `{ "ok": true }`
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
### POST /mail/messages/:uid/move
|
|
411
|
+
|
|
412
|
+
**Auth:** Agent
|
|
413
|
+
|
|
414
|
+
**Request Body:** `{ "from": "INBOX", "to": "Archive" }`
|
|
415
|
+
|
|
416
|
+
**Response:** `{ "ok": true }`
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
### GET /mail/folders
|
|
421
|
+
|
|
422
|
+
**Auth:** Agent
|
|
423
|
+
|
|
424
|
+
**Response:** `{ "folders": ["INBOX", "Sent Items", "Drafts", ...] }`
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
### POST /mail/folders
|
|
429
|
+
|
|
430
|
+
**Auth:** Agent
|
|
431
|
+
|
|
432
|
+
**Request Body:** `{ "name": "Projects" }`
|
|
433
|
+
|
|
434
|
+
**Validation:** Name <= 200 chars, no `\`, `*`, `%` characters.
|
|
435
|
+
|
|
436
|
+
**Response:** `{ "ok": true, "folder": "Projects" }`
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
### GET /mail/folders/:folder
|
|
441
|
+
|
|
442
|
+
**Auth:** Agent
|
|
443
|
+
|
|
444
|
+
**Query Params:** `limit` (1–200, default 20), `offset` (0+)
|
|
445
|
+
|
|
446
|
+
**Response:**
|
|
447
|
+
```json
|
|
448
|
+
{
|
|
449
|
+
"messages": [...],
|
|
450
|
+
"count": 20,
|
|
451
|
+
"total": 150,
|
|
452
|
+
"folder": "Archive"
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## Routes: Batch Operations
|
|
459
|
+
|
|
460
|
+
All batch endpoints require Agent auth. Max 1000 UIDs per request. All UIDs must be positive integers.
|
|
461
|
+
|
|
462
|
+
### POST /mail/batch/delete
|
|
463
|
+
|
|
464
|
+
**Request:** `{ "uids": [1, 2, 3], "folder": "INBOX" }`
|
|
465
|
+
|
|
466
|
+
**Response:** `{ "ok": true, "deleted": 3 }`
|
|
467
|
+
|
|
468
|
+
### POST /mail/batch/seen
|
|
469
|
+
|
|
470
|
+
**Request:** `{ "uids": [1, 2, 3], "folder": "INBOX" }`
|
|
471
|
+
|
|
472
|
+
**Response:** `{ "ok": true, "marked": 3 }`
|
|
473
|
+
|
|
474
|
+
### POST /mail/batch/unseen
|
|
475
|
+
|
|
476
|
+
**Request:** `{ "uids": [1, 2, 3], "folder": "INBOX" }`
|
|
477
|
+
|
|
478
|
+
**Response:** `{ "ok": true, "marked": 3 }`
|
|
479
|
+
|
|
480
|
+
### POST /mail/batch/move
|
|
481
|
+
|
|
482
|
+
**Request:** `{ "uids": [1, 2, 3], "from": "INBOX", "to": "Archive" }`
|
|
483
|
+
|
|
484
|
+
**Response:** `{ "ok": true, "moved": 3 }`
|
|
485
|
+
|
|
486
|
+
### POST /mail/batch/read
|
|
487
|
+
|
|
488
|
+
**Request:** `{ "uids": [1, 2, 3], "folder": "INBOX" }`
|
|
489
|
+
|
|
490
|
+
**Response:** `{ "messages": [{uid, subject, from, to, text, html, ...}], "count": 3 }`
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
## Routes: Spam
|
|
495
|
+
|
|
496
|
+
### GET /mail/spam
|
|
497
|
+
|
|
498
|
+
**Auth:** Agent
|
|
499
|
+
|
|
500
|
+
**Query Params:** `limit` (1–200, default 20), `offset` (0+)
|
|
501
|
+
|
|
502
|
+
**Response:** Same structure as folder listing (messages from Spam folder).
|
|
503
|
+
|
|
504
|
+
### POST /mail/messages/:uid/spam
|
|
505
|
+
|
|
506
|
+
**Auth:** Agent
|
|
507
|
+
|
|
508
|
+
**Request:** `{ "folder": "INBOX" }` (optional, source folder)
|
|
509
|
+
|
|
510
|
+
Creates Spam folder if needed, moves message there.
|
|
511
|
+
|
|
512
|
+
**Response:** `{ "ok": true, "movedToSpam": true }`
|
|
513
|
+
|
|
514
|
+
### POST /mail/messages/:uid/not-spam
|
|
515
|
+
|
|
516
|
+
**Auth:** Agent
|
|
517
|
+
|
|
518
|
+
Moves message from Spam to INBOX.
|
|
519
|
+
|
|
520
|
+
**Response:** `{ "ok": true, "movedToInbox": true }`
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## Routes: Pending Outbound (Human-Only Approval)
|
|
525
|
+
|
|
526
|
+
### GET /mail/pending
|
|
527
|
+
|
|
528
|
+
**Auth:** Both (agent sees own, master sees all)
|
|
529
|
+
|
|
530
|
+
**Response:**
|
|
531
|
+
```json
|
|
532
|
+
{
|
|
533
|
+
"pending": [{
|
|
534
|
+
"id": "uuid",
|
|
535
|
+
"agentId": "uuid",
|
|
536
|
+
"to": "string | string[]",
|
|
537
|
+
"subject": "string",
|
|
538
|
+
"warnings": [...],
|
|
539
|
+
"summary": "string",
|
|
540
|
+
"status": "pending | approved | rejected",
|
|
541
|
+
"createdAt": "datetime",
|
|
542
|
+
"resolvedAt": "datetime | null",
|
|
543
|
+
"resolvedBy": "master | null"
|
|
544
|
+
}],
|
|
545
|
+
"count": 5
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### GET /mail/pending/:id
|
|
550
|
+
|
|
551
|
+
**Auth:** Both (agent sees own only, master sees any)
|
|
552
|
+
|
|
553
|
+
**Response:** Full pending email details including `mailOptions`.
|
|
554
|
+
|
|
555
|
+
### POST /mail/pending/:id/approve
|
|
556
|
+
|
|
557
|
+
**Auth:** Master only (403 for agent keys)
|
|
558
|
+
|
|
559
|
+
**Flow:**
|
|
560
|
+
1. Look up pending email, verify status is "pending"
|
|
561
|
+
2. Look up originating agent
|
|
562
|
+
3. Re-parse stored `mailOptions` from JSON
|
|
563
|
+
4. Refresh `fromName` from current agent metadata
|
|
564
|
+
5. Reconstitute Buffer objects (attachments)
|
|
565
|
+
6. Send via gateway first, fallback to local SMTP
|
|
566
|
+
7. Update: `status = 'approved'`, `resolved_at = datetime('now')`, `resolved_by = 'master'`
|
|
567
|
+
|
|
568
|
+
**Response:** `{ "sent": true, "messageId": "...", "approved": true, "pendingId": "uuid" }`
|
|
569
|
+
|
|
570
|
+
### POST /mail/pending/:id/reject
|
|
571
|
+
|
|
572
|
+
**Auth:** Master only (403 for agent keys)
|
|
573
|
+
|
|
574
|
+
Updates: `status = 'rejected'`, `resolved_at = datetime('now')`, `resolved_by = 'master'`
|
|
575
|
+
|
|
576
|
+
**Response:** `{ "ok": true, "rejected": true, "pendingId": "uuid" }`
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
580
|
+
## Routes: Events (SSE)
|
|
581
|
+
|
|
582
|
+
### GET /events
|
|
583
|
+
|
|
584
|
+
**Auth:** Agent (`requireAgent`)
|
|
585
|
+
|
|
586
|
+
**Connection Limit:** 5 per agent (returns 429 if exceeded)
|
|
587
|
+
|
|
588
|
+
**SSE Headers:**
|
|
589
|
+
```
|
|
590
|
+
Content-Type: text/event-stream
|
|
591
|
+
Cache-Control: no-cache
|
|
592
|
+
Connection: keep-alive
|
|
593
|
+
X-Accel-Buffering: no
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
**Keepalive:** `: ping\n\n` every 30 seconds
|
|
597
|
+
|
|
598
|
+
### Event Types
|
|
599
|
+
|
|
600
|
+
**`connected`**
|
|
601
|
+
```json
|
|
602
|
+
{ "type": "connected", "agentId": "uuid" }
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
**`new`** — New email received
|
|
606
|
+
```json
|
|
607
|
+
{
|
|
608
|
+
"type": "new",
|
|
609
|
+
"uid": 123,
|
|
610
|
+
"subject": "string",
|
|
611
|
+
"from": [{ "name": "string", "address": "string" }],
|
|
612
|
+
"date": "ISO-8601",
|
|
613
|
+
"spam": { // Only for external emails flagged as spam
|
|
614
|
+
"score": 75,
|
|
615
|
+
"isSpam": true,
|
|
616
|
+
"topCategory": "phishing",
|
|
617
|
+
"matches": ["ruleId"],
|
|
618
|
+
"movedToSpam": true
|
|
619
|
+
},
|
|
620
|
+
"warning": { // Only for elevated-score external emails
|
|
621
|
+
"score": 40,
|
|
622
|
+
"isWarning": true,
|
|
623
|
+
"topCategory": "link_analysis",
|
|
624
|
+
"matches": ["ruleId"]
|
|
625
|
+
},
|
|
626
|
+
"rule": { // Only if an email rule matched
|
|
627
|
+
"ruleId": "uuid",
|
|
628
|
+
"ruleName": "Auto-archive newsletters",
|
|
629
|
+
"actions": { "move_to": "Archive" }
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
**Processing order for new emails:**
|
|
635
|
+
1. Relay detection (check `X-AgenticMail-Relay` header)
|
|
636
|
+
2. Internal check (skip spam filter for agent-to-agent)
|
|
637
|
+
3. Spam scoring → auto-move to Spam if threshold exceeded
|
|
638
|
+
4. Rule evaluation → first matching rule's actions execute
|
|
639
|
+
5. Event pushed to all agent's SSE connections
|
|
640
|
+
|
|
641
|
+
**`expunge`** — Message deleted
|
|
642
|
+
```json
|
|
643
|
+
{ "type": "expunge", "uid": 123 }
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
**`flags`** — Flags changed
|
|
647
|
+
```json
|
|
648
|
+
{ "type": "flags", "uid": 123, "flags": ["\\Seen"] }
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
**`error`** — IMAP connection error
|
|
652
|
+
```json
|
|
653
|
+
{ "type": "error", "message": "Connection lost" }
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
**`task`** — Task assigned (pushed via `pushEventToAgent()`)
|
|
657
|
+
```json
|
|
658
|
+
{ "type": "task", "taskId": "uuid", "taskType": "string", "from": "agentName" }
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### Helper Functions
|
|
662
|
+
|
|
663
|
+
| Function | Purpose |
|
|
664
|
+
|----------|---------|
|
|
665
|
+
| `pushEventToAgent(agentId, event)` | Push event to specific agent's SSE connections |
|
|
666
|
+
| `broadcastEvent(event)` | Push event to ALL active SSE connections |
|
|
667
|
+
| `closeAllWatchers()` | Stop all watchers, clear all connections (shutdown) |
|
|
668
|
+
|
|
669
|
+
---
|
|
670
|
+
|
|
671
|
+
## Routes: Accounts
|
|
672
|
+
|
|
673
|
+
### POST /accounts
|
|
674
|
+
|
|
675
|
+
**Auth:** Master
|
|
676
|
+
|
|
677
|
+
**Request Body:**
|
|
678
|
+
```json
|
|
679
|
+
{
|
|
680
|
+
"name": "string", // Required, max 64 chars, /^[a-zA-Z0-9._-]+$/
|
|
681
|
+
"domain": "string", // Optional
|
|
682
|
+
"password": "string", // Optional (auto-generated if omitted)
|
|
683
|
+
"metadata": {}, // Optional (object, _prefixed keys stripped)
|
|
684
|
+
"role": "string", // Optional (must be in AGENT_ROLES)
|
|
685
|
+
"persistent": true // Optional (first agent auto-persistent)
|
|
686
|
+
}
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
**Response (201):** Sanitized agent object (internal `_`-prefixed metadata stripped)
|
|
690
|
+
|
|
691
|
+
### GET /accounts
|
|
692
|
+
|
|
693
|
+
**Auth:** Master
|
|
694
|
+
|
|
695
|
+
**Response:** `{ "agents": [Agent, ...] }`
|
|
696
|
+
|
|
697
|
+
### GET /accounts/directory
|
|
698
|
+
|
|
699
|
+
**Auth:** Both
|
|
700
|
+
|
|
701
|
+
**Response:** `{ "agents": [{ "name": "...", "email": "...", "role": "..." }] }`
|
|
702
|
+
|
|
703
|
+
### GET /accounts/directory/:name
|
|
704
|
+
|
|
705
|
+
**Auth:** Both
|
|
706
|
+
|
|
707
|
+
**Response:** `{ "name", "email", "role" }` or 404
|
|
708
|
+
|
|
709
|
+
### GET /accounts/me
|
|
710
|
+
|
|
711
|
+
**Auth:** Agent (`requireAgent`)
|
|
712
|
+
|
|
713
|
+
**Response:** Sanitized agent object
|
|
714
|
+
|
|
715
|
+
### PATCH /accounts/me
|
|
716
|
+
|
|
717
|
+
**Auth:** Agent
|
|
718
|
+
|
|
719
|
+
**Request:** `{ "metadata": { "ownerName": "John" } }`
|
|
720
|
+
|
|
721
|
+
**Response:** Updated agent
|
|
722
|
+
|
|
723
|
+
### GET /accounts/:id
|
|
724
|
+
|
|
725
|
+
**Auth:** Master
|
|
726
|
+
|
|
727
|
+
**Response:** Sanitized agent or 404
|
|
728
|
+
|
|
729
|
+
### DELETE /accounts/:id
|
|
730
|
+
|
|
731
|
+
**Auth:** Master
|
|
732
|
+
|
|
733
|
+
**Query Params:**
|
|
734
|
+
- `archive` (default: true) — archive emails before deletion
|
|
735
|
+
- `reason` — deletion reason
|
|
736
|
+
- `deletedBy` (default: "api")
|
|
737
|
+
|
|
738
|
+
**Validation:** Cannot delete last remaining agent (400).
|
|
739
|
+
|
|
740
|
+
**Response:** Deletion summary (if archive=true) or 204
|
|
741
|
+
|
|
742
|
+
### PATCH /accounts/:id/persistent
|
|
743
|
+
|
|
744
|
+
**Auth:** Master
|
|
745
|
+
|
|
746
|
+
**Request:** `{ "persistent": true }`
|
|
747
|
+
|
|
748
|
+
**Response:** `{ "ok": true, "persistent": true }`
|
|
749
|
+
|
|
750
|
+
### GET /accounts/inactive
|
|
751
|
+
|
|
752
|
+
**Auth:** Master
|
|
753
|
+
|
|
754
|
+
**Query Params:** `hours` (default: 24, min: 1)
|
|
755
|
+
|
|
756
|
+
Uses `COALESCE(last_activity_at, created_at)` so new agents aren't flagged.
|
|
757
|
+
|
|
758
|
+
**Response:** `{ "agents": [...], "count": 3 }`
|
|
759
|
+
|
|
760
|
+
### POST /accounts/cleanup
|
|
761
|
+
|
|
762
|
+
**Auth:** Master
|
|
763
|
+
|
|
764
|
+
**Request:** `{ "hours": 24, "dryRun": false }`
|
|
765
|
+
|
|
766
|
+
**Response:** `{ "deleted": ["agent1"], "count": 1, "dryRun": false }`
|
|
767
|
+
|
|
768
|
+
### GET /accounts/deletions
|
|
769
|
+
|
|
770
|
+
**Auth:** Master
|
|
771
|
+
|
|
772
|
+
**Response:** `{ "deletions": [DeletionReport, ...] }`
|
|
773
|
+
|
|
774
|
+
### GET /accounts/deletions/:id
|
|
775
|
+
|
|
776
|
+
**Auth:** Master
|
|
777
|
+
|
|
778
|
+
**Response:** Full deletion report or 404
|
|
779
|
+
|
|
780
|
+
---
|
|
781
|
+
|
|
782
|
+
## Routes: Tasks
|
|
783
|
+
|
|
784
|
+
### POST /tasks/assign
|
|
785
|
+
|
|
786
|
+
**Auth:** Both
|
|
787
|
+
|
|
788
|
+
**Request Body:**
|
|
789
|
+
```json
|
|
790
|
+
{
|
|
791
|
+
"assignee": "string", // Required (agent name)
|
|
792
|
+
"taskType": "string", // Optional (default: "generic")
|
|
793
|
+
"payload": {}, // Optional
|
|
794
|
+
"expiresInSeconds": 3600 // Optional
|
|
795
|
+
}
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
**Notification cascade:**
|
|
799
|
+
1. SSE push to assignee via `pushEventToAgent()`
|
|
800
|
+
2. Broadcast to all SSE if no direct watcher
|
|
801
|
+
3. Email notification (fire-and-forget)
|
|
802
|
+
|
|
803
|
+
**Response (201):**
|
|
804
|
+
```json
|
|
805
|
+
{ "id": "uuid", "assignee": "name", "assigneeId": "uuid", "status": "pending" }
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
### GET /tasks/pending
|
|
809
|
+
|
|
810
|
+
**Auth:** Agent
|
|
811
|
+
|
|
812
|
+
**Query Params:** `assignee` (optional — check different agent's tasks)
|
|
813
|
+
|
|
814
|
+
**Response:** `{ "tasks": [...], "count": 5 }`
|
|
815
|
+
|
|
816
|
+
### GET /tasks/assigned
|
|
817
|
+
|
|
818
|
+
**Auth:** Both
|
|
819
|
+
|
|
820
|
+
**Response:** Tasks assigned BY current user (limit 50)
|
|
821
|
+
|
|
822
|
+
### GET /tasks/:id
|
|
823
|
+
|
|
824
|
+
**Auth:** Both
|
|
825
|
+
|
|
826
|
+
**Response:** Full task object with parsed payload/result
|
|
827
|
+
|
|
828
|
+
### POST /tasks/:id/claim
|
|
829
|
+
|
|
830
|
+
**Auth:** Agent
|
|
831
|
+
|
|
832
|
+
Updates: `pending → claimed`, sets `claimed_at`.
|
|
833
|
+
|
|
834
|
+
Capability-based: any agent with task ID can claim (supports sub-agents).
|
|
835
|
+
|
|
836
|
+
**Response:** Claimed task object or 404
|
|
837
|
+
|
|
838
|
+
### POST /tasks/:id/result
|
|
839
|
+
|
|
840
|
+
**Auth:** Agent
|
|
841
|
+
|
|
842
|
+
**Request:** `{ "result": {} }`
|
|
843
|
+
|
|
844
|
+
Updates: `claimed → completed`, stores result, sets `completed_at`.
|
|
845
|
+
|
|
846
|
+
**Instant RPC wake:** If assigner is long-polling via `/tasks/rpc`, resolver is called immediately.
|
|
847
|
+
|
|
848
|
+
**Response:** `{ "ok": true, "taskId": "...", "status": "completed" }`
|
|
849
|
+
|
|
850
|
+
### POST /tasks/:id/fail
|
|
851
|
+
|
|
852
|
+
**Auth:** Agent
|
|
853
|
+
|
|
854
|
+
**Request:** `{ "error": "reason" }`
|
|
855
|
+
|
|
856
|
+
Updates: `claimed → failed`, stores error, sets `completed_at`.
|
|
857
|
+
|
|
858
|
+
**Response:** `{ "ok": true, "taskId": "...", "status": "failed" }`
|
|
859
|
+
|
|
860
|
+
### POST /tasks/rpc
|
|
861
|
+
|
|
862
|
+
**Auth:** Both
|
|
863
|
+
|
|
864
|
+
**Request Body:**
|
|
865
|
+
```json
|
|
866
|
+
{
|
|
867
|
+
"target": "string", // Required (agent name)
|
|
868
|
+
"task": "string", // Required (task description)
|
|
869
|
+
"payload": {}, // Optional
|
|
870
|
+
"timeout": 180 // Optional (5–300 seconds, default 180)
|
|
871
|
+
}
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
**Mechanism:**
|
|
875
|
+
1. Creates task with `task_type = 'rpc'`
|
|
876
|
+
2. Disables socket timeout: `req.socket.setTimeout(0)`
|
|
877
|
+
3. Pushes SSE event + email notification
|
|
878
|
+
4. Registers resolver in `rpcResolvers: Map<string, Function>`
|
|
879
|
+
5. Polls every 2 seconds as fallback
|
|
880
|
+
6. When `/result` or `/fail` called → resolver fires instantly
|
|
881
|
+
|
|
882
|
+
**Response:**
|
|
883
|
+
```json
|
|
884
|
+
{
|
|
885
|
+
"taskId": "uuid",
|
|
886
|
+
"status": "completed | failed | timeout | disconnected",
|
|
887
|
+
"result": {}, // If completed
|
|
888
|
+
"error": "string", // If failed
|
|
889
|
+
"message": "string" // If timeout
|
|
890
|
+
}
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
---
|
|
894
|
+
|
|
895
|
+
## Routes: Gateway
|
|
896
|
+
|
|
897
|
+
### GET /gateway/setup-guide
|
|
898
|
+
|
|
899
|
+
**Auth:** Master
|
|
900
|
+
|
|
901
|
+
Returns comparison of relay mode vs domain mode with pros/cons and requirements.
|
|
902
|
+
|
|
903
|
+
### GET /gateway/status
|
|
904
|
+
|
|
905
|
+
**Auth:** Master
|
|
906
|
+
|
|
907
|
+
Returns current gateway configuration and status.
|
|
908
|
+
|
|
909
|
+
### POST /gateway/relay
|
|
910
|
+
|
|
911
|
+
**Auth:** Master
|
|
912
|
+
|
|
913
|
+
**Request Body:**
|
|
914
|
+
```json
|
|
915
|
+
{
|
|
916
|
+
"provider": "gmail | outlook | custom", // Default: "custom"
|
|
917
|
+
"email": "string", // Required
|
|
918
|
+
"password": "string", // Required (app password)
|
|
919
|
+
"smtpHost": "string", // Optional (preset for gmail/outlook)
|
|
920
|
+
"smtpPort": 465, // Optional
|
|
921
|
+
"imapHost": "string", // Optional
|
|
922
|
+
"imapPort": 993, // Optional
|
|
923
|
+
"agentName": "string", // Optional (auto-create agent)
|
|
924
|
+
"agentRole": "string", // Optional
|
|
925
|
+
"skipDefaultAgent": false // Optional
|
|
926
|
+
}
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
**Provider Presets:**
|
|
930
|
+
- Gmail: `smtp.gmail.com:465`, `imap.gmail.com:993`
|
|
931
|
+
- Outlook: `smtp-mail.outlook.com:587`, `outlook.office365.com:993`
|
|
932
|
+
|
|
933
|
+
**Response:**
|
|
934
|
+
```json
|
|
935
|
+
{
|
|
936
|
+
"status": "ok",
|
|
937
|
+
"mode": "relay",
|
|
938
|
+
"email": "you@gmail.com",
|
|
939
|
+
"provider": "gmail",
|
|
940
|
+
"agent": {
|
|
941
|
+
"id": "uuid",
|
|
942
|
+
"name": "secretary",
|
|
943
|
+
"email": "secretary@local",
|
|
944
|
+
"apiKey": "ak_...",
|
|
945
|
+
"role": "admin",
|
|
946
|
+
"subAddress": "you+secretary@gmail.com"
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
### POST /gateway/domain
|
|
952
|
+
|
|
953
|
+
**Auth:** Master
|
|
954
|
+
|
|
955
|
+
**Request Body:**
|
|
956
|
+
```json
|
|
957
|
+
{
|
|
958
|
+
"cloudflareToken": "string", // Required
|
|
959
|
+
"cloudflareAccountId": "string", // Required
|
|
960
|
+
"domain": "string", // Optional (use existing)
|
|
961
|
+
"purchase": {}, // Optional (buy new domain)
|
|
962
|
+
"gmailRelay": { // Optional (Gmail for outbound)
|
|
963
|
+
"email": "string",
|
|
964
|
+
"appPassword": "string"
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
### POST /gateway/domain/alias-setup
|
|
970
|
+
|
|
971
|
+
**Auth:** Master
|
|
972
|
+
|
|
973
|
+
**Request:** `{ "agentEmail": "secretary@yourdomain.com", "agentDisplayName": "Secretary" }`
|
|
974
|
+
|
|
975
|
+
Returns step-by-step Gmail "Send mail as" alias instructions with SMTP settings:
|
|
976
|
+
- Server: `smtp.gmail.com`
|
|
977
|
+
- Port: 465
|
|
978
|
+
- Security: SSL
|
|
979
|
+
|
|
980
|
+
### GET /gateway/domain/payment-setup
|
|
981
|
+
|
|
982
|
+
**Auth:** Master
|
|
983
|
+
|
|
984
|
+
Returns Cloudflare payment method setup instructions.
|
|
985
|
+
|
|
986
|
+
### POST /gateway/domain/purchase
|
|
987
|
+
|
|
988
|
+
**Auth:** Master
|
|
989
|
+
|
|
990
|
+
**Request:** `{ "keywords": ["mycompany"], "tld": "com" }`
|
|
991
|
+
|
|
992
|
+
**Response:** `{ "domains": [{ "domain": "...", "price": "..." }] }`
|
|
993
|
+
|
|
994
|
+
### GET /gateway/domain/dns
|
|
995
|
+
|
|
996
|
+
**Auth:** Master
|
|
997
|
+
|
|
998
|
+
**Response:** `{ "domain": "...", "dns": [...] }`
|
|
999
|
+
|
|
1000
|
+
### POST /gateway/tunnel/start
|
|
1001
|
+
|
|
1002
|
+
**Auth:** Master
|
|
1003
|
+
|
|
1004
|
+
**Response:** `{ "status": "ok", "tunnel": {...} }`
|
|
1005
|
+
|
|
1006
|
+
### POST /gateway/tunnel/stop
|
|
1007
|
+
|
|
1008
|
+
**Auth:** Master
|
|
1009
|
+
|
|
1010
|
+
**Response:** `{ "status": "ok", "tunnel": {...} }`
|
|
1011
|
+
|
|
1012
|
+
### POST /gateway/test
|
|
1013
|
+
|
|
1014
|
+
**Auth:** Master
|
|
1015
|
+
|
|
1016
|
+
**Request:** `{ "to": "test@example.com" }`
|
|
1017
|
+
|
|
1018
|
+
**Response:** `{ "status": "ok", "messageId": "..." }` or 400 if no gateway
|
|
1019
|
+
|
|
1020
|
+
---
|
|
1021
|
+
|
|
1022
|
+
## Routes: Domains
|
|
1023
|
+
|
|
1024
|
+
### POST /domains
|
|
1025
|
+
|
|
1026
|
+
**Auth:** Master
|
|
1027
|
+
|
|
1028
|
+
**Request:** `{ "domain": "yourdomain.com" }`
|
|
1029
|
+
|
|
1030
|
+
**Response (201):** Setup result from `DomainManager`
|
|
1031
|
+
|
|
1032
|
+
### GET /domains
|
|
1033
|
+
|
|
1034
|
+
**Auth:** Master
|
|
1035
|
+
|
|
1036
|
+
**Response:** `{ "domains": [...] }`
|
|
1037
|
+
|
|
1038
|
+
### GET /domains/:domain/dns
|
|
1039
|
+
|
|
1040
|
+
**Auth:** Master
|
|
1041
|
+
|
|
1042
|
+
**Response:** `{ "records": [...] }`
|
|
1043
|
+
|
|
1044
|
+
### POST /domains/:domain/verify
|
|
1045
|
+
|
|
1046
|
+
**Auth:** Master
|
|
1047
|
+
|
|
1048
|
+
**Response:** `{ "domain": "...", "verified": true }`
|
|
1049
|
+
|
|
1050
|
+
### DELETE /domains/:domain
|
|
1051
|
+
|
|
1052
|
+
**Auth:** Master
|
|
1053
|
+
|
|
1054
|
+
**Response:** 204 or 404
|
|
1055
|
+
|
|
1056
|
+
---
|
|
1057
|
+
|
|
1058
|
+
## Routes: Inbound Webhook
|
|
1059
|
+
|
|
1060
|
+
### POST /mail/inbound
|
|
1061
|
+
|
|
1062
|
+
**Auth:** `X-Inbound-Secret` header (NOT Bearer token)
|
|
1063
|
+
|
|
1064
|
+
**Default secret:** `inbound_2sabi_secret_key` (configurable via `AGENTICMAIL_INBOUND_SECRET`)
|
|
1065
|
+
|
|
1066
|
+
**Request Body:**
|
|
1067
|
+
```json
|
|
1068
|
+
{
|
|
1069
|
+
"from": "sender@external.com",
|
|
1070
|
+
"to": "agentname@yourdomain.com",
|
|
1071
|
+
"subject": "string",
|
|
1072
|
+
"rawEmail": "base64-encoded-mime"
|
|
1073
|
+
}
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
**Flow:**
|
|
1077
|
+
1. Validate secret header
|
|
1078
|
+
2. Extract local part from `to` address → agent lookup
|
|
1079
|
+
3. Deduplication check: `gatewayManager.isAlreadyDelivered(messageId, agentName)`
|
|
1080
|
+
4. Decode base64 → parse with `parseEmail()`
|
|
1081
|
+
5. Deliver to agent's mailbox via SMTP (authenticated as agent)
|
|
1082
|
+
6. Custom headers: `X-AgenticMail-Inbound: cloudflare-worker`, `X-Original-From`, `X-Original-Message-Id`
|
|
1083
|
+
7. Preserve threading: `inReplyTo`, `references`
|
|
1084
|
+
8. Record delivery: `gatewayManager.recordDelivery(messageId, agentName)`
|
|
1085
|
+
|
|
1086
|
+
**Response:** `{ "ok": true, "delivered": "agent@domain.com" }`
|
|
1087
|
+
|
|
1088
|
+
**Duplicate Response:** `{ "ok": true, "delivered": "agent@domain.com", "duplicate": true }`
|
|
1089
|
+
|
|
1090
|
+
---
|
|
1091
|
+
|
|
1092
|
+
## Routes: Features
|
|
1093
|
+
|
|
1094
|
+
### Contacts
|
|
1095
|
+
|
|
1096
|
+
| Method | Path | Auth | Description |
|
|
1097
|
+
|--------|------|------|-------------|
|
|
1098
|
+
| `GET` | `/contacts` | Agent | List contacts (`ORDER BY name, email`) |
|
|
1099
|
+
| `POST` | `/contacts` | Agent | Create/update contact. Body: `{ name, email, notes }` |
|
|
1100
|
+
| `DELETE` | `/contacts/:id` | Agent | Delete contact |
|
|
1101
|
+
|
|
1102
|
+
SQL uses `INSERT OR REPLACE` — adding a contact with existing email updates it.
|
|
1103
|
+
|
|
1104
|
+
### Drafts
|
|
1105
|
+
|
|
1106
|
+
| Method | Path | Auth | Description |
|
|
1107
|
+
|--------|------|------|-------------|
|
|
1108
|
+
| `GET` | `/drafts` | Agent | List drafts (`ORDER BY updated_at DESC`) |
|
|
1109
|
+
| `POST` | `/drafts` | Agent | Create draft. Body: `{ to, subject, text, html, cc, bcc, inReplyTo, references }` |
|
|
1110
|
+
| `PUT` | `/drafts/:id` | Agent | Update draft (all fields) |
|
|
1111
|
+
| `DELETE` | `/drafts/:id` | Agent | Delete draft |
|
|
1112
|
+
| `POST` | `/drafts/:id/send` | Agent | Send draft and delete it |
|
|
1113
|
+
|
|
1114
|
+
### Signatures
|
|
1115
|
+
|
|
1116
|
+
| Method | Path | Auth | Description |
|
|
1117
|
+
|--------|------|------|-------------|
|
|
1118
|
+
| `GET` | `/signatures` | Agent | List signatures (`ORDER BY is_default DESC, name`) |
|
|
1119
|
+
| `POST` | `/signatures` | Agent | Create. Body: `{ name, text, html, isDefault }` |
|
|
1120
|
+
| `DELETE` | `/signatures/:id` | Agent | Delete |
|
|
1121
|
+
|
|
1122
|
+
Setting `isDefault: true` automatically unsets all other signatures.
|
|
1123
|
+
|
|
1124
|
+
### Templates
|
|
1125
|
+
|
|
1126
|
+
| Method | Path | Auth | Description |
|
|
1127
|
+
|--------|------|------|-------------|
|
|
1128
|
+
| `GET` | `/templates` | Agent | List templates (`ORDER BY name`) |
|
|
1129
|
+
| `POST` | `/templates` | Agent | Create. Body: `{ name, subject, text, html }` |
|
|
1130
|
+
| `DELETE` | `/templates/:id` | Agent | Delete |
|
|
1131
|
+
| `POST` | `/templates/:id/send` | Agent | Send from template |
|
|
1132
|
+
|
|
1133
|
+
**Template Send:**
|
|
1134
|
+
```json
|
|
1135
|
+
{
|
|
1136
|
+
"to": "string", // Required
|
|
1137
|
+
"variables": { // Optional
|
|
1138
|
+
"name": "John",
|
|
1139
|
+
"company": "Acme"
|
|
1140
|
+
},
|
|
1141
|
+
"cc": "string",
|
|
1142
|
+
"bcc": "string"
|
|
1143
|
+
}
|
|
1144
|
+
```
|
|
1145
|
+
Replaces `{{ variableName }}` in subject and body with provided values.
|
|
1146
|
+
|
|
1147
|
+
### Scheduled Emails
|
|
1148
|
+
|
|
1149
|
+
| Method | Path | Auth | Description |
|
|
1150
|
+
|--------|------|------|-------------|
|
|
1151
|
+
| `GET` | `/scheduled` | Agent | List scheduled (`ORDER BY send_at ASC`) |
|
|
1152
|
+
| `POST` | `/scheduled` | Agent | Schedule email |
|
|
1153
|
+
| `DELETE` | `/scheduled/:id` | Agent | Cancel (pending only) |
|
|
1154
|
+
|
|
1155
|
+
**Schedule Request:**
|
|
1156
|
+
```json
|
|
1157
|
+
{
|
|
1158
|
+
"to": "string", // Required
|
|
1159
|
+
"subject": "string", // Required
|
|
1160
|
+
"text": "string",
|
|
1161
|
+
"html": "string",
|
|
1162
|
+
"cc": "string",
|
|
1163
|
+
"bcc": "string",
|
|
1164
|
+
"sendAt": "string" // Required (see format list below)
|
|
1165
|
+
}
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
**Supported `sendAt` formats:**
|
|
1169
|
+
| Format | Example |
|
|
1170
|
+
|--------|---------|
|
|
1171
|
+
| ISO 8601 | `2026-02-14T10:00:00Z` |
|
|
1172
|
+
| Relative | `in 30 minutes`, `in 2 hours` |
|
|
1173
|
+
| Named | `tomorrow 8am`, `tomorrow 2pm` |
|
|
1174
|
+
| Day of week | `next monday 9am`, `next friday 2pm` |
|
|
1175
|
+
| MM-DD-YYYY | `02-14-2026 3:30 PM EST` |
|
|
1176
|
+
| Casual | `tonight`, `this evening` (20:00) |
|
|
1177
|
+
|
|
1178
|
+
**Timezone abbreviations supported:** EST, EDT, CST, CDT, MST, MDT, PST, PDT, GMT, UTC, BST, CET, CEST, IST, JST, AEST, AEDT, NZST, NZDT, WAT, EAT, SAST, HKT, SGT, KST, HST, AKST, AKDT, AST, ADT, NST, NDT
|
|
1179
|
+
|
|
1180
|
+
### Tags
|
|
1181
|
+
|
|
1182
|
+
| Method | Path | Auth | Description |
|
|
1183
|
+
|--------|------|------|-------------|
|
|
1184
|
+
| `GET` | `/tags` | Agent | List tags (`ORDER BY name`) |
|
|
1185
|
+
| `POST` | `/tags` | Agent | Create. Body: `{ name, color }` (default color: `#888888`) |
|
|
1186
|
+
| `DELETE` | `/tags/:id` | Agent | Delete tag |
|
|
1187
|
+
| `POST` | `/tags/:id/messages` | Agent | Tag a message. Body: `{ uid, folder }` |
|
|
1188
|
+
| `DELETE` | `/tags/:id/messages/:uid` | Agent | Untag. Query: `?folder=INBOX` |
|
|
1189
|
+
| `GET` | `/tags/:id/messages` | Agent | List messages with tag |
|
|
1190
|
+
| `GET` | `/messages/:uid/tags` | Agent | List tags on message |
|
|
1191
|
+
|
|
1192
|
+
### Email Rules
|
|
1193
|
+
|
|
1194
|
+
| Method | Path | Auth | Description |
|
|
1195
|
+
|--------|------|------|-------------|
|
|
1196
|
+
| `GET` | `/rules` | Agent | List rules (`ORDER BY priority DESC, created_at`) |
|
|
1197
|
+
| `POST` | `/rules` | Agent | Create rule |
|
|
1198
|
+
| `DELETE` | `/rules/:id` | Agent | Delete rule |
|
|
1199
|
+
|
|
1200
|
+
**Rule Request (201):**
|
|
1201
|
+
```json
|
|
1202
|
+
{
|
|
1203
|
+
"name": "Auto-archive newsletters",
|
|
1204
|
+
"conditions": {
|
|
1205
|
+
"from_contains": "newsletter",
|
|
1206
|
+
"from_exact": "news@example.com",
|
|
1207
|
+
"subject_contains": "weekly update",
|
|
1208
|
+
"subject_regex": "\\[Newsletter\\]",
|
|
1209
|
+
"to_contains": "list",
|
|
1210
|
+
"has_attachment": true
|
|
1211
|
+
},
|
|
1212
|
+
"actions": {
|
|
1213
|
+
"mark_read": true,
|
|
1214
|
+
"delete": false,
|
|
1215
|
+
"move_to": "Archive"
|
|
1216
|
+
},
|
|
1217
|
+
"priority": 10,
|
|
1218
|
+
"enabled": true
|
|
1219
|
+
}
|
|
1220
|
+
```
|
|
1221
|
+
|
|
1222
|
+
**Rule evaluation:** Runs on every new email (via SSE event handler). Rules checked by priority (highest first). First match wins. Conditions are case-insensitive. `subject_regex` uses JavaScript RegExp.
|
|
1223
|
+
|
|
1224
|
+
---
|
|
1225
|
+
|
|
1226
|
+
## Background Services
|
|
1227
|
+
|
|
1228
|
+
### Scheduled Email Sender
|
|
1229
|
+
|
|
1230
|
+
**Interval:** 30,000ms (30 seconds)
|
|
1231
|
+
|
|
1232
|
+
**Per cycle:**
|
|
1233
|
+
1. Query: `SELECT * FROM scheduled_emails WHERE status = 'pending' AND send_at <= datetime('now')`
|
|
1234
|
+
2. For each: look up agent, build mail options, send via gateway or SMTP
|
|
1235
|
+
3. On success: `status = 'sent'`, `sent_at = datetime('now')`
|
|
1236
|
+
4. On failure: `status = 'failed'`, `error = message`
|
|
1237
|
+
|
|
1238
|
+
**Housekeeping (runs each cycle):**
|
|
1239
|
+
- `DELETE FROM delivered_messages WHERE delivered_at < datetime('now', '-30 days')`
|
|
1240
|
+
- `DELETE FROM spam_log WHERE created_at < datetime('now', '-30 days')`
|
|
1241
|
+
|
|
1242
|
+
### Gateway Resume
|
|
1243
|
+
|
|
1244
|
+
On startup, calls `gatewayManager.resume()` to restore relay polling or domain tunnel from saved configuration.
|
|
1245
|
+
|
|
1246
|
+
---
|
|
1247
|
+
|
|
1248
|
+
## Shutdown Sequence
|
|
1249
|
+
|
|
1250
|
+
1. Set `shuttingDown = true`
|
|
1251
|
+
2. Clear scheduled sender interval
|
|
1252
|
+
3. `closeAllWatchers()` — stop all SSE watchers and IMAP connections
|
|
1253
|
+
4. `closeCaches()` — set `draining = true`, close all cached SMTP/IMAP connections
|
|
1254
|
+
5. `gatewayManager.shutdown()` — stop relay polling, tunnel
|
|
1255
|
+
6. `server.close()` — stop accepting connections
|
|
1256
|
+
7. `setTimeout(() => process.exit(0), 5000)` — force exit safety net
|
|
1257
|
+
|
|
1258
|
+
Signals: SIGTERM and SIGINT both trigger shutdown.
|
|
1259
|
+
|
|
1260
|
+
---
|
|
1261
|
+
|
|
1262
|
+
## Constants Summary
|
|
1263
|
+
|
|
1264
|
+
| Constant | Value | Location |
|
|
1265
|
+
|----------|-------|----------|
|
|
1266
|
+
| Request body limit | 10 MB | `app.ts` |
|
|
1267
|
+
| Rate limit window | 60,000ms | `app.ts` |
|
|
1268
|
+
| Rate limit max | 100 requests/window | `app.ts` |
|
|
1269
|
+
| Activity throttle | 60,000ms | `auth.ts` |
|
|
1270
|
+
| Cache TTL | 600,000ms (10 min) | `mail.ts` |
|
|
1271
|
+
| Max cache entries | 100 | `mail.ts` |
|
|
1272
|
+
| Cache eviction interval | 60,000ms | `mail.ts` |
|
|
1273
|
+
| Max SSE per agent | 5 | `events.ts` |
|
|
1274
|
+
| SSE ping interval | 30,000ms | `events.ts` |
|
|
1275
|
+
| Scheduled sender interval | 30,000ms | `features.ts` |
|
|
1276
|
+
| Data retention (spam_log) | 30 days | `features.ts` |
|
|
1277
|
+
| Data retention (delivered_messages) | 30 days | `features.ts` |
|
|
1278
|
+
| Max batch UIDs | 1,000 | `mail.ts` |
|
|
1279
|
+
| Max folder name length | 200 chars | `mail.ts` |
|
|
1280
|
+
| RPC timeout min | 5,000ms | `tasks.ts` |
|
|
1281
|
+
| RPC timeout max | 300,000ms (5 min) | `tasks.ts` |
|
|
1282
|
+
| RPC timeout default | 180,000ms (3 min) | `tasks.ts` |
|
|
1283
|
+
| RPC poll interval | 2,000ms | `tasks.ts` |
|
|
1284
|
+
| Shutdown force exit | 5,000ms | `index.ts` |
|
|
1285
|
+
| Agent name max length | 64 chars | `accounts.ts` |
|
|
1286
|
+
| Default tag color | `#888888` | `features.ts` |
|
|
1287
|
+
| Digest preview default | 200 chars | `mail.ts` |
|
|
1288
|
+
| Digest preview range | 50–500 chars | `mail.ts` |
|
|
1289
|
+
| Inbox limit range | 1–200 | `mail.ts` |
|
|
1290
|
+
| Digest limit range | 1–50 | `mail.ts` |
|
|
1291
|
+
| Default inbound secret | `inbound_2sabi_secret_key` | `inbound.ts` |
|
|
1292
|
+
|
|
1293
|
+
---
|
|
1294
|
+
|
|
1295
|
+
## Environment Variables
|
|
1296
|
+
|
|
1297
|
+
| Variable | Required | Default | Description |
|
|
1298
|
+
|----------|----------|---------|-------------|
|
|
1299
|
+
| `AGENTICMAIL_MASTER_KEY` | Yes | — | Master API key |
|
|
1300
|
+
| `AGENTICMAIL_API_PORT` | No | `3100` | Server port |
|
|
1301
|
+
| `STALWART_URL` | No | `http://localhost:8080` | Stalwart admin URL |
|
|
1302
|
+
| `STALWART_ADMIN_USER` | No | `admin` | Stalwart admin user |
|
|
1303
|
+
| `STALWART_ADMIN_PASSWORD` | No | `changeme` | Stalwart admin password |
|
|
1304
|
+
| `SMTP_HOST` | No | `localhost` | SMTP host |
|
|
1305
|
+
| `SMTP_PORT` | No | `587` | SMTP port |
|
|
1306
|
+
| `IMAP_HOST` | No | `localhost` | IMAP host |
|
|
1307
|
+
| `IMAP_PORT` | No | `143` | IMAP port |
|
|
1308
|
+
| `AGENTICMAIL_INBOUND_SECRET` | No | `inbound_2sabi_secret_key` | Inbound webhook secret |
|
|
1309
|
+
|
|
1310
|
+
---
|
|
1311
|
+
|
|
1312
|
+
## License
|
|
1313
|
+
|
|
1314
|
+
[MIT](./LICENSE) - Ope Olatunji ([@ope-olatunji](https://github.com/ope-olatunji))
|