@swarmclawai/swarmclaw 0.7.2 → 0.7.4

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.
Files changed (274) hide show
  1. package/README.md +116 -50
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +43 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +39 -8
  12. package/src/app/api/agents/route.ts +35 -2
  13. package/src/app/api/auth/route.ts +77 -8
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  19. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  20. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  21. package/src/app/api/chats/[id]/route.ts +30 -0
  22. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  23. package/src/app/api/chats/heartbeat/route.ts +2 -1
  24. package/src/app/api/chats/route.ts +23 -1
  25. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  26. package/src/app/api/connectors/doctor/route.ts +13 -0
  27. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  28. package/src/app/api/external-agents/[id]/route.ts +31 -0
  29. package/src/app/api/external-agents/register/route.ts +3 -0
  30. package/src/app/api/external-agents/route.ts +66 -0
  31. package/src/app/api/files/open/route.ts +16 -14
  32. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  33. package/src/app/api/gateways/[id]/route.ts +79 -0
  34. package/src/app/api/gateways/route.ts +57 -0
  35. package/src/app/api/memory/maintenance/route.ts +11 -1
  36. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  37. package/src/app/api/openclaw/gateway/route.ts +10 -7
  38. package/src/app/api/openclaw/skills/route.ts +12 -4
  39. package/src/app/api/plugins/dependencies/route.ts +24 -0
  40. package/src/app/api/plugins/install/route.ts +15 -92
  41. package/src/app/api/plugins/route.ts +3 -26
  42. package/src/app/api/plugins/settings/route.ts +17 -12
  43. package/src/app/api/plugins/ui/route.ts +1 -0
  44. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  45. package/src/app/api/schedules/[id]/route.ts +38 -9
  46. package/src/app/api/schedules/route.ts +51 -28
  47. package/src/app/api/settings/route.ts +55 -17
  48. package/src/app/api/setup/doctor/route.ts +6 -4
  49. package/src/app/api/tasks/[id]/route.ts +16 -6
  50. package/src/app/api/tasks/bulk/route.ts +3 -3
  51. package/src/app/api/tasks/route.ts +9 -4
  52. package/src/app/api/webhooks/[id]/route.ts +8 -1
  53. package/src/app/page.tsx +135 -17
  54. package/src/cli/binary.test.js +142 -0
  55. package/src/cli/index.js +38 -11
  56. package/src/cli/index.test.js +195 -0
  57. package/src/cli/index.ts +21 -12
  58. package/src/cli/server-cmd.test.js +59 -0
  59. package/src/cli/spec.js +20 -2
  60. package/src/components/agents/agent-card.tsx +15 -12
  61. package/src/components/agents/agent-chat-list.tsx +101 -1
  62. package/src/components/agents/agent-list.tsx +46 -9
  63. package/src/components/agents/agent-sheet.tsx +456 -23
  64. package/src/components/agents/inspector-panel.tsx +110 -49
  65. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  66. package/src/components/auth/access-key-gate.tsx +36 -97
  67. package/src/components/auth/setup-wizard.tsx +970 -275
  68. package/src/components/chat/chat-area.tsx +70 -27
  69. package/src/components/chat/chat-card.tsx +6 -21
  70. package/src/components/chat/chat-header.tsx +263 -366
  71. package/src/components/chat/chat-list.tsx +62 -26
  72. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  73. package/src/components/chat/message-list.tsx +145 -19
  74. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  75. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  76. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  77. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  78. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  79. package/src/components/chatrooms/chatroom-view.tsx +422 -209
  80. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  81. package/src/components/connectors/connector-list.tsx +265 -127
  82. package/src/components/connectors/connector-sheet.tsx +217 -0
  83. package/src/components/gateways/gateway-sheet.tsx +567 -0
  84. package/src/components/home/home-view.tsx +128 -4
  85. package/src/components/input/chat-input.tsx +135 -86
  86. package/src/components/layout/app-layout.tsx +385 -194
  87. package/src/components/layout/mobile-header.tsx +26 -8
  88. package/src/components/memory/memory-browser.tsx +71 -6
  89. package/src/components/memory/memory-card.tsx +18 -0
  90. package/src/components/memory/memory-detail.tsx +58 -31
  91. package/src/components/memory/memory-sheet.tsx +32 -4
  92. package/src/components/plugins/plugin-list.tsx +15 -3
  93. package/src/components/plugins/plugin-sheet.tsx +118 -9
  94. package/src/components/projects/project-detail.tsx +189 -1
  95. package/src/components/providers/provider-list.tsx +158 -2
  96. package/src/components/providers/provider-sheet.tsx +81 -70
  97. package/src/components/shared/agent-picker-list.tsx +2 -2
  98. package/src/components/shared/bottom-sheet.tsx +31 -15
  99. package/src/components/shared/command-palette.tsx +111 -24
  100. package/src/components/shared/confirm-dialog.tsx +45 -30
  101. package/src/components/shared/model-combobox.tsx +90 -8
  102. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  103. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  104. package/src/components/shared/settings/section-heartbeat.tsx +88 -6
  105. package/src/components/shared/settings/section-orchestrator.tsx +6 -3
  106. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  107. package/src/components/shared/settings/section-secrets.tsx +6 -6
  108. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  109. package/src/components/shared/settings/section-voice.tsx +5 -1
  110. package/src/components/shared/settings/section-web-search.tsx +10 -2
  111. package/src/components/shared/settings/settings-page.tsx +248 -47
  112. package/src/components/tasks/approvals-panel.tsx +211 -18
  113. package/src/components/tasks/task-board.tsx +242 -46
  114. package/src/components/ui/dialog.tsx +2 -2
  115. package/src/components/usage/metrics-dashboard.tsx +74 -1
  116. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  117. package/src/components/wallets/wallet-panel.tsx +17 -5
  118. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  119. package/src/lib/auth.ts +17 -0
  120. package/src/lib/chat-streaming-state.test.ts +108 -0
  121. package/src/lib/chat-streaming-state.ts +108 -0
  122. package/src/lib/heartbeat-defaults.ts +48 -0
  123. package/src/lib/memory-presentation.ts +59 -0
  124. package/src/lib/openclaw-agent-id.test.ts +14 -0
  125. package/src/lib/openclaw-agent-id.ts +31 -0
  126. package/src/lib/provider-model-discovery-client.ts +29 -0
  127. package/src/lib/providers/index.ts +12 -5
  128. package/src/lib/runtime-loop.ts +105 -3
  129. package/src/lib/safe-storage.ts +6 -1
  130. package/src/lib/server/agent-assignment.test.ts +112 -0
  131. package/src/lib/server/agent-assignment.ts +169 -0
  132. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  133. package/src/lib/server/agent-runtime-config.ts +277 -0
  134. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  135. package/src/lib/server/approvals-auto-approve.test.ts +264 -0
  136. package/src/lib/server/approvals.ts +483 -75
  137. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  138. package/src/lib/server/browser-state.test.ts +118 -0
  139. package/src/lib/server/browser-state.ts +123 -0
  140. package/src/lib/server/build-llm.test.ts +44 -0
  141. package/src/lib/server/build-llm.ts +11 -4
  142. package/src/lib/server/builtin-plugins.ts +34 -0
  143. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  144. package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
  145. package/src/lib/server/chat-execution.ts +402 -125
  146. package/src/lib/server/chatroom-health.test.ts +26 -0
  147. package/src/lib/server/chatroom-health.ts +2 -3
  148. package/src/lib/server/chatroom-helpers.test.ts +74 -2
  149. package/src/lib/server/chatroom-helpers.ts +144 -11
  150. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  151. package/src/lib/server/connectors/discord.ts +175 -11
  152. package/src/lib/server/connectors/doctor.test.ts +80 -0
  153. package/src/lib/server/connectors/doctor.ts +116 -0
  154. package/src/lib/server/connectors/manager.ts +994 -130
  155. package/src/lib/server/connectors/policy.test.ts +222 -0
  156. package/src/lib/server/connectors/policy.ts +452 -0
  157. package/src/lib/server/connectors/slack.ts +189 -10
  158. package/src/lib/server/connectors/telegram.ts +65 -15
  159. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  160. package/src/lib/server/connectors/thread-context.ts +72 -0
  161. package/src/lib/server/connectors/types.ts +41 -11
  162. package/src/lib/server/daemon-state.ts +62 -3
  163. package/src/lib/server/data-dir.ts +13 -0
  164. package/src/lib/server/delegation-jobs.test.ts +140 -0
  165. package/src/lib/server/delegation-jobs.ts +248 -0
  166. package/src/lib/server/document-utils.test.ts +47 -0
  167. package/src/lib/server/document-utils.ts +397 -0
  168. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  169. package/src/lib/server/eval/agent-regression.ts +1742 -0
  170. package/src/lib/server/eval/runner.ts +11 -1
  171. package/src/lib/server/eval/store.ts +2 -1
  172. package/src/lib/server/heartbeat-service.ts +23 -43
  173. package/src/lib/server/heartbeat-source.test.ts +22 -0
  174. package/src/lib/server/heartbeat-source.ts +7 -0
  175. package/src/lib/server/identity-continuity.test.ts +77 -0
  176. package/src/lib/server/identity-continuity.ts +127 -0
  177. package/src/lib/server/mailbox-utils.ts +347 -0
  178. package/src/lib/server/main-agent-loop.ts +31 -964
  179. package/src/lib/server/memory-db.ts +4 -6
  180. package/src/lib/server/memory-tiers.ts +40 -0
  181. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  182. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  183. package/src/lib/server/openclaw-exec-config.ts +6 -5
  184. package/src/lib/server/openclaw-gateway.ts +123 -36
  185. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  186. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  187. package/src/lib/server/openclaw-sync.ts +3 -2
  188. package/src/lib/server/orchestrator-lg.ts +18 -8
  189. package/src/lib/server/orchestrator.ts +5 -4
  190. package/src/lib/server/playwright-proxy.mjs +27 -3
  191. package/src/lib/server/plugins.test.ts +215 -0
  192. package/src/lib/server/plugins.ts +832 -69
  193. package/src/lib/server/provider-health.ts +33 -3
  194. package/src/lib/server/provider-model-discovery.ts +481 -0
  195. package/src/lib/server/queue.ts +4 -21
  196. package/src/lib/server/runtime-settings.test.ts +119 -0
  197. package/src/lib/server/runtime-settings.ts +12 -92
  198. package/src/lib/server/schedule-normalization.ts +187 -0
  199. package/src/lib/server/scheduler.ts +2 -0
  200. package/src/lib/server/session-archive-memory.test.ts +85 -0
  201. package/src/lib/server/session-archive-memory.ts +230 -0
  202. package/src/lib/server/session-mailbox.ts +8 -18
  203. package/src/lib/server/session-reset-policy.test.ts +99 -0
  204. package/src/lib/server/session-reset-policy.ts +311 -0
  205. package/src/lib/server/session-run-manager.ts +33 -80
  206. package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
  207. package/src/lib/server/session-tools/calendar.ts +2 -12
  208. package/src/lib/server/session-tools/connector.ts +109 -8
  209. package/src/lib/server/session-tools/context.ts +14 -2
  210. package/src/lib/server/session-tools/crawl.ts +447 -0
  211. package/src/lib/server/session-tools/crud.ts +96 -34
  212. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  213. package/src/lib/server/session-tools/delegate.ts +406 -20
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  215. package/src/lib/server/session-tools/discovery.ts +40 -12
  216. package/src/lib/server/session-tools/document.ts +283 -0
  217. package/src/lib/server/session-tools/email.ts +1 -3
  218. package/src/lib/server/session-tools/extract.ts +137 -0
  219. package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
  220. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  221. package/src/lib/server/session-tools/file.ts +243 -24
  222. package/src/lib/server/session-tools/http.ts +9 -3
  223. package/src/lib/server/session-tools/human-loop.ts +227 -0
  224. package/src/lib/server/session-tools/image-gen.ts +1 -3
  225. package/src/lib/server/session-tools/index.ts +87 -2
  226. package/src/lib/server/session-tools/mailbox.ts +276 -0
  227. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  228. package/src/lib/server/session-tools/memory.ts +35 -3
  229. package/src/lib/server/session-tools/monitor.ts +162 -12
  230. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  231. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  232. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  233. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  234. package/src/lib/server/session-tools/platform.ts +142 -4
  235. package/src/lib/server/session-tools/plugin-creator.ts +95 -25
  236. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  237. package/src/lib/server/session-tools/replicate.ts +1 -3
  238. package/src/lib/server/session-tools/sandbox.ts +51 -92
  239. package/src/lib/server/session-tools/schedule.ts +20 -10
  240. package/src/lib/server/session-tools/session-info.ts +58 -4
  241. package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
  242. package/src/lib/server/session-tools/shell.ts +2 -2
  243. package/src/lib/server/session-tools/subagent.ts +195 -27
  244. package/src/lib/server/session-tools/table.ts +587 -0
  245. package/src/lib/server/session-tools/wallet.ts +13 -10
  246. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  247. package/src/lib/server/session-tools/web.ts +947 -108
  248. package/src/lib/server/storage.ts +255 -10
  249. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  250. package/src/lib/server/stream-agent-chat.ts +185 -25
  251. package/src/lib/server/structured-extract.test.ts +72 -0
  252. package/src/lib/server/structured-extract.ts +373 -0
  253. package/src/lib/server/task-mention.test.ts +16 -2
  254. package/src/lib/server/task-mention.ts +61 -11
  255. package/src/lib/server/tool-aliases.ts +80 -12
  256. package/src/lib/server/tool-capability-policy.ts +7 -1
  257. package/src/lib/server/tool-retry.ts +2 -0
  258. package/src/lib/server/watch-jobs.test.ts +173 -0
  259. package/src/lib/server/watch-jobs.ts +532 -0
  260. package/src/lib/server/ws-hub.ts +5 -3
  261. package/src/lib/setup-defaults.ts +352 -11
  262. package/src/lib/tool-definitions.ts +3 -4
  263. package/src/lib/validation/schemas.test.ts +26 -0
  264. package/src/lib/validation/schemas.ts +62 -1
  265. package/src/lib/ws-client.ts +14 -12
  266. package/src/proxy.ts +5 -5
  267. package/src/stores/use-app-store.ts +43 -7
  268. package/src/stores/use-chat-store.ts +31 -2
  269. package/src/stores/use-chatroom-store.ts +153 -26
  270. package/src/types/index.ts +470 -44
  271. package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
  272. package/src/components/chat/new-chat-sheet.tsx +0 -253
  273. package/src/lib/server/main-session.ts +0 -17
  274. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -0,0 +1,347 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { ImapFlow } from 'imapflow'
4
+ import { createTransport } from 'nodemailer'
5
+ import { simpleParser } from 'mailparser'
6
+ import { UPLOAD_DIR, loadConnectors } from './storage'
7
+ import { getPluginManager } from './plugins'
8
+
9
+ export interface MailboxConfig {
10
+ imapHost: string
11
+ imapPort: number
12
+ smtpHost: string
13
+ smtpPort: number
14
+ user: string
15
+ password: string
16
+ smtpUsername: string
17
+ smtpPassword: string
18
+ folder: string
19
+ subjectPrefix?: string
20
+ fromAddress: string
21
+ fromName: string
22
+ }
23
+
24
+ export interface MailboxAttachment {
25
+ id: string
26
+ filename: string
27
+ contentType: string | null
28
+ sizeBytes: number
29
+ }
30
+
31
+ export interface MailboxMessage {
32
+ id: string
33
+ uid: number
34
+ messageId: string | null
35
+ subject: string
36
+ from: string
37
+ fromName: string
38
+ date: string | null
39
+ snippet: string
40
+ text: string
41
+ html: string | null
42
+ threadKey: string
43
+ references: string[]
44
+ hasAttachments: boolean
45
+ attachments: MailboxAttachment[]
46
+ flags: string[]
47
+ }
48
+
49
+ function pickString(...values: unknown[]): string {
50
+ for (const value of values) {
51
+ if (typeof value !== 'string') continue
52
+ const trimmed = value.trim()
53
+ if (trimmed) return trimmed
54
+ }
55
+ return ''
56
+ }
57
+
58
+ function pickNumber(fallback: number, ...values: unknown[]): number {
59
+ for (const value of values) {
60
+ const parsed = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : Number.NaN
61
+ if (Number.isFinite(parsed) && parsed > 0) return Math.trunc(parsed)
62
+ }
63
+ return fallback
64
+ }
65
+
66
+ function normalizeThreadKey(subject: string, references: string[]): string {
67
+ if (references.length > 0) return references[references.length - 1]
68
+ return subject.replace(/^re:\s*/i, '').trim().toLowerCase()
69
+ }
70
+
71
+ function sanitizeAttachmentName(value: string | undefined, fallback: string): string {
72
+ const cleaned = String(value || '').replace(/[^a-zA-Z0-9._-]/g, '_').replace(/_+/g, '_').replace(/^_+|_+$/g, '')
73
+ return cleaned || fallback
74
+ }
75
+
76
+ export function getMailboxConfig(): MailboxConfig {
77
+ const pluginManager = getPluginManager()
78
+ const mailboxSettings = pluginManager.getPluginSettings('mailbox') as Record<string, unknown>
79
+ const emailSettings = pluginManager.getPluginSettings('email') as Record<string, unknown>
80
+ const connectors = loadConnectors()
81
+ const emailConnector = Object.values(connectors)
82
+ .find((entry) => entry && typeof entry === 'object' && String((entry as Record<string, unknown>).platform || '').toLowerCase() === 'email') as Record<string, unknown> | undefined
83
+ const connectorConfig = emailConnector && typeof emailConnector.config === 'object' && emailConnector.config
84
+ ? emailConnector.config as Record<string, unknown>
85
+ : {}
86
+
87
+ const user = pickString(mailboxSettings.user, connectorConfig.user)
88
+ const password = pickString(mailboxSettings.password, connectorConfig.password)
89
+
90
+ return {
91
+ imapHost: pickString(mailboxSettings.imapHost, connectorConfig.imapHost),
92
+ imapPort: pickNumber(993, mailboxSettings.imapPort, connectorConfig.imapPort),
93
+ smtpHost: pickString(mailboxSettings.smtpHost, emailSettings.host, connectorConfig.smtpHost),
94
+ smtpPort: pickNumber(587, mailboxSettings.smtpPort, emailSettings.port, connectorConfig.smtpPort),
95
+ user,
96
+ password,
97
+ smtpUsername: pickString(mailboxSettings.smtpUsername, emailSettings.username, connectorConfig.user, user),
98
+ smtpPassword: pickString(mailboxSettings.smtpPassword, emailSettings.password, connectorConfig.password, password),
99
+ folder: pickString(mailboxSettings.folder, connectorConfig.folder, 'INBOX') || 'INBOX',
100
+ subjectPrefix: pickString(mailboxSettings.subjectPrefix, connectorConfig.subjectPrefix) || undefined,
101
+ fromAddress: pickString(mailboxSettings.fromAddress, emailSettings.fromAddress, connectorConfig.user, user),
102
+ fromName: pickString(mailboxSettings.fromName, emailSettings.fromName, 'SwarmClaw Agent'),
103
+ }
104
+ }
105
+
106
+ function ensureMailboxConfigured(config: MailboxConfig): void {
107
+ if (!config.imapHost || !config.user || !config.password) {
108
+ throw new Error('Mailbox plugin requires IMAP host, user, and password.')
109
+ }
110
+ }
111
+
112
+ async function withImapClient<T>(config: MailboxConfig, fn: (client: ImapFlow) => Promise<T>): Promise<T> {
113
+ ensureMailboxConfigured(config)
114
+ const client = new ImapFlow({
115
+ host: config.imapHost,
116
+ port: config.imapPort,
117
+ secure: config.imapPort === 993,
118
+ auth: {
119
+ user: config.user,
120
+ pass: config.password,
121
+ },
122
+ logger: false,
123
+ })
124
+ await client.connect()
125
+ try {
126
+ return await fn(client)
127
+ } finally {
128
+ try { await client.logout() } catch { /* ignore */ }
129
+ }
130
+ }
131
+
132
+ function messageMatchesFilters(message: MailboxMessage, filters: {
133
+ query?: string
134
+ from?: string
135
+ subjectContains?: string
136
+ bodyContains?: string
137
+ unreadOnly?: boolean
138
+ hasAttachments?: boolean
139
+ uidGreaterThan?: number
140
+ }) {
141
+ if (typeof filters.uidGreaterThan === 'number' && message.uid <= filters.uidGreaterThan) return false
142
+ if (filters.unreadOnly === true && message.flags.includes('\\Seen')) return false
143
+ if (filters.hasAttachments === true && !message.hasAttachments) return false
144
+ const from = filters.from?.trim().toLowerCase()
145
+ if (from && !message.from.toLowerCase().includes(from) && !message.fromName.toLowerCase().includes(from)) return false
146
+ const subjectContains = filters.subjectContains?.trim().toLowerCase()
147
+ if (subjectContains && !message.subject.toLowerCase().includes(subjectContains)) return false
148
+ const bodyContains = filters.bodyContains?.trim().toLowerCase()
149
+ if (bodyContains && !message.text.toLowerCase().includes(bodyContains)) return false
150
+ const query = filters.query?.trim().toLowerCase()
151
+ if (query) {
152
+ const hay = `${message.subject}\n${message.from}\n${message.fromName}\n${message.text}`.toLowerCase()
153
+ if (!hay.includes(query)) return false
154
+ }
155
+ return true
156
+ }
157
+
158
+ function toMailboxMessage(raw: {
159
+ uid: number
160
+ envelope?: {
161
+ from?: Array<{ name?: string; address?: string }>
162
+ subject?: string
163
+ messageId?: string
164
+ date?: Date
165
+ inReplyTo?: string
166
+ references?: string[]
167
+ }
168
+ flags?: Set<string>
169
+ source?: Buffer
170
+ }, parsed: Awaited<ReturnType<typeof simpleParser>>): MailboxMessage {
171
+ const fromAddress = raw.envelope?.from?.[0]?.address || parsed.from?.value?.[0]?.address || 'unknown'
172
+ const fromName = raw.envelope?.from?.[0]?.name || parsed.from?.value?.[0]?.name || fromAddress
173
+ const references = [
174
+ ...(Array.isArray(raw.envelope?.references) ? raw.envelope?.references : []),
175
+ ...(parsed.references ? (Array.isArray(parsed.references) ? parsed.references : [parsed.references]) : []),
176
+ ].filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
177
+
178
+ return {
179
+ id: String(raw.uid),
180
+ uid: raw.uid,
181
+ messageId: raw.envelope?.messageId || parsed.messageId || null,
182
+ subject: raw.envelope?.subject || parsed.subject || '(no subject)',
183
+ from: fromAddress,
184
+ fromName,
185
+ date: raw.envelope?.date ? raw.envelope.date.toISOString() : (parsed.date ? parsed.date.toISOString() : null),
186
+ snippet: (parsed.text || parsed.html || '').replace(/\s+/g, ' ').trim().slice(0, 240),
187
+ text: (parsed.text || '').trim(),
188
+ html: typeof parsed.html === 'string' ? parsed.html : null,
189
+ threadKey: normalizeThreadKey(raw.envelope?.subject || parsed.subject || '', references),
190
+ references,
191
+ hasAttachments: parsed.attachments.length > 0,
192
+ attachments: parsed.attachments.map((attachment, index) => ({
193
+ id: `${raw.uid}:${index}`,
194
+ filename: sanitizeAttachmentName(attachment.filename || undefined, `attachment-${index + 1}`),
195
+ contentType: attachment.contentType || null,
196
+ sizeBytes: attachment.size || 0,
197
+ })),
198
+ flags: Array.from(raw.flags || new Set<string>()),
199
+ }
200
+ }
201
+
202
+ export async function getMailboxHighwaterUid(config = getMailboxConfig(), folder?: string): Promise<number> {
203
+ return withImapClient(config, async (client) => {
204
+ const targetFolder = folder || config.folder || 'INBOX'
205
+ const lock = await client.getMailboxLock(targetFolder)
206
+ try {
207
+ const status = await client.status(targetFolder, { uidNext: true })
208
+ return typeof status.uidNext === 'number' ? Math.max(0, status.uidNext - 1) : 0
209
+ } finally {
210
+ lock.release()
211
+ }
212
+ })
213
+ }
214
+
215
+ export async function fetchMailboxMessages(filters?: {
216
+ folder?: string
217
+ query?: string
218
+ from?: string
219
+ subjectContains?: string
220
+ bodyContains?: string
221
+ unreadOnly?: boolean
222
+ hasAttachments?: boolean
223
+ uidGreaterThan?: number
224
+ limit?: number
225
+ }): Promise<MailboxMessage[]> {
226
+ const config = getMailboxConfig()
227
+ return withImapClient(config, async (client) => {
228
+ const folder = filters?.folder || config.folder || 'INBOX'
229
+ const limit = Math.max(1, Math.min(filters?.limit || 20, 100))
230
+ const lock = await client.getMailboxLock(folder)
231
+ try {
232
+ const status = await client.status(folder, { uidNext: true })
233
+ const endUid = typeof status.uidNext === 'number' ? Math.max(0, status.uidNext - 1) : 0
234
+ if (endUid <= 0) return []
235
+ const startUid = Math.max(1, endUid - Math.max(limit * 4, 60) + 1)
236
+ const messages: MailboxMessage[] = []
237
+ for await (const raw of client.fetch(`${startUid}:${endUid}`, {
238
+ uid: true,
239
+ envelope: true,
240
+ flags: true,
241
+ source: true,
242
+ }, { uid: true })) {
243
+ if (!raw.source) continue
244
+ const parsed = await simpleParser(raw.source)
245
+ const message = toMailboxMessage(raw, parsed)
246
+ if (!messageMatchesFilters(message, filters || {})) continue
247
+ if (config.subjectPrefix && !message.subject.startsWith(config.subjectPrefix)) continue
248
+ messages.push(message)
249
+ }
250
+ return messages.sort((a, b) => b.uid - a.uid).slice(0, limit)
251
+ } finally {
252
+ lock.release()
253
+ }
254
+ })
255
+ }
256
+
257
+ export async function fetchMailboxMessageByUid(uid: number, folder?: string): Promise<MailboxMessage | null> {
258
+ const messages = await fetchMailboxMessages({ folder, uidGreaterThan: uid - 1, limit: 100 })
259
+ return messages.find((message) => message.uid === uid) || null
260
+ }
261
+
262
+ export async function downloadMailboxAttachment(params: {
263
+ uid: number
264
+ attachmentId?: string
265
+ attachmentName?: string
266
+ folder?: string
267
+ saveTo?: string
268
+ cwd?: string
269
+ }): Promise<{ filePath: string; fileName: string; url: string | null }> {
270
+ const config = getMailboxConfig()
271
+ return withImapClient(config, async (client) => {
272
+ const folder = params.folder || config.folder || 'INBOX'
273
+ const lock = await client.getMailboxLock(folder)
274
+ try {
275
+ for await (const raw of client.fetch(String(params.uid), { uid: true, source: true }, { uid: true })) {
276
+ if (!raw.source) continue
277
+ const parsed = await simpleParser(raw.source)
278
+ const selected = parsed.attachments.find((attachment, index) => {
279
+ const generatedId = `${params.uid}:${index}`
280
+ if (params.attachmentId && generatedId === params.attachmentId) return true
281
+ if (params.attachmentName && attachment.filename === params.attachmentName) return true
282
+ return !params.attachmentId && !params.attachmentName && index === 0
283
+ })
284
+ if (!selected) throw new Error('Attachment not found.')
285
+
286
+ const fileName = sanitizeAttachmentName(selected.filename || undefined, `attachment-${params.uid}`)
287
+ const targetPath = params.saveTo
288
+ ? path.resolve(params.cwd || process.cwd(), params.saveTo)
289
+ : path.join(UPLOAD_DIR, `${Date.now()}-${fileName}`)
290
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true })
291
+ fs.writeFileSync(targetPath, selected.content)
292
+
293
+ const publicPath = targetPath.startsWith(UPLOAD_DIR)
294
+ ? targetPath
295
+ : path.join(UPLOAD_DIR, `${Date.now()}-${path.basename(targetPath)}`)
296
+ if (publicPath !== targetPath) fs.copyFileSync(targetPath, publicPath)
297
+ return {
298
+ filePath: targetPath,
299
+ fileName,
300
+ url: `/api/uploads/${path.basename(publicPath)}`,
301
+ }
302
+ }
303
+ throw new Error(`Mailbox message not found: ${params.uid}`)
304
+ } finally {
305
+ lock.release()
306
+ }
307
+ })
308
+ }
309
+
310
+ export async function replyMailboxMessage(params: {
311
+ uid: number
312
+ text: string
313
+ html?: string
314
+ subject?: string
315
+ folder?: string
316
+ }): Promise<{ to: string; subject: string }> {
317
+ const config = getMailboxConfig()
318
+ if (!config.smtpHost || !config.fromAddress) {
319
+ throw new Error('Mailbox reply requires SMTP host and fromAddress configuration.')
320
+ }
321
+
322
+ const message = await fetchMailboxMessageByUid(params.uid, params.folder)
323
+ if (!message) throw new Error(`Mailbox message not found: ${params.uid}`)
324
+
325
+ const transport = createTransport({
326
+ host: config.smtpHost,
327
+ port: config.smtpPort,
328
+ secure: config.smtpPort === 465,
329
+ auth: {
330
+ user: config.smtpUsername || config.user,
331
+ pass: config.smtpPassword || config.password,
332
+ },
333
+ })
334
+
335
+ const subject = params.subject?.trim() || `Re: ${message.subject.replace(/^Re:\s*/i, '')}`
336
+ await transport.sendMail({
337
+ from: config.fromName ? `"${config.fromName}" <${config.fromAddress}>` : config.fromAddress,
338
+ to: message.from,
339
+ subject,
340
+ text: params.text,
341
+ html: params.html,
342
+ inReplyTo: message.messageId || undefined,
343
+ references: message.messageId || undefined,
344
+ })
345
+
346
+ return { to: message.from, subject }
347
+ }