@rubytech/create-maxy-code 0.1.163 → 0.1.165

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 (106) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/neo4j/edge-annotations.json +4 -0
  3. package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
  4. package/payload/platform/plugins/admin/PLUGIN.md +1 -6
  5. package/payload/platform/plugins/admin/hooks/lib/admin-graph-pass-common.sh +12 -15
  6. package/payload/platform/plugins/admin/mcp/dist/index.js +0 -41
  7. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  8. package/payload/platform/plugins/docs/references/platform.md +1 -1
  9. package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
  10. package/payload/platform/plugins/email/PLUGIN.md +8 -12
  11. package/payload/platform/plugins/email/mcp/dist/index.js +8 -34
  12. package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
  13. package/payload/platform/plugins/email/mcp/dist/lib/attachment-archive.d.ts +39 -0
  14. package/payload/platform/plugins/email/mcp/dist/lib/attachment-archive.d.ts.map +1 -0
  15. package/payload/platform/plugins/email/mcp/dist/lib/attachment-archive.js +0 -0
  16. package/payload/platform/plugins/email/mcp/dist/lib/attachment-archive.js.map +1 -0
  17. package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.d.ts +23 -1
  18. package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.d.ts.map +1 -1
  19. package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.js +131 -3
  20. package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.js.map +1 -1
  21. package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-lookup.d.ts +19 -0
  22. package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-lookup.d.ts.map +1 -0
  23. package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-lookup.js +51 -0
  24. package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-lookup.js.map +1 -0
  25. package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts +0 -18
  26. package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts.map +1 -1
  27. package/payload/platform/plugins/email/mcp/dist/lib/graph.js +0 -12
  28. package/payload/platform/plugins/email/mcp/dist/lib/graph.js.map +1 -1
  29. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +91 -40
  30. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
  31. package/payload/platform/plugins/email/mcp/dist/lib/imap.js +204 -169
  32. package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
  33. package/payload/platform/plugins/email/mcp/dist/tools/email-fetch.d.ts.map +1 -1
  34. package/payload/platform/plugins/email/mcp/dist/tools/email-fetch.js +6 -1
  35. package/payload/platform/plugins/email/mcp/dist/tools/email-fetch.js.map +1 -1
  36. package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.d.ts +7 -15
  37. package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.d.ts.map +1 -1
  38. package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.js +206 -86
  39. package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.js.map +1 -1
  40. package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.d.ts.map +1 -1
  41. package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js +104 -4
  42. package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js.map +1 -1
  43. package/payload/platform/plugins/email/mcp/dist/tools/email-read.d.ts.map +1 -1
  44. package/payload/platform/plugins/email/mcp/dist/tools/email-read.js +22 -13
  45. package/payload/platform/plugins/email/mcp/dist/tools/email-read.js.map +1 -1
  46. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.d.ts.map +1 -1
  47. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js +5 -4
  48. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js.map +1 -1
  49. package/payload/platform/plugins/email/mcp/dist/tools/email-search.d.ts.map +1 -1
  50. package/payload/platform/plugins/email/mcp/dist/tools/email-search.js +17 -13
  51. package/payload/platform/plugins/email/mcp/dist/tools/email-search.js.map +1 -1
  52. package/payload/platform/plugins/email/references/email-reference.md +8 -8
  53. package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
  54. package/payload/platform/plugins/memory/PLUGIN.md +2 -2
  55. package/payload/platform/plugins/memory/mcp/dist/index.js +4 -3
  56. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  57. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-origin-gate.test.js +1 -13
  58. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-origin-gate.test.js.map +1 -1
  59. package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.d.ts.map +1 -1
  60. package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.js +3 -5
  61. package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.js.map +1 -1
  62. package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.d.ts +1 -1
  63. package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.d.ts.map +1 -1
  64. package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.js +10 -4
  65. package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.js.map +1 -1
  66. package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.d.ts.map +1 -1
  67. package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.js +5 -6
  68. package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.js.map +1 -1
  69. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +21 -4
  70. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
  71. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
  72. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +6 -8
  73. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
  74. package/payload/platform/plugins/memory/references/schema-base.md +3 -2
  75. package/payload/platform/plugins/memory/references/schema-estate-agent.md +1 -1
  76. package/payload/platform/plugins/memory/references/transcript-formats/circleback.md +49 -0
  77. package/payload/platform/plugins/memory/references/transcript-formats/granola.md +50 -0
  78. package/payload/platform/plugins/memory/references/transcript-formats/otter.md +50 -0
  79. package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +1 -0
  80. package/payload/platform/plugins/memory/skills/conversation-archive-mcp/SKILL.md +170 -0
  81. package/payload/platform/scripts/seed-neo4j.sh +1 -3
  82. package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts +9 -1
  83. package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts.map +1 -1
  84. package/payload/platform/services/claude-session-manager/dist/fs-watcher.js +74 -38
  85. package/payload/platform/services/claude-session-manager/dist/fs-watcher.js.map +1 -1
  86. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  87. package/payload/platform/services/claude-session-manager/dist/http-server.js +16 -0
  88. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  89. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +16 -1
  90. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  91. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +21 -7
  92. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  93. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts +7 -0
  94. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts.map +1 -1
  95. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js +10 -0
  96. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js.map +1 -1
  97. package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts +52 -10
  98. package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts.map +1 -1
  99. package/payload/platform/services/claude-session-manager/dist/systemd-scope.js +73 -13
  100. package/payload/platform/services/claude-session-manager/dist/systemd-scope.js.map +1 -1
  101. package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
  102. package/payload/platform/templates/specialists/agents/librarian.md +1 -0
  103. package/payload/platform/plugins/admin/hooks/__tests__/per-turn-graph-pass-gate.test.sh +0 -387
  104. package/payload/platform/plugins/admin/hooks/__tests__/pre-turn-graph-pass.test.sh +0 -441
  105. package/payload/platform/plugins/admin/hooks/per-turn-graph-pass-gate.sh +0 -111
  106. package/payload/platform/plugins/admin/hooks/pre-turn-graph-pass.sh +0 -171
@@ -13,18 +13,6 @@ export function stripAngleBrackets(value) {
13
13
  }
14
14
  return trimmed;
15
15
  }
16
- export async function getMessageEnvelope(_messageId, _accountId) {
17
- // Task 442 — sourcing per-message envelopes from IMAP, not the graph.
18
- return null;
19
- }
20
- export async function getThreadInfo(_messageIds, _accountId) {
21
- // Task 442 — per-message thread-depth lookup requires per-message nodes
22
- // the new `:ConversationArchive` shape does not store.
23
- return new Map();
24
- }
25
- // Re-export neo4j so consumers depending on the legacy module surface keep
26
- // working without churn. The actual neo4j-driver usage is now in
27
- // conversation-archive-dispatch.ts.
28
16
  export { neo4j };
29
17
  export { getSession };
30
18
  //# sourceMappingURL=graph.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"graph.js","sourceRoot":"","sources":["../../src/lib/graph.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,MAAM,cAAc,CAAC;AAmCjC;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAoB;IACrD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACrD,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAsBD,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,UAAkB,EAClB,UAAkB;IAElB,sEAAsE;IACtE,OAAO,IAAI,CAAC;AACd,CAAC;AAQD,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,WAAqB,EACrB,UAAkB;IAElB,wEAAwE;IACxE,uDAAuD;IACvD,OAAO,IAAI,GAAG,EAAE,CAAC;AACnB,CAAC;AAED,2EAA2E;AAC3E,iEAAiE;AACjE,oCAAoC;AACpC,OAAO,EAAE,KAAK,EAAE,CAAC;AACjB,OAAO,EAAE,UAAU,EAAE,CAAC"}
1
+ {"version":3,"file":"graph.js","sourceRoot":"","sources":["../../src/lib/graph.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,MAAM,cAAc,CAAC;AAuCjC;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAoB;IACrD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACrD,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,OAAO,EAAE,KAAK,EAAE,CAAC;AACjB,OAAO,EAAE,UAAU,EAAE,CAAC"}
@@ -8,6 +8,7 @@ export interface EmailMessage {
8
8
  subject: string;
9
9
  date: string;
10
10
  bodyPreview: string;
11
+ attachments: AttachmentMeta[];
11
12
  }
12
13
  export interface PaginatedMessages {
13
14
  messages: EmailMessage[];
@@ -28,6 +29,77 @@ export declare function resolveFolderName(client: ImapFlow, folder: "inbox" | "s
28
29
  * Used for extracting readable text from HTML-only email bodies.
29
30
  */
30
31
  export declare function stripHtml(html: string): string;
32
+ /** Hard cap on a single attachment's bytes — over this size, metadata is surfaced
33
+ * but bytes are not downloaded. Matches typical provider send limits. */
34
+ export declare const MAX_ATTACHMENT_BYTES: number;
35
+ /** Attachment metadata as derived from IMAP bodyStructure — no bytes. */
36
+ export interface AttachmentMeta {
37
+ /** IMAP body part key, e.g. "2" or "1.3". Used to fetch bytes later. */
38
+ partKey: string;
39
+ filename: string;
40
+ mimeType: string;
41
+ sizeBytes: number;
42
+ /** RFC 2045 transfer encoding — needed to decode bytes at fetch time. */
43
+ encoding: string | null;
44
+ }
45
+ /**
46
+ * Body-structure node shape (subset of ImapFlow's BodyStructure).
47
+ * Defined locally because ImapFlow does not export a public type for this.
48
+ */
49
+ interface BodyStructureNode {
50
+ part?: string;
51
+ type?: string;
52
+ encoding?: string;
53
+ size?: number;
54
+ disposition?: string;
55
+ dispositionParameters?: {
56
+ filename?: string;
57
+ };
58
+ parameters?: {
59
+ name?: string;
60
+ };
61
+ childNodes?: BodyStructureNode[];
62
+ }
63
+ /**
64
+ * Walk an IMAP bodyStructure and return metadata for every attachment-shaped
65
+ * part. No bytes are downloaded — cheap, runs at preview time.
66
+ *
67
+ * An attachment-shaped part is one of:
68
+ * - disposition === "attachment" (RFC 2183), OR
69
+ * - has a filename in parameters.name or dispositionParameters.filename AND
70
+ * is not a multipart container.
71
+ *
72
+ * Inline parts without a filename (e.g. the message body) are excluded.
73
+ */
74
+ export declare function walkAttachmentMetadata(structure: BodyStructureNode | undefined): AttachmentMeta[];
75
+ /**
76
+ * Format an AttachmentMeta[] as a single-line operator-readable footer.
77
+ * Returns empty string when the array is empty so callers can skip the
78
+ * line entirely.
79
+ */
80
+ export declare function formatAttachmentsFooter(attachments: AttachmentMeta[]): string;
81
+ /**
82
+ * Result of attempting to fetch one attachment's bytes from IMAP.
83
+ * `bytes === null` indicates a non-fatal failure (oversize, short buffer,
84
+ * fetch error) — the caller surfaces metadata-only graph attach instead.
85
+ */
86
+ export interface AttachmentBytesResult {
87
+ uid: number;
88
+ partKey: string;
89
+ bytes: Buffer | null;
90
+ failureReason: "oversize" | "short-buffer" | "fetch-error" | null;
91
+ }
92
+ /**
93
+ * Download decoded bytes for a list of (uid, partKey) attachments from IMAP.
94
+ * Opens one connection, reuses it across all requests. Per-attachment
95
+ * failures are isolated — one bad part does not abort the batch.
96
+ */
97
+ export declare function fetchAttachmentBytesForMessages(config: EmailConfig, password: string, requests: Array<{
98
+ uid: number;
99
+ partKey: string;
100
+ expectedSize: number;
101
+ encoding: string | null;
102
+ }>): Promise<AttachmentBytesResult[]>;
31
103
  /**
32
104
  * Decode a raw IMAP body part buffer according to its transfer encoding.
33
105
  * Returns a UTF-8 string. Falls back to raw UTF-8 for unknown encodings.
@@ -148,6 +220,8 @@ export interface FetchedEmail {
148
220
  subject: string;
149
221
  date: string;
150
222
  bodyPreview: string;
223
+ /** Attachment metadata derived from bodyStructure; bytes downloaded later. */
224
+ attachments: AttachmentMeta[];
151
225
  }
152
226
  /**
153
227
  * Fetch emails from INBOX since a given UID (exclusive) for incremental polling.
@@ -165,51 +239,28 @@ export declare function fetchSinceUid(config: EmailConfig, password: string, sin
165
239
  maxUid: number;
166
240
  }>;
167
241
  /**
168
- * Get the inbox message count.
242
+ * Resolve an envelope by Message-ID via IMAP. Searches INBOX first, then
243
+ * the \Sent special-use folder. Returns null when no folder yields a hit.
244
+ *
245
+ * Uses `header: { "message-id": ... }` IMAP SEARCH — sufficient on every
246
+ * server that honours RFC 3501 header search. The Message-ID is sent with
247
+ * angle brackets restored because some servers (notably Gmail) only match
248
+ * the raw header value, not the bare identifier.
169
249
  */
170
- export declare function getInboxCount(config: EmailConfig, password: string): Promise<number>;
171
- export interface AutoRespondMessage {
250
+ export declare function fetchEnvelopeByMessageId(config: EmailConfig, password: string, messageId: string): Promise<{
172
251
  uid: number;
252
+ mailbox: string;
173
253
  messageId: string;
174
- from: string;
175
- to: string[];
254
+ fromAddress: string;
255
+ fromName: string | null;
256
+ toAddresses: string[];
257
+ ccAddresses: string[];
176
258
  subject: string;
177
259
  date: string;
178
- inReplyTo: string | null;
179
- references: string | null;
180
- body: string;
181
- /** True if the message has auto-reply indicators and should be skipped. */
182
- isAutoReply: boolean;
183
- }
260
+ } | null>;
184
261
  /**
185
- * Parse raw IMAP header text into a map of lowercased header names → values.
186
- * Handles multi-line continuation (lines starting with whitespace).
187
- */
188
- export declare function parseRawHeaders(raw: string): Map<string, string[]>;
189
- /**
190
- * Detect whether a message is an auto-reply based on RFC 3834 headers.
191
- *
192
- * Checks:
193
- * - Auto-Submitted: any value other than "no"
194
- * - X-Auto-Response-Suppress: present
195
- * - Precedence: bulk, junk, or list
196
- * - List-Unsubscribe or List-Id: present (mailing list)
197
- */
198
- export declare function detectAutoReply(headers: Map<string, string[]>): boolean;
199
- /**
200
- * Fetch new messages for auto-respond processing.
201
- *
202
- * Returns messages with UIDs greater than `sinceUid`, filtered by the
203
- * agent's address (TO header). Includes envelope, body, and auto-reply
204
- * detection headers in a single IMAP FETCH.
205
- *
206
- * Unlike fetchRecent (optimised for tool output with pagination),
207
- * this function fetches everything needed for auto-respond in one pass:
208
- * envelope for threading, body for agent input, headers for loop guards.
262
+ * Get the inbox message count.
209
263
  */
210
- export declare function fetchForAutoRespond(config: EmailConfig, password: string, options: {
211
- sinceUid: number;
212
- agentAddress: string;
213
- limit?: number;
214
- }): Promise<AutoRespondMessage[]>;
264
+ export declare function getInboxCount(config: EmailConfig, password: string): Promise<number>;
265
+ export {};
215
266
  //# sourceMappingURL=imap.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"imap.d.ts","sourceRoot":"","sources":["../../src/lib/imap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAA2B,MAAM,UAAU,CAAC;AAC7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAuBpD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,qGAAqG;IACrG,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAGD,eAAO,MAAM,SAAS,KAAK,CAAC;AAE5B;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,QAAQ,EAChB,MAAM,EAAE,OAAO,GAAG,MAAM,GACvB,OAAO,CAAC,MAAM,CAAC,CAYjB;AAqBD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAa9C;AAQD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAgBrE;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE;IACxC,SAAS,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,aAAa,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC;CAChH,GAAG,MAAM,CAqCT;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAyBlG;AAWD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE;IAAE,EAAE,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,EAC7D,aAAa,EAAE,MAAM,GACpB,OAAO,CAIT;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,IAAI,CAAC;IACb,MAAM,CAAC,EAAE,IAAI,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GAAG;IACF,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,UAAU,EAAE,MAAM,CAAC;CACpB,CAiBA;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,IAAI,CAAC;IACb,MAAM,CAAC,EAAE,IAAI,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,GAAG;IACF,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC,CA0BA;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IACP,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,IAAI,CAAC;IACb,MAAM,CAAC,EAAE,IAAI,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACf,GACL,OAAO,CAAC,iBAAiB,CAAC,CAsH5B;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE;IACL,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,IAAI,CAAC;IACb,MAAM,CAAC,EAAE,IAAI,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GACA,OAAO,CAAC,iBAAiB,CAAC,CAsG5B;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,YAAY,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAqF1E;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAS1F;AAaD,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,EAAE,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,WAAW,EAAE,OAAO,CAAC;CACtB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAoClE;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,OAAO,CAiBvE;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;IACP,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GACA,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAqG/B"}
1
+ {"version":3,"file":"imap.d.ts","sourceRoot":"","sources":["../../src/lib/imap.ts"],"names":[],"mappings":"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"}
@@ -81,6 +81,142 @@ export function stripHtml(html) {
81
81
  const BODY_MAX_BYTES = 4096;
82
82
  /** Max characters for body preview in tool output. */
83
83
  const BODY_PREVIEW_LENGTH = 500;
84
+ /** Hard cap on a single attachment's bytes — over this size, metadata is surfaced
85
+ * but bytes are not downloaded. Matches typical provider send limits. */
86
+ export const MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
87
+ /**
88
+ * Walk an IMAP bodyStructure and return metadata for every attachment-shaped
89
+ * part. No bytes are downloaded — cheap, runs at preview time.
90
+ *
91
+ * An attachment-shaped part is one of:
92
+ * - disposition === "attachment" (RFC 2183), OR
93
+ * - has a filename in parameters.name or dispositionParameters.filename AND
94
+ * is not a multipart container.
95
+ *
96
+ * Inline parts without a filename (e.g. the message body) are excluded.
97
+ */
98
+ export function walkAttachmentMetadata(structure) {
99
+ if (!structure)
100
+ return [];
101
+ const out = [];
102
+ const visit = (node) => {
103
+ if (node.childNodes && node.childNodes.length > 0) {
104
+ for (const child of node.childNodes)
105
+ visit(child);
106
+ return;
107
+ }
108
+ const filename = node.dispositionParameters?.filename?.trim() ||
109
+ node.parameters?.name?.trim() ||
110
+ "";
111
+ const isAttachmentDisposition = typeof node.disposition === "string" &&
112
+ node.disposition.toLowerCase() === "attachment";
113
+ const isMultipart = typeof node.type === "string" && node.type.toLowerCase().startsWith("multipart/");
114
+ if (isMultipart)
115
+ return;
116
+ if (!isAttachmentDisposition && !filename)
117
+ return;
118
+ if (!node.part)
119
+ return; // can't fetch bytes without a part key
120
+ out.push({
121
+ partKey: node.part,
122
+ filename: filename || `unnamed-${node.part}`,
123
+ mimeType: (node.type ?? "application/octet-stream").toLowerCase(),
124
+ sizeBytes: typeof node.size === "number" ? node.size : 0,
125
+ encoding: node.encoding ?? null,
126
+ });
127
+ };
128
+ visit(structure);
129
+ return out;
130
+ }
131
+ function formatBytes(n) {
132
+ if (n < 1024)
133
+ return `${n} B`;
134
+ if (n < 1024 * 1024)
135
+ return `${(n / 1024).toFixed(1)} KB`;
136
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
137
+ }
138
+ /**
139
+ * Format an AttachmentMeta[] as a single-line operator-readable footer.
140
+ * Returns empty string when the array is empty so callers can skip the
141
+ * line entirely.
142
+ */
143
+ export function formatAttachmentsFooter(attachments) {
144
+ if (attachments.length === 0)
145
+ return "";
146
+ const parts = attachments
147
+ .map((a) => `${a.filename} (${a.mimeType}, ${formatBytes(a.sizeBytes)})`)
148
+ .join("; ");
149
+ return `Attachments (${attachments.length}): ${parts}`;
150
+ }
151
+ /**
152
+ * Download decoded bytes for a list of (uid, partKey) attachments from IMAP.
153
+ * Opens one connection, reuses it across all requests. Per-attachment
154
+ * failures are isolated — one bad part does not abort the batch.
155
+ */
156
+ export async function fetchAttachmentBytesForMessages(config, password, requests) {
157
+ if (requests.length === 0)
158
+ return [];
159
+ const client = createClient(config, password);
160
+ const results = [];
161
+ try {
162
+ await client.connect();
163
+ const lock = await client.getMailboxLock("INBOX");
164
+ try {
165
+ for (const req of requests) {
166
+ if (req.expectedSize > MAX_ATTACHMENT_BYTES) {
167
+ diag("fetchAttachmentBytes", "oversize", `uid=${req.uid} part=${req.partKey} size=${req.expectedSize} cap=${MAX_ATTACHMENT_BYTES}`);
168
+ results.push({ uid: req.uid, partKey: req.partKey, bytes: null, failureReason: "oversize" });
169
+ continue;
170
+ }
171
+ try {
172
+ const { content } = await client.download(String(req.uid), req.partKey, { uid: true });
173
+ if (!content) {
174
+ diag("fetchAttachmentBytes", "no-stream", `uid=${req.uid} part=${req.partKey}`);
175
+ results.push({ uid: req.uid, partKey: req.partKey, bytes: null, failureReason: "fetch-error" });
176
+ continue;
177
+ }
178
+ const chunks = [];
179
+ let total = 0;
180
+ let overflow = false;
181
+ for await (const chunk of content) {
182
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
183
+ total += buf.length;
184
+ if (total > MAX_ATTACHMENT_BYTES) {
185
+ overflow = true;
186
+ break;
187
+ }
188
+ chunks.push(buf);
189
+ }
190
+ if (overflow) {
191
+ diag("fetchAttachmentBytes", "stream-oversize", `uid=${req.uid} part=${req.partKey} streamed=${total}`);
192
+ results.push({ uid: req.uid, partKey: req.partKey, bytes: null, failureReason: "oversize" });
193
+ continue;
194
+ }
195
+ // ImapFlow's download() returns already-decoded bytes when given a
196
+ // partKey, so we do not re-decode against `encoding` here.
197
+ const bytes = Buffer.concat(chunks);
198
+ if (req.expectedSize > 0 && bytes.length === 0) {
199
+ diag("fetchAttachmentBytes", "short-buffer", `uid=${req.uid} part=${req.partKey} expected=${req.expectedSize} got=0`);
200
+ results.push({ uid: req.uid, partKey: req.partKey, bytes: null, failureReason: "short-buffer" });
201
+ continue;
202
+ }
203
+ results.push({ uid: req.uid, partKey: req.partKey, bytes, failureReason: null });
204
+ }
205
+ catch (err) {
206
+ diag("fetchAttachmentBytes", "fetch-error", `uid=${req.uid} part=${req.partKey} ${err instanceof Error ? err.message : String(err)}`);
207
+ results.push({ uid: req.uid, partKey: req.partKey, bytes: null, failureReason: "fetch-error" });
208
+ }
209
+ }
210
+ }
211
+ finally {
212
+ lock.release();
213
+ }
214
+ }
215
+ finally {
216
+ await safeLogout(client);
217
+ }
218
+ return results;
219
+ }
84
220
  /**
85
221
  * Decode a raw IMAP body part buffer according to its transfer encoding.
86
222
  * Returns a UTF-8 string. Falls back to raw UTF-8 for unknown encodings.
@@ -328,6 +464,7 @@ export async function fetchRecent(config, password, options = {}) {
328
464
  break;
329
465
  const bodyPreview = extractBodyFromFetch(msg);
330
466
  diag("fetchRecent", `body #${envelopeCount}`, `uid=${msg.uid} preview=${bodyPreview.length}chars type=${msg.bodyStructure?.type ?? "?"}`);
467
+ const attachments = walkAttachmentMetadata(msg.bodyStructure);
331
468
  messages.push({
332
469
  uid: msg.uid,
333
470
  messageId,
@@ -336,6 +473,7 @@ export async function fetchRecent(config, password, options = {}) {
336
473
  subject,
337
474
  date,
338
475
  bodyPreview,
476
+ attachments,
339
477
  });
340
478
  }
341
479
  diag("fetchRecent", "fetch done", `${envelopeCount} envelopes, ${messages.length} messages in ${Date.now() - t2}ms (total ${Date.now() - t0}ms)`);
@@ -423,6 +561,7 @@ export async function searchMessages(config, password, query) {
423
561
  if (messages.length >= limit)
424
562
  break;
425
563
  const bodyPreview = extractBodyFromFetch(msg);
564
+ const attachments = walkAttachmentMetadata(msg.bodyStructure);
426
565
  messages.push({
427
566
  uid: msg.uid,
428
567
  messageId,
@@ -431,6 +570,7 @@ export async function searchMessages(config, password, query) {
431
570
  subject: subj,
432
571
  date,
433
572
  bodyPreview,
573
+ attachments,
434
574
  });
435
575
  }
436
576
  diag("searchMessages", "fetch done", `${envelopeCount} envelopes, ${messages.length} messages in ${Date.now() - t2}ms (total ${Date.now() - t0}ms)`);
@@ -528,7 +668,8 @@ export async function fetchSinceUid(config, password, sinceUid, recipientAddress
528
668
  const toAddresses = (msg.envelope?.to ?? []).map((a) => a.address).filter((a) => !!a);
529
669
  const ccAddresses = (msg.envelope?.cc ?? []).map((a) => a.address).filter((a) => !!a);
530
670
  const bodyPreview = extractBodyFromFetch(msg);
531
- emails.push({ uid, messageId, inReplyTo, toAddresses, ccAddresses, fromAddress: fromAddr, fromName, subject, date, bodyPreview });
671
+ const attachments = walkAttachmentMetadata(msg.bodyStructure);
672
+ emails.push({ uid, messageId, inReplyTo, toAddresses, ccAddresses, fromAddress: fromAddr, fromName, subject, date, bodyPreview, attachments });
532
673
  }
533
674
  diag("fetchSinceUid", "fetch done", `${emails.length} emails (maxUid=${maxUid}) in ${Date.now() - t2}ms (total ${Date.now() - t0}ms)`);
534
675
  return { uidValidity, emails, maxUid };
@@ -542,191 +683,85 @@ export async function fetchSinceUid(config, password, sinceUid, recipientAddress
542
683
  }
543
684
  }
544
685
  /**
545
- * Get the inbox message count.
686
+ * Resolve an envelope by Message-ID via IMAP. Searches INBOX first, then
687
+ * the \Sent special-use folder. Returns null when no folder yields a hit.
688
+ *
689
+ * Uses `header: { "message-id": ... }` IMAP SEARCH — sufficient on every
690
+ * server that honours RFC 3501 header search. The Message-ID is sent with
691
+ * angle brackets restored because some servers (notably Gmail) only match
692
+ * the raw header value, not the bare identifier.
546
693
  */
547
- export async function getInboxCount(config, password) {
694
+ export async function fetchEnvelopeByMessageId(config, password, messageId) {
695
+ const cleanId = messageId.replace(/^<|>$/g, "");
696
+ const needle = `<${cleanId}>`;
548
697
  const client = createClient(config, password);
698
+ const t0 = Date.now();
699
+ diag("fetchEnvelopeByMessageId", "start", `messageId=${cleanId}`);
549
700
  try {
550
701
  await client.connect();
551
- const mailbox = await client.mailboxOpen("INBOX");
552
- return mailbox.exists;
553
- }
554
- finally {
555
- await safeLogout(client);
556
- }
557
- }
558
- // ─── Auto-respond types and fetch ────────────────────────────────────
559
- /** Headers used to detect auto-replies (RFC 3834) and mailing lists. */
560
- const AUTO_REPLY_HEADER_FIELDS = [
561
- "Auto-Submitted",
562
- "X-Auto-Response-Suppress",
563
- "Precedence",
564
- "List-Unsubscribe",
565
- "List-Id",
566
- ];
567
- /**
568
- * Parse raw IMAP header text into a map of lowercased header names → values.
569
- * Handles multi-line continuation (lines starting with whitespace).
570
- */
571
- export function parseRawHeaders(raw) {
572
- const headers = new Map();
573
- const lines = raw.split(/\r?\n/);
574
- let currentKey = "";
575
- let currentValue = "";
576
- for (const line of lines) {
577
- if (line === "")
578
- break; // End of headers
579
- if (/^\s/.test(line)) {
580
- // Continuation of previous header
581
- currentValue += " " + line.trim();
702
+ diag("fetchEnvelopeByMessageId", "connected", `${Date.now() - t0}ms`);
703
+ const folders = ["INBOX"];
704
+ try {
705
+ const sentPath = await resolveFolderName(client, "sent");
706
+ folders.push(sentPath);
582
707
  }
583
- else {
584
- // Flush previous header
585
- if (currentKey) {
586
- const existing = headers.get(currentKey) ?? [];
587
- existing.push(currentValue);
588
- headers.set(currentKey, existing);
589
- }
590
- const colonIdx = line.indexOf(":");
591
- if (colonIdx > 0) {
592
- currentKey = line.slice(0, colonIdx).trim().toLowerCase();
593
- currentValue = line.slice(colonIdx + 1).trim();
708
+ catch {
709
+ // No \Sent folder advertised — only search INBOX.
710
+ }
711
+ for (const folderPath of folders) {
712
+ const lock = await client.getMailboxLock(folderPath);
713
+ try {
714
+ const uids = await client.search({ header: { "message-id": needle } }, { uid: true });
715
+ if (!uids || uids.length === 0)
716
+ continue;
717
+ const uid = uids[0];
718
+ for await (const msg of client.fetch(String(uid), { envelope: true, uid: true }, { uid: true })) {
719
+ const env = msg.envelope ?? {};
720
+ const fromAddr = env.from?.[0]?.address ?? "";
721
+ if (!fromAddr)
722
+ continue;
723
+ diag("fetchEnvelopeByMessageId", "match", `folder=${folderPath} uid=${msg.uid} in ${Date.now() - t0}ms`);
724
+ return {
725
+ uid: msg.uid,
726
+ mailbox: folderPath,
727
+ messageId: env.messageId ?? messageId,
728
+ fromAddress: fromAddr,
729
+ fromName: env.from?.[0]?.name ?? null,
730
+ toAddresses: (env.to ?? [])
731
+ .map((a) => a.address)
732
+ .filter((a) => !!a),
733
+ ccAddresses: (env.cc ?? [])
734
+ .map((a) => a.address)
735
+ .filter((a) => !!a),
736
+ subject: env.subject ?? "",
737
+ date: env.date?.toISOString() ?? "",
738
+ };
739
+ }
594
740
  }
595
- else {
596
- currentKey = "";
597
- currentValue = "";
741
+ finally {
742
+ lock.release();
598
743
  }
599
744
  }
745
+ diag("fetchEnvelopeByMessageId", "no-match", `searched=${folders.join(",")} in ${Date.now() - t0}ms`);
746
+ return null;
600
747
  }
601
- // Flush last header
602
- if (currentKey) {
603
- const existing = headers.get(currentKey) ?? [];
604
- existing.push(currentValue);
605
- headers.set(currentKey, existing);
606
- }
607
- return headers;
608
- }
609
- /**
610
- * Detect whether a message is an auto-reply based on RFC 3834 headers.
611
- *
612
- * Checks:
613
- * - Auto-Submitted: any value other than "no"
614
- * - X-Auto-Response-Suppress: present
615
- * - Precedence: bulk, junk, or list
616
- * - List-Unsubscribe or List-Id: present (mailing list)
617
- */
618
- export function detectAutoReply(headers) {
619
- const autoSubmitted = headers.get("auto-submitted");
620
- if (autoSubmitted && autoSubmitted.some((v) => v.trim().toLowerCase() !== "no")) {
621
- return true;
748
+ catch (err) {
749
+ diag("fetchEnvelopeByMessageId", "FAILED", `${err instanceof Error ? err.message : String(err)} after ${Date.now() - t0}ms`);
750
+ throw err;
622
751
  }
623
- if (headers.has("x-auto-response-suppress"))
624
- return true;
625
- const precedence = headers.get("precedence");
626
- if (precedence) {
627
- const val = precedence[0]?.trim().toLowerCase();
628
- if (val === "bulk" || val === "junk" || val === "list")
629
- return true;
752
+ finally {
753
+ await safeLogout(client);
630
754
  }
631
- if (headers.has("list-unsubscribe") || headers.has("list-id"))
632
- return true;
633
- return false;
634
755
  }
635
756
  /**
636
- * Fetch new messages for auto-respond processing.
637
- *
638
- * Returns messages with UIDs greater than `sinceUid`, filtered by the
639
- * agent's address (TO header). Includes envelope, body, and auto-reply
640
- * detection headers in a single IMAP FETCH.
641
- *
642
- * Unlike fetchRecent (optimised for tool output with pagination),
643
- * this function fetches everything needed for auto-respond in one pass:
644
- * envelope for threading, body for agent input, headers for loop guards.
757
+ * Get the inbox message count.
645
758
  */
646
- export async function fetchForAutoRespond(config, password, options) {
647
- const { sinceUid, agentAddress, limit = 20 } = options;
759
+ export async function getInboxCount(config, password) {
648
760
  const client = createClient(config, password);
649
- const t0 = Date.now();
650
- diag("fetchForAutoRespond", "start", `sinceUid=${sinceUid} agentAddress=${agentAddress} limit=${limit}`);
651
761
  try {
652
762
  await client.connect();
653
- diag("fetchForAutoRespond", "connected", `${Date.now() - t0}ms`);
654
- const lock = await client.getMailboxLock("INBOX");
655
- diag("fetchForAutoRespond", "mailbox locked", `${Date.now() - t0}ms`);
656
- try {
657
- // Search for messages with UID > sinceUid
658
- const criteria = {};
659
- if (sinceUid > 0) {
660
- criteria.uid = `${sinceUid + 1}:*`;
661
- }
662
- else {
663
- // First poll: only fetch recent messages (last 7 days)
664
- const since = new Date();
665
- since.setDate(since.getDate() - DEFAULT_SINCE_DAYS);
666
- criteria.since = since;
667
- }
668
- const t1 = Date.now();
669
- const searchResult = await client.search(criteria, { uid: true });
670
- const uids = searchResult || [];
671
- diag("fetchForAutoRespond", "search done", `${uids.length} UIDs in ${Date.now() - t1}ms`);
672
- if (uids.length === 0)
673
- return [];
674
- // Take the oldest N UIDs (process in chronological order)
675
- const processUids = uids.slice(0, limit);
676
- const messages = [];
677
- const t2 = Date.now();
678
- const headerSection = `HEADER.FIELDS (${AUTO_REPLY_HEADER_FIELDS.join(" ")})`;
679
- for await (const msg of client.fetch(processUids.map(String).join(","), {
680
- envelope: true,
681
- uid: true,
682
- bodyStructure: true,
683
- bodyParts: [
684
- { key: "1", start: 0, maxLength: BODY_MAX_BYTES },
685
- ],
686
- headers: AUTO_REPLY_HEADER_FIELDS.map((h) => h.toLowerCase()),
687
- }, { uid: true })) {
688
- // Client-side TO filter: only messages addressed to the agent
689
- if (!matchesRecipient(msg.envelope ?? {}, agentAddress))
690
- continue;
691
- const envelope = msg.envelope ?? {};
692
- const fromAddr = envelope.from?.[0]?.address ?? "";
693
- const toAddrs = (envelope.to ?? []).map((a) => a.address ?? "");
694
- const subject = envelope.subject ?? "(no subject)";
695
- const date = envelope.date?.toISOString() ?? new Date().toISOString();
696
- const messageId = envelope.messageId ?? "";
697
- const inReplyTo = envelope.inReplyTo ?? null;
698
- // Extract References from envelope (not available in all IMAP servers)
699
- // Fall back to null — threading still works with just In-Reply-To
700
- const references = null;
701
- // Extract body
702
- const body = extractBodyFromFetch(msg);
703
- // Detect auto-reply from raw headers (Buffer from ImapFlow)
704
- const rawHeaders = msg.headers ? msg.headers.toString("utf-8") : "";
705
- const headerMap = parseRawHeaders(rawHeaders);
706
- const isAutoReply = detectAutoReply(headerMap);
707
- messages.push({
708
- uid: msg.uid,
709
- messageId,
710
- from: fromAddr,
711
- to: toAddrs,
712
- subject,
713
- date,
714
- inReplyTo,
715
- references,
716
- body,
717
- isAutoReply,
718
- });
719
- }
720
- diag("fetchForAutoRespond", "fetch done", `${messages.length} messages in ${Date.now() - t2}ms (total ${Date.now() - t0}ms)`);
721
- return messages;
722
- }
723
- finally {
724
- lock.release();
725
- }
726
- }
727
- catch (err) {
728
- diag("fetchForAutoRespond", "FAILED", `${err instanceof Error ? err.message : String(err)} after ${Date.now() - t0}ms`);
729
- throw err;
763
+ const mailbox = await client.mailboxOpen("INBOX");
764
+ return mailbox.exists;
730
765
  }
731
766
  finally {
732
767
  await safeLogout(client);