aamp-openclaw-plugin 0.1.7 → 0.1.8
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/README.md +3 -3
- package/bin/aamp-openclaw-plugin.mjs +177 -0
- package/dist/file-store.js +43 -0
- package/dist/file-store.js.map +7 -0
- package/dist/index.js +39 -1212
- package/dist/index.js.map +4 -4
- package/openclaw.plugin.json +6 -5
- package/package.json +13 -2
- package/skills/SKILL.md +1 -1
package/dist/index.js
CHANGED
|
@@ -1,1199 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import WebSocket from "ws";
|
|
11
|
-
import { EventEmitter } from "events";
|
|
12
|
-
|
|
13
|
-
// node_modules/aamp-sdk/dist/types.js
|
|
14
|
-
var AAMP_HEADER = {
|
|
15
|
-
INTENT: "X-AAMP-Intent",
|
|
16
|
-
TASK_ID: "X-AAMP-TaskId",
|
|
17
|
-
TIMEOUT: "X-AAMP-Timeout",
|
|
18
|
-
CONTEXT_LINKS: "X-AAMP-ContextLinks",
|
|
19
|
-
STATUS: "X-AAMP-Status",
|
|
20
|
-
OUTPUT: "X-AAMP-Output",
|
|
21
|
-
ERROR_MSG: "X-AAMP-ErrorMsg",
|
|
22
|
-
STRUCTURED_RESULT: "X-AAMP-StructuredResult",
|
|
23
|
-
QUESTION: "X-AAMP-Question",
|
|
24
|
-
BLOCKED_REASON: "X-AAMP-BlockedReason",
|
|
25
|
-
SUGGESTED_OPTIONS: "X-AAMP-SuggestedOptions",
|
|
26
|
-
PARENT_TASK_ID: "X-AAMP-ParentTaskId"
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
// node_modules/aamp-sdk/dist/parser.js
|
|
30
|
-
function normalizeHeaders(headers) {
|
|
31
|
-
return Object.fromEntries(Object.entries(headers).map(([k, v]) => [
|
|
32
|
-
k.toLowerCase(),
|
|
33
|
-
Array.isArray(v) ? v[0] : v
|
|
34
|
-
]));
|
|
35
|
-
}
|
|
36
|
-
function getAampHeader(headers, headerName) {
|
|
37
|
-
return headers[headerName.toLowerCase()];
|
|
38
|
-
}
|
|
39
|
-
function decodeStructuredResult(value) {
|
|
40
|
-
if (!value)
|
|
41
|
-
return void 0;
|
|
42
|
-
try {
|
|
43
|
-
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
44
|
-
const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
|
|
45
|
-
const decoded = Buffer.from(normalized + padding, "base64").toString("utf-8");
|
|
46
|
-
return JSON.parse(decoded);
|
|
47
|
-
} catch {
|
|
48
|
-
return void 0;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
function encodeStructuredResult(value) {
|
|
52
|
-
if (!value)
|
|
53
|
-
return void 0;
|
|
54
|
-
const json = JSON.stringify(value);
|
|
55
|
-
return Buffer.from(json, "utf-8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
56
|
-
}
|
|
57
|
-
function parseAampHeaders(meta) {
|
|
58
|
-
const headers = normalizeHeaders(meta.headers);
|
|
59
|
-
const intent = getAampHeader(headers, AAMP_HEADER.INTENT);
|
|
60
|
-
const taskId = getAampHeader(headers, AAMP_HEADER.TASK_ID);
|
|
61
|
-
if (!intent || !taskId)
|
|
62
|
-
return null;
|
|
63
|
-
const from = meta.from.replace(/^<|>$/g, "");
|
|
64
|
-
const to = meta.to.replace(/^<|>$/g, "");
|
|
65
|
-
if (intent === "task.dispatch") {
|
|
66
|
-
const timeoutStr = getAampHeader(headers, AAMP_HEADER.TIMEOUT) ?? "300";
|
|
67
|
-
const contextLinksStr = getAampHeader(headers, AAMP_HEADER.CONTEXT_LINKS) ?? "";
|
|
68
|
-
const parentTaskId = getAampHeader(headers, AAMP_HEADER.PARENT_TASK_ID);
|
|
69
|
-
const dispatch = {
|
|
70
|
-
intent: "task.dispatch",
|
|
71
|
-
taskId,
|
|
72
|
-
title: meta.subject.replace(/^\[AAMP Task\]\s*/, "").trim() || "Untitled Task",
|
|
73
|
-
timeoutSecs: parseInt(timeoutStr, 10) || 300,
|
|
74
|
-
contextLinks: contextLinksStr ? contextLinksStr.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
75
|
-
...parentTaskId ? { parentTaskId } : {},
|
|
76
|
-
from,
|
|
77
|
-
to,
|
|
78
|
-
messageId: meta.messageId,
|
|
79
|
-
subject: meta.subject,
|
|
80
|
-
bodyText: ""
|
|
81
|
-
// filled in by jmap-push.ts after parsing
|
|
82
|
-
};
|
|
83
|
-
return dispatch;
|
|
84
|
-
}
|
|
85
|
-
if (intent === "task.result") {
|
|
86
|
-
const status = getAampHeader(headers, AAMP_HEADER.STATUS) ?? "completed";
|
|
87
|
-
const output = getAampHeader(headers, AAMP_HEADER.OUTPUT) ?? "";
|
|
88
|
-
const errorMsg = getAampHeader(headers, AAMP_HEADER.ERROR_MSG);
|
|
89
|
-
const structuredResult = decodeStructuredResult(getAampHeader(headers, AAMP_HEADER.STRUCTURED_RESULT));
|
|
90
|
-
const result = {
|
|
91
|
-
intent: "task.result",
|
|
92
|
-
taskId,
|
|
93
|
-
status,
|
|
94
|
-
output,
|
|
95
|
-
errorMsg,
|
|
96
|
-
structuredResult,
|
|
97
|
-
from,
|
|
98
|
-
to
|
|
99
|
-
};
|
|
100
|
-
return result;
|
|
101
|
-
}
|
|
102
|
-
if (intent === "task.help") {
|
|
103
|
-
const question = getAampHeader(headers, AAMP_HEADER.QUESTION) ?? "";
|
|
104
|
-
const blockedReason = getAampHeader(headers, AAMP_HEADER.BLOCKED_REASON) ?? "";
|
|
105
|
-
const suggestedOptionsStr = getAampHeader(headers, AAMP_HEADER.SUGGESTED_OPTIONS) ?? "";
|
|
106
|
-
const help = {
|
|
107
|
-
intent: "task.help",
|
|
108
|
-
taskId,
|
|
109
|
-
question,
|
|
110
|
-
blockedReason,
|
|
111
|
-
suggestedOptions: suggestedOptionsStr ? suggestedOptionsStr.split("|").map((s) => s.trim()).filter(Boolean) : [],
|
|
112
|
-
from,
|
|
113
|
-
to
|
|
114
|
-
};
|
|
115
|
-
return help;
|
|
116
|
-
}
|
|
117
|
-
if (intent === "task.ack") {
|
|
118
|
-
const ack = {
|
|
119
|
-
intent: "task.ack",
|
|
120
|
-
taskId,
|
|
121
|
-
from,
|
|
122
|
-
to
|
|
123
|
-
};
|
|
124
|
-
return ack;
|
|
125
|
-
}
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
function buildDispatchHeaders(params) {
|
|
129
|
-
const headers = {
|
|
130
|
-
[AAMP_HEADER.INTENT]: "task.dispatch",
|
|
131
|
-
[AAMP_HEADER.TASK_ID]: params.taskId
|
|
132
|
-
};
|
|
133
|
-
if (params.timeoutSecs != null) {
|
|
134
|
-
headers[AAMP_HEADER.TIMEOUT] = String(params.timeoutSecs);
|
|
135
|
-
}
|
|
136
|
-
if (params.contextLinks.length > 0) {
|
|
137
|
-
headers[AAMP_HEADER.CONTEXT_LINKS] = params.contextLinks.join(",");
|
|
138
|
-
}
|
|
139
|
-
if (params.parentTaskId) {
|
|
140
|
-
headers[AAMP_HEADER.PARENT_TASK_ID] = params.parentTaskId;
|
|
141
|
-
}
|
|
142
|
-
return headers;
|
|
143
|
-
}
|
|
144
|
-
function buildAckHeaders(opts) {
|
|
145
|
-
return {
|
|
146
|
-
[AAMP_HEADER.INTENT]: "task.ack",
|
|
147
|
-
[AAMP_HEADER.TASK_ID]: opts.taskId
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
function buildResultHeaders(params) {
|
|
151
|
-
const headers = {
|
|
152
|
-
[AAMP_HEADER.INTENT]: "task.result",
|
|
153
|
-
[AAMP_HEADER.TASK_ID]: params.taskId,
|
|
154
|
-
[AAMP_HEADER.STATUS]: params.status,
|
|
155
|
-
[AAMP_HEADER.OUTPUT]: params.output
|
|
156
|
-
};
|
|
157
|
-
if (params.errorMsg) {
|
|
158
|
-
headers[AAMP_HEADER.ERROR_MSG] = params.errorMsg;
|
|
159
|
-
}
|
|
160
|
-
const structuredResult = encodeStructuredResult(params.structuredResult);
|
|
161
|
-
if (structuredResult) {
|
|
162
|
-
headers[AAMP_HEADER.STRUCTURED_RESULT] = structuredResult;
|
|
163
|
-
}
|
|
164
|
-
return headers;
|
|
165
|
-
}
|
|
166
|
-
function buildHelpHeaders(params) {
|
|
167
|
-
return {
|
|
168
|
-
[AAMP_HEADER.INTENT]: "task.help",
|
|
169
|
-
[AAMP_HEADER.TASK_ID]: params.taskId,
|
|
170
|
-
[AAMP_HEADER.QUESTION]: params.question,
|
|
171
|
-
[AAMP_HEADER.BLOCKED_REASON]: params.blockedReason,
|
|
172
|
-
[AAMP_HEADER.SUGGESTED_OPTIONS]: params.suggestedOptions.join("|")
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// node_modules/aamp-sdk/dist/jmap-push.js
|
|
177
|
-
var JmapPushClient = class extends EventEmitter {
|
|
178
|
-
ws = null;
|
|
179
|
-
session = null;
|
|
180
|
-
reconnectTimer = null;
|
|
181
|
-
pollTimer = null;
|
|
182
|
-
pingTimer = null;
|
|
183
|
-
seenMessageIds = /* @__PURE__ */ new Set();
|
|
184
|
-
connected = false;
|
|
185
|
-
pollingActive = false;
|
|
186
|
-
running = false;
|
|
187
|
-
connecting = false;
|
|
188
|
-
/** JMAP Email state — tracks processed position; null = not yet initialized */
|
|
189
|
-
emailState = null;
|
|
190
|
-
startedAtMs = Date.now();
|
|
191
|
-
email;
|
|
192
|
-
password;
|
|
193
|
-
jmapUrl;
|
|
194
|
-
reconnectInterval;
|
|
195
|
-
rejectUnauthorized;
|
|
196
|
-
pingIntervalMs = 5e3;
|
|
197
|
-
constructor(opts) {
|
|
198
|
-
super();
|
|
199
|
-
this.email = opts.email;
|
|
200
|
-
this.password = opts.password;
|
|
201
|
-
this.jmapUrl = opts.jmapUrl.replace(/\/$/, "");
|
|
202
|
-
this.reconnectInterval = opts.reconnectInterval ?? 5e3;
|
|
203
|
-
this.rejectUnauthorized = opts.rejectUnauthorized ?? true;
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Start the JMAP Push listener
|
|
207
|
-
*/
|
|
208
|
-
async start() {
|
|
209
|
-
this.running = true;
|
|
210
|
-
await this.connect();
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* Stop the JMAP Push listener
|
|
214
|
-
*/
|
|
215
|
-
stop() {
|
|
216
|
-
this.running = false;
|
|
217
|
-
if (this.reconnectTimer) {
|
|
218
|
-
clearTimeout(this.reconnectTimer);
|
|
219
|
-
this.reconnectTimer = null;
|
|
220
|
-
}
|
|
221
|
-
if (this.pollTimer) {
|
|
222
|
-
clearTimeout(this.pollTimer);
|
|
223
|
-
this.pollTimer = null;
|
|
224
|
-
}
|
|
225
|
-
if (this.pingTimer) {
|
|
226
|
-
clearInterval(this.pingTimer);
|
|
227
|
-
this.pingTimer = null;
|
|
228
|
-
}
|
|
229
|
-
if (this.ws) {
|
|
230
|
-
this.ws.close();
|
|
231
|
-
this.ws = null;
|
|
232
|
-
}
|
|
233
|
-
this.connected = false;
|
|
234
|
-
this.pollingActive = false;
|
|
235
|
-
this.connecting = false;
|
|
236
|
-
}
|
|
237
|
-
getAuthHeader() {
|
|
238
|
-
const creds = `${this.email}:${this.password}`;
|
|
239
|
-
return `Basic ${Buffer.from(creds).toString("base64")}`;
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
|
-
* Fetch the JMAP session object
|
|
243
|
-
*/
|
|
244
|
-
async fetchSession() {
|
|
245
|
-
const url = `${this.jmapUrl}/.well-known/jmap`;
|
|
246
|
-
const res = await fetch(url, {
|
|
247
|
-
headers: { Authorization: this.getAuthHeader() }
|
|
248
|
-
});
|
|
249
|
-
if (!res.ok) {
|
|
250
|
-
throw new Error(`Failed to fetch JMAP session: ${res.status} ${res.statusText}`);
|
|
251
|
-
}
|
|
252
|
-
return res.json();
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Perform a JMAP API call
|
|
256
|
-
*/
|
|
257
|
-
async jmapCall(methods) {
|
|
258
|
-
if (!this.session)
|
|
259
|
-
throw new Error("No JMAP session");
|
|
260
|
-
const apiUrl = `${this.jmapUrl}/jmap/`;
|
|
261
|
-
const res = await fetch(apiUrl, {
|
|
262
|
-
method: "POST",
|
|
263
|
-
headers: {
|
|
264
|
-
Authorization: this.getAuthHeader(),
|
|
265
|
-
"Content-Type": "application/json"
|
|
266
|
-
},
|
|
267
|
-
body: JSON.stringify({
|
|
268
|
-
using: [
|
|
269
|
-
"urn:ietf:params:jmap:core",
|
|
270
|
-
"urn:ietf:params:jmap:mail"
|
|
271
|
-
],
|
|
272
|
-
methodCalls: methods
|
|
273
|
-
})
|
|
274
|
-
});
|
|
275
|
-
if (!res.ok) {
|
|
276
|
-
throw new Error(`JMAP API call failed: ${res.status}`);
|
|
277
|
-
}
|
|
278
|
-
return res.json();
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
|
-
* Initialize emailState by fetching the current state without loading any emails.
|
|
282
|
-
* Called on first connect so we only process emails that arrive AFTER this point.
|
|
283
|
-
*/
|
|
284
|
-
async initEmailState(accountId) {
|
|
285
|
-
const response = await this.jmapCall([
|
|
286
|
-
["Email/get", { accountId, ids: [] }, "g0"]
|
|
287
|
-
]);
|
|
288
|
-
const getResp = response.methodResponses.find(([name]) => name === "Email/get");
|
|
289
|
-
if (getResp) {
|
|
290
|
-
this.emailState = getResp[1].state ?? null;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* Fetch only emails created since `sinceState` using Email/changes.
|
|
295
|
-
* Updates `this.emailState` to the new state after fetching.
|
|
296
|
-
* Returns [] and resets state if the server cannot calculate changes (state too old).
|
|
297
|
-
*/
|
|
298
|
-
async fetchEmailsSince(accountId, sinceState) {
|
|
299
|
-
const changesResp = await this.jmapCall([
|
|
300
|
-
["Email/changes", { accountId, sinceState, maxChanges: 50 }, "c1"]
|
|
301
|
-
]);
|
|
302
|
-
const changesResult = changesResp.methodResponses.find(([name]) => name === "Email/changes");
|
|
303
|
-
if (!changesResult || changesResult[0] === "error") {
|
|
304
|
-
await this.initEmailState(accountId);
|
|
305
|
-
return [];
|
|
306
|
-
}
|
|
307
|
-
const changes = changesResult[1];
|
|
308
|
-
if (changes.newState) {
|
|
309
|
-
this.emailState = changes.newState;
|
|
310
|
-
}
|
|
311
|
-
const newIds = changes.created ?? [];
|
|
312
|
-
if (newIds.length === 0)
|
|
313
|
-
return [];
|
|
314
|
-
const emailResp = await this.jmapCall([
|
|
315
|
-
[
|
|
316
|
-
"Email/get",
|
|
317
|
-
{
|
|
318
|
-
accountId,
|
|
319
|
-
ids: newIds,
|
|
320
|
-
properties: ["id", "subject", "from", "to", "headers", "messageId", "receivedAt", "textBody", "bodyValues", "attachments"],
|
|
321
|
-
fetchTextBodyValues: true,
|
|
322
|
-
maxBodyValueBytes: 262144
|
|
323
|
-
},
|
|
324
|
-
"g1"
|
|
325
|
-
]
|
|
326
|
-
]);
|
|
327
|
-
const getResult = emailResp.methodResponses.find(([name]) => name === "Email/get");
|
|
328
|
-
if (!getResult)
|
|
329
|
-
return [];
|
|
330
|
-
const data = getResult[1];
|
|
331
|
-
return data.list ?? [];
|
|
332
|
-
}
|
|
333
|
-
/**
|
|
334
|
-
* Process a received email.
|
|
335
|
-
*
|
|
336
|
-
* Priority:
|
|
337
|
-
* 1. If X-AAMP-Intent is present → emit typed AAMP event (task.dispatch / task.result / task.help)
|
|
338
|
-
* 2. If In-Reply-To is present → emit 'reply' event so the application layer can
|
|
339
|
-
* resolve the thread (inReplyTo → taskId via Redis/DB) and handle human replies.
|
|
340
|
-
* 3. Otherwise → ignore (not an AAMP-related email)
|
|
341
|
-
*/
|
|
342
|
-
processEmail(email) {
|
|
343
|
-
const headerMap = {};
|
|
344
|
-
for (const h of email.headers ?? []) {
|
|
345
|
-
headerMap[h.name.toLowerCase()] = h.value.trim();
|
|
346
|
-
}
|
|
347
|
-
const fromAddr = email.from?.[0]?.email ?? "";
|
|
348
|
-
const toAddr = email.to?.[0]?.email ?? "";
|
|
349
|
-
const messageId = email.messageId?.[0] ?? email.id;
|
|
350
|
-
if (this.seenMessageIds.has(messageId))
|
|
351
|
-
return;
|
|
352
|
-
this.seenMessageIds.add(messageId);
|
|
353
|
-
const msg = parseAampHeaders({
|
|
354
|
-
from: fromAddr,
|
|
355
|
-
to: toAddr,
|
|
356
|
-
messageId,
|
|
357
|
-
subject: email.subject ?? "",
|
|
358
|
-
headers: headerMap
|
|
359
|
-
});
|
|
360
|
-
if (msg && "intent" in msg) {
|
|
361
|
-
const aampTextPartId = email.textBody?.[0]?.partId;
|
|
362
|
-
const aampBodyText = aampTextPartId ? (email.bodyValues?.[aampTextPartId]?.value ?? "").trim() : "";
|
|
363
|
-
msg.bodyText = aampBodyText;
|
|
364
|
-
const receivedAttachments = (email.attachments ?? []).map((a) => ({
|
|
365
|
-
filename: a.name ?? "attachment",
|
|
366
|
-
contentType: a.type,
|
|
367
|
-
size: a.size,
|
|
368
|
-
blobId: a.blobId
|
|
369
|
-
}));
|
|
370
|
-
if (receivedAttachments.length > 0) {
|
|
371
|
-
;
|
|
372
|
-
msg.attachments = receivedAttachments;
|
|
373
|
-
}
|
|
374
|
-
if (msg.intent === "task.dispatch") {
|
|
375
|
-
this.emit("_autoAck", { to: fromAddr, taskId: msg.taskId, messageId });
|
|
376
|
-
}
|
|
377
|
-
this.emit(msg.intent, msg);
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
const rawInReplyTo = headerMap["in-reply-to"] ?? "";
|
|
381
|
-
if (!rawInReplyTo)
|
|
382
|
-
return;
|
|
383
|
-
const rawReferences = headerMap["references"] ?? "";
|
|
384
|
-
const referencesIds = rawReferences.split(/\s+/).map((s) => s.replace(/[<>]/g, "").trim()).filter(Boolean);
|
|
385
|
-
const inReplyTo = rawInReplyTo.replace(/[<>]/g, "").trim();
|
|
386
|
-
const textPartId = email.textBody?.[0]?.partId;
|
|
387
|
-
const bodyText = textPartId ? (email.bodyValues?.[textPartId]?.value ?? "").trim() : "";
|
|
388
|
-
const reply = {
|
|
389
|
-
inReplyTo,
|
|
390
|
-
messageId,
|
|
391
|
-
from: fromAddr,
|
|
392
|
-
to: toAddr,
|
|
393
|
-
subject: email.subject ?? "",
|
|
394
|
-
bodyText
|
|
395
|
-
};
|
|
396
|
-
if (referencesIds.length > 0) {
|
|
397
|
-
Object.assign(reply, { references: referencesIds });
|
|
398
|
-
}
|
|
399
|
-
this.emit("reply", reply);
|
|
400
|
-
}
|
|
401
|
-
async fetchRecentEmails(accountId) {
|
|
402
|
-
const queryResp = await this.jmapCall([
|
|
403
|
-
[
|
|
404
|
-
"Email/query",
|
|
405
|
-
{
|
|
406
|
-
accountId,
|
|
407
|
-
sort: [{ property: "receivedAt", isAscending: false }],
|
|
408
|
-
limit: 20
|
|
409
|
-
},
|
|
410
|
-
"q1"
|
|
411
|
-
]
|
|
412
|
-
]);
|
|
413
|
-
const queryResult = queryResp.methodResponses.find(([name]) => name === "Email/query");
|
|
414
|
-
if (!queryResult)
|
|
415
|
-
return [];
|
|
416
|
-
const ids = (queryResult[1].ids ?? []).slice(0, 20);
|
|
417
|
-
if (ids.length === 0)
|
|
418
|
-
return [];
|
|
419
|
-
const emailResp = await this.jmapCall([
|
|
420
|
-
[
|
|
421
|
-
"Email/get",
|
|
422
|
-
{
|
|
423
|
-
accountId,
|
|
424
|
-
ids,
|
|
425
|
-
properties: ["id", "subject", "from", "to", "headers", "messageId", "receivedAt", "textBody", "bodyValues", "attachments"],
|
|
426
|
-
fetchTextBodyValues: true,
|
|
427
|
-
maxBodyValueBytes: 262144
|
|
428
|
-
},
|
|
429
|
-
"gRecent"
|
|
430
|
-
]
|
|
431
|
-
]);
|
|
432
|
-
const getResult = emailResp.methodResponses.find(([name]) => name === "Email/get");
|
|
433
|
-
if (!getResult)
|
|
434
|
-
return [];
|
|
435
|
-
return getResult[1].list ?? [];
|
|
436
|
-
}
|
|
437
|
-
shouldProcessBootstrapEmail(email) {
|
|
438
|
-
const receivedAtMs = new Date(email.receivedAt).getTime();
|
|
439
|
-
return Number.isFinite(receivedAtMs) && receivedAtMs >= this.startedAtMs - 15e3;
|
|
440
|
-
}
|
|
441
|
-
/**
|
|
442
|
-
* Connect to JMAP WebSocket
|
|
443
|
-
*/
|
|
444
|
-
async connect() {
|
|
445
|
-
if (this.connecting || !this.running)
|
|
446
|
-
return;
|
|
447
|
-
this.connecting = true;
|
|
448
|
-
try {
|
|
449
|
-
this.session = await this.fetchSession();
|
|
450
|
-
} catch (err) {
|
|
451
|
-
this.connecting = false;
|
|
452
|
-
this.emit("error", new Error(`Failed to get JMAP session: ${err.message}`));
|
|
453
|
-
this.startPolling("session fetch failed");
|
|
454
|
-
this.scheduleReconnect();
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
const stalwartWsUrl = `${this.jmapUrl}/_jmap_ws`.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://");
|
|
458
|
-
this.ws = new WebSocket(stalwartWsUrl, "jmap", {
|
|
459
|
-
headers: {
|
|
460
|
-
Authorization: this.getAuthHeader()
|
|
461
|
-
},
|
|
462
|
-
perMessageDeflate: false,
|
|
463
|
-
rejectUnauthorized: this.rejectUnauthorized
|
|
464
|
-
});
|
|
465
|
-
this.ws.on("unexpected-response", (_req, res) => {
|
|
466
|
-
this.connecting = false;
|
|
467
|
-
const headerSummary = Object.entries(res.headers).map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(", ") : value ?? ""}`).join("; ");
|
|
468
|
-
this.startPolling(`websocket handshake failed: ${res.statusCode ?? "unknown"}`);
|
|
469
|
-
this.emit("error", new Error(`JMAP WebSocket handshake failed: ${res.statusCode ?? "unknown"} ${res.statusMessage ?? ""}${headerSummary ? ` | headers: ${headerSummary}` : ""}`));
|
|
470
|
-
this.scheduleReconnect();
|
|
471
|
-
});
|
|
472
|
-
this.ws.on("open", async () => {
|
|
473
|
-
this.connecting = false;
|
|
474
|
-
this.connected = true;
|
|
475
|
-
this.stopPolling();
|
|
476
|
-
this.startPingHeartbeat();
|
|
477
|
-
const accountId = this.session?.primaryAccounts["urn:ietf:params:jmap:mail"];
|
|
478
|
-
if (accountId && this.emailState === null) {
|
|
479
|
-
await this.initEmailState(accountId);
|
|
480
|
-
}
|
|
481
|
-
this.ws.send(JSON.stringify({
|
|
482
|
-
"@type": "WebSocketPushEnable",
|
|
483
|
-
dataTypes: ["Email"],
|
|
484
|
-
pushState: null
|
|
485
|
-
}));
|
|
486
|
-
this.emit("connected");
|
|
487
|
-
});
|
|
488
|
-
this.ws.on("pong", () => {
|
|
489
|
-
});
|
|
490
|
-
this.ws.on("message", async (rawData) => {
|
|
491
|
-
try {
|
|
492
|
-
const msg = JSON.parse(rawData.toString());
|
|
493
|
-
if (msg["@type"] === "StateChange") {
|
|
494
|
-
await this.handleStateChange(msg);
|
|
495
|
-
}
|
|
496
|
-
} catch (err) {
|
|
497
|
-
this.emit("error", new Error(`Failed to process JMAP push message: ${err.message}`));
|
|
498
|
-
}
|
|
499
|
-
});
|
|
500
|
-
this.ws.on("close", (code, reason) => {
|
|
501
|
-
this.connecting = false;
|
|
502
|
-
this.connected = false;
|
|
503
|
-
this.stopPingHeartbeat();
|
|
504
|
-
const reasonStr = reason?.toString() ?? "connection closed";
|
|
505
|
-
this.startPolling(reasonStr);
|
|
506
|
-
this.emit("disconnected", reasonStr);
|
|
507
|
-
if (this.running) {
|
|
508
|
-
this.scheduleReconnect();
|
|
509
|
-
}
|
|
510
|
-
});
|
|
511
|
-
this.ws.on("error", (err) => {
|
|
512
|
-
this.connecting = false;
|
|
513
|
-
this.stopPingHeartbeat();
|
|
514
|
-
this.startPolling(err.message);
|
|
515
|
-
this.emit("error", err);
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
startPingHeartbeat() {
|
|
519
|
-
if (this.pingTimer) {
|
|
520
|
-
clearInterval(this.pingTimer);
|
|
521
|
-
this.pingTimer = null;
|
|
522
|
-
}
|
|
523
|
-
this.pingTimer = setInterval(() => {
|
|
524
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
|
|
525
|
-
return;
|
|
526
|
-
try {
|
|
527
|
-
this.ws.ping();
|
|
528
|
-
} catch (err) {
|
|
529
|
-
this.emit("error", new Error(`Failed to send WebSocket ping: ${err.message}`));
|
|
530
|
-
}
|
|
531
|
-
}, this.pingIntervalMs);
|
|
532
|
-
}
|
|
533
|
-
stopPingHeartbeat() {
|
|
534
|
-
if (this.pingTimer) {
|
|
535
|
-
clearInterval(this.pingTimer);
|
|
536
|
-
this.pingTimer = null;
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
async handleStateChange(stateChange) {
|
|
540
|
-
if (!this.session)
|
|
541
|
-
return;
|
|
542
|
-
const accountId = this.session.primaryAccounts["urn:ietf:params:jmap:mail"];
|
|
543
|
-
if (!accountId)
|
|
544
|
-
return;
|
|
545
|
-
const changedAccount = stateChange.changed[accountId];
|
|
546
|
-
if (!changedAccount?.Email)
|
|
547
|
-
return;
|
|
548
|
-
try {
|
|
549
|
-
if (this.emailState === null) {
|
|
550
|
-
await this.initEmailState(accountId);
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
const emails = await this.fetchEmailsSince(accountId, this.emailState);
|
|
554
|
-
for (const email of emails) {
|
|
555
|
-
this.processEmail(email);
|
|
556
|
-
}
|
|
557
|
-
} catch (err) {
|
|
558
|
-
this.emit("error", new Error(`Failed to fetch emails: ${err.message}`));
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
scheduleReconnect() {
|
|
562
|
-
if (this.reconnectTimer)
|
|
563
|
-
return;
|
|
564
|
-
this.reconnectTimer = setTimeout(async () => {
|
|
565
|
-
this.reconnectTimer = null;
|
|
566
|
-
if (this.running) {
|
|
567
|
-
await this.connect();
|
|
568
|
-
}
|
|
569
|
-
}, this.reconnectInterval);
|
|
570
|
-
}
|
|
571
|
-
isConnected() {
|
|
572
|
-
return this.connected || this.pollingActive;
|
|
573
|
-
}
|
|
574
|
-
isUsingPollingFallback() {
|
|
575
|
-
return this.pollingActive && !this.connected;
|
|
576
|
-
}
|
|
577
|
-
stopPolling() {
|
|
578
|
-
if (this.pollTimer) {
|
|
579
|
-
clearTimeout(this.pollTimer);
|
|
580
|
-
this.pollTimer = null;
|
|
581
|
-
}
|
|
582
|
-
this.pollingActive = false;
|
|
583
|
-
}
|
|
584
|
-
startPolling(reason) {
|
|
585
|
-
if (!this.running || this.pollingActive)
|
|
586
|
-
return;
|
|
587
|
-
this.pollingActive = true;
|
|
588
|
-
this.emit("error", new Error(`JMAP WebSocket unavailable, falling back to polling: ${reason}`));
|
|
589
|
-
this.emit("connected");
|
|
590
|
-
const poll = async () => {
|
|
591
|
-
if (!this.running || this.connected) {
|
|
592
|
-
this.stopPolling();
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
try {
|
|
596
|
-
if (!this.session) {
|
|
597
|
-
this.session = await this.fetchSession();
|
|
598
|
-
}
|
|
599
|
-
const accountId = this.session.primaryAccounts["urn:ietf:params:jmap:mail"] ?? Object.keys(this.session.accounts)[0];
|
|
600
|
-
if (!accountId) {
|
|
601
|
-
throw new Error("No mail account available in JMAP session");
|
|
602
|
-
}
|
|
603
|
-
if (this.emailState === null) {
|
|
604
|
-
const recentEmails = await this.fetchRecentEmails(accountId);
|
|
605
|
-
for (const email of recentEmails.sort((a, b) => {
|
|
606
|
-
const aTs = new Date(a.receivedAt).getTime();
|
|
607
|
-
const bTs = new Date(b.receivedAt).getTime();
|
|
608
|
-
return aTs - bTs;
|
|
609
|
-
})) {
|
|
610
|
-
if (!this.shouldProcessBootstrapEmail(email))
|
|
611
|
-
continue;
|
|
612
|
-
this.processEmail(email);
|
|
613
|
-
}
|
|
614
|
-
await this.initEmailState(accountId);
|
|
615
|
-
} else {
|
|
616
|
-
const emails = await this.fetchEmailsSince(accountId, this.emailState);
|
|
617
|
-
for (const email of emails) {
|
|
618
|
-
this.processEmail(email);
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
} catch (err) {
|
|
622
|
-
this.emit("error", new Error(`Polling fallback failed: ${err.message}`));
|
|
623
|
-
} finally {
|
|
624
|
-
if (this.running && !this.connected) {
|
|
625
|
-
this.pollTimer = setTimeout(poll, this.reconnectInterval);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
};
|
|
629
|
-
this.pollTimer = setTimeout(poll, 0);
|
|
630
|
-
}
|
|
631
|
-
/**
|
|
632
|
-
* Download a blob (attachment) by its JMAP blobId.
|
|
633
|
-
* Returns the raw binary content as a Buffer.
|
|
634
|
-
*/
|
|
635
|
-
async downloadBlob(blobId, filename) {
|
|
636
|
-
if (!this.session) {
|
|
637
|
-
this.session = await this.fetchSession();
|
|
638
|
-
}
|
|
639
|
-
const accountId = this.session.primaryAccounts["urn:ietf:params:jmap:mail"] ?? Object.keys(this.session.accounts)[0];
|
|
640
|
-
let downloadUrl = this.session.downloadUrl ?? `${this.jmapUrl}/jmap/download/{accountId}/{blobId}/{name}`;
|
|
641
|
-
try {
|
|
642
|
-
const parsed = new URL(downloadUrl);
|
|
643
|
-
const configured = new URL(this.jmapUrl);
|
|
644
|
-
parsed.protocol = configured.protocol;
|
|
645
|
-
parsed.host = configured.host;
|
|
646
|
-
downloadUrl = parsed.toString();
|
|
647
|
-
} catch {
|
|
648
|
-
}
|
|
649
|
-
const safeFilename = filename ?? "attachment";
|
|
650
|
-
downloadUrl = downloadUrl.replace(/\{accountId\}|%7BaccountId%7D/gi, encodeURIComponent(accountId)).replace(/\{blobId\}|%7BblobId%7D/gi, encodeURIComponent(blobId)).replace(/\{name\}|%7Bname%7D/gi, encodeURIComponent(safeFilename)).replace(/\{type\}|%7Btype%7D/gi, "application/octet-stream");
|
|
651
|
-
const maxAttempts = 8;
|
|
652
|
-
let lastStatus = null;
|
|
653
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
654
|
-
const res = await fetch(downloadUrl, {
|
|
655
|
-
headers: { Authorization: this.getAuthHeader() }
|
|
656
|
-
});
|
|
657
|
-
lastStatus = res.status;
|
|
658
|
-
if (res.ok) {
|
|
659
|
-
const arrayBuffer = await res.arrayBuffer();
|
|
660
|
-
return Buffer.from(arrayBuffer);
|
|
661
|
-
}
|
|
662
|
-
if (attempt < maxAttempts && (res.status === 404 || res.status === 429 || res.status === 503)) {
|
|
663
|
-
console.warn(`[AAMP-SDK] blob download retry status=${res.status} attempt=${attempt}/${maxAttempts} url=${downloadUrl}`);
|
|
664
|
-
const delay = Math.min(1e3 * Math.pow(2, attempt - 1), 15e3);
|
|
665
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
666
|
-
continue;
|
|
667
|
-
}
|
|
668
|
-
console.error(`[AAMP-SDK] blob download failed status=${res.status} attempt=${attempt}/${maxAttempts} url=${downloadUrl}`);
|
|
669
|
-
throw new Error(`Blob download failed: status=${res.status} attempt=${attempt}/${maxAttempts} blobId=${blobId} filename=${filename ?? "attachment"} url=${downloadUrl}`);
|
|
670
|
-
}
|
|
671
|
-
throw new Error(`Blob download failed after retries: status=${lastStatus ?? "unknown"} attempt=${maxAttempts}/${maxAttempts} blobId=${blobId} filename=${filename ?? "attachment"} url=${downloadUrl}`);
|
|
672
|
-
}
|
|
673
|
-
/**
|
|
674
|
-
* Actively reconcile recent mailbox contents via JMAP HTTP.
|
|
675
|
-
* Useful as a safety net when the WebSocket stays "connected"
|
|
676
|
-
* but a notification is missed by an intermediate layer.
|
|
677
|
-
*/
|
|
678
|
-
async reconcileRecentEmails(limit = 20) {
|
|
679
|
-
if (!this.session) {
|
|
680
|
-
this.session = await this.fetchSession();
|
|
681
|
-
}
|
|
682
|
-
const accountId = this.session.primaryAccounts["urn:ietf:params:jmap:mail"] ?? Object.keys(this.session.accounts)[0];
|
|
683
|
-
if (!accountId) {
|
|
684
|
-
throw new Error("No mail account available in JMAP session");
|
|
685
|
-
}
|
|
686
|
-
const queryResp = await this.jmapCall([
|
|
687
|
-
[
|
|
688
|
-
"Email/query",
|
|
689
|
-
{
|
|
690
|
-
accountId,
|
|
691
|
-
sort: [{ property: "receivedAt", isAscending: false }],
|
|
692
|
-
limit
|
|
693
|
-
},
|
|
694
|
-
"qReconcile"
|
|
695
|
-
]
|
|
696
|
-
]);
|
|
697
|
-
const queryResult = queryResp.methodResponses.find(([name]) => name === "Email/query");
|
|
698
|
-
if (!queryResult)
|
|
699
|
-
return 0;
|
|
700
|
-
const ids = (queryResult[1].ids ?? []).slice(0, limit);
|
|
701
|
-
if (ids.length === 0)
|
|
702
|
-
return 0;
|
|
703
|
-
const emailResp = await this.jmapCall([
|
|
704
|
-
[
|
|
705
|
-
"Email/get",
|
|
706
|
-
{
|
|
707
|
-
accountId,
|
|
708
|
-
ids,
|
|
709
|
-
properties: ["id", "subject", "from", "to", "headers", "messageId", "receivedAt", "textBody", "bodyValues", "attachments"],
|
|
710
|
-
fetchTextBodyValues: true,
|
|
711
|
-
maxBodyValueBytes: 262144
|
|
712
|
-
},
|
|
713
|
-
"gReconcile"
|
|
714
|
-
]
|
|
715
|
-
]);
|
|
716
|
-
const getResult = emailResp.methodResponses.find(([name]) => name === "Email/get");
|
|
717
|
-
if (!getResult)
|
|
718
|
-
return 0;
|
|
719
|
-
const emails = getResult[1].list ?? [];
|
|
720
|
-
for (const email of emails.sort((a, b) => {
|
|
721
|
-
const aTs = new Date(a.receivedAt).getTime();
|
|
722
|
-
const bTs = new Date(b.receivedAt).getTime();
|
|
723
|
-
return aTs - bTs;
|
|
724
|
-
})) {
|
|
725
|
-
if (!this.shouldProcessBootstrapEmail(email))
|
|
726
|
-
continue;
|
|
727
|
-
this.processEmail(email);
|
|
728
|
-
}
|
|
729
|
-
return emails.length;
|
|
730
|
-
}
|
|
731
|
-
};
|
|
732
|
-
|
|
733
|
-
// node_modules/aamp-sdk/dist/smtp-sender.js
|
|
734
|
-
import { createTransport } from "nodemailer";
|
|
735
|
-
import { randomUUID } from "crypto";
|
|
736
|
-
var sanitize = (s) => s.replace(/[\r\n]/g, " ").trim();
|
|
737
|
-
var SmtpSender = class {
|
|
738
|
-
config;
|
|
739
|
-
transport;
|
|
740
|
-
constructor(config) {
|
|
741
|
-
this.config = config;
|
|
742
|
-
this.transport = createTransport({
|
|
743
|
-
host: config.host,
|
|
744
|
-
port: config.port,
|
|
745
|
-
secure: config.secure ?? false,
|
|
746
|
-
auth: {
|
|
747
|
-
user: config.user,
|
|
748
|
-
pass: config.password
|
|
749
|
-
},
|
|
750
|
-
tls: {
|
|
751
|
-
rejectUnauthorized: config.rejectUnauthorized ?? true
|
|
752
|
-
}
|
|
753
|
-
});
|
|
754
|
-
}
|
|
755
|
-
senderDomain() {
|
|
756
|
-
return this.config.user.split("@")[1]?.toLowerCase() ?? "";
|
|
757
|
-
}
|
|
758
|
-
recipientDomain(email) {
|
|
759
|
-
return email.split("@")[1]?.toLowerCase() ?? "";
|
|
760
|
-
}
|
|
761
|
-
shouldUseHttpFallback(to) {
|
|
762
|
-
return Boolean(this.config.httpBaseUrl && this.config.authToken && this.senderDomain() && this.senderDomain() === this.recipientDomain(to));
|
|
763
|
-
}
|
|
764
|
-
async sendViaHttp(opts) {
|
|
765
|
-
const base = this.config.httpBaseUrl?.replace(/\/$/, "");
|
|
766
|
-
if (!base || !this.config.authToken) {
|
|
767
|
-
throw new Error("HTTP send fallback is not configured");
|
|
768
|
-
}
|
|
769
|
-
const res = await fetch(`${base}/api/send`, {
|
|
770
|
-
method: "POST",
|
|
771
|
-
headers: {
|
|
772
|
-
Authorization: `Basic ${this.config.authToken}`,
|
|
773
|
-
"Content-Type": "application/json"
|
|
774
|
-
},
|
|
775
|
-
body: JSON.stringify({
|
|
776
|
-
to: opts.to,
|
|
777
|
-
subject: opts.subject,
|
|
778
|
-
text: opts.text,
|
|
779
|
-
aampHeaders: opts.aampHeaders,
|
|
780
|
-
attachments: opts.attachments?.map((a) => ({
|
|
781
|
-
filename: a.filename,
|
|
782
|
-
contentType: a.contentType,
|
|
783
|
-
content: typeof a.content === "string" ? a.content : a.content.toString("base64")
|
|
784
|
-
}))
|
|
785
|
-
})
|
|
786
|
-
});
|
|
787
|
-
const data = await res.json().catch(() => ({}));
|
|
788
|
-
if (!res.ok) {
|
|
789
|
-
throw new Error(data.details || `HTTP send failed: ${res.status}`);
|
|
790
|
-
}
|
|
791
|
-
return { messageId: data.messageId };
|
|
792
|
-
}
|
|
793
|
-
/**
|
|
794
|
-
* Send a task.dispatch email.
|
|
795
|
-
* Returns both the generated taskId and the SMTP Message-ID so callers can
|
|
796
|
-
* store a reverse-index (messageId → taskId) for In-Reply-To thread routing.
|
|
797
|
-
*/
|
|
798
|
-
async sendTask(opts) {
|
|
799
|
-
const taskId = randomUUID();
|
|
800
|
-
const aampHeaders = buildDispatchHeaders({
|
|
801
|
-
taskId,
|
|
802
|
-
timeoutSecs: opts.timeoutSecs,
|
|
803
|
-
contextLinks: opts.contextLinks ?? [],
|
|
804
|
-
parentTaskId: opts.parentTaskId
|
|
805
|
-
});
|
|
806
|
-
const sendMailOpts = {
|
|
807
|
-
from: this.config.user,
|
|
808
|
-
to: opts.to,
|
|
809
|
-
subject: `[AAMP Task] ${sanitize(opts.title)}`,
|
|
810
|
-
text: [
|
|
811
|
-
`Task: ${opts.title}`,
|
|
812
|
-
`Task ID: ${taskId}`,
|
|
813
|
-
opts.timeoutSecs ? `Deadline: ${opts.timeoutSecs}s` : `Deadline: none`,
|
|
814
|
-
opts.contextLinks?.length ? `Context:
|
|
815
|
-
${opts.contextLinks.map((l) => ` ${l}`).join("\n")}` : "",
|
|
816
|
-
``,
|
|
817
|
-
`--- This email was sent by AAMP. Reply directly to submit your result. ---`
|
|
818
|
-
].filter(Boolean).join("\n"),
|
|
819
|
-
headers: aampHeaders
|
|
820
|
-
};
|
|
821
|
-
if (opts.attachments?.length) {
|
|
822
|
-
sendMailOpts.attachments = opts.attachments.map((a) => ({
|
|
823
|
-
filename: a.filename,
|
|
824
|
-
content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content,
|
|
825
|
-
contentType: a.contentType
|
|
826
|
-
}));
|
|
827
|
-
}
|
|
828
|
-
if (this.shouldUseHttpFallback(opts.to)) {
|
|
829
|
-
const info2 = await this.sendViaHttp({
|
|
830
|
-
to: opts.to,
|
|
831
|
-
subject: sendMailOpts.subject,
|
|
832
|
-
text: sendMailOpts.text,
|
|
833
|
-
aampHeaders,
|
|
834
|
-
attachments: opts.attachments?.map((a) => ({
|
|
835
|
-
filename: a.filename,
|
|
836
|
-
contentType: a.contentType,
|
|
837
|
-
content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content
|
|
838
|
-
}))
|
|
839
|
-
});
|
|
840
|
-
return { taskId, messageId: info2.messageId ?? "" };
|
|
841
|
-
}
|
|
842
|
-
const info = await this.transport.sendMail(sendMailOpts);
|
|
843
|
-
return { taskId, messageId: info.messageId ?? "" };
|
|
844
|
-
}
|
|
845
|
-
/**
|
|
846
|
-
* Send a task.result email back to the dispatcher
|
|
847
|
-
*/
|
|
848
|
-
async sendResult(opts) {
|
|
849
|
-
const aampHeaders = buildResultHeaders({
|
|
850
|
-
taskId: opts.taskId,
|
|
851
|
-
status: opts.status,
|
|
852
|
-
output: opts.output,
|
|
853
|
-
errorMsg: opts.errorMsg,
|
|
854
|
-
structuredResult: opts.structuredResult
|
|
855
|
-
});
|
|
856
|
-
const mailOpts = {
|
|
857
|
-
from: this.config.user,
|
|
858
|
-
to: opts.to,
|
|
859
|
-
subject: `[AAMP Result] Task ${opts.taskId} \u2014 ${opts.status}`,
|
|
860
|
-
text: [
|
|
861
|
-
`AAMP Task Result`,
|
|
862
|
-
``,
|
|
863
|
-
`Task ID: ${opts.taskId}`,
|
|
864
|
-
`Status: ${opts.status}`,
|
|
865
|
-
``,
|
|
866
|
-
`Output:`,
|
|
867
|
-
opts.output,
|
|
868
|
-
opts.errorMsg ? `
|
|
869
|
-
Error: ${opts.errorMsg}` : ""
|
|
870
|
-
].filter((s) => s !== "").join("\n"),
|
|
871
|
-
headers: aampHeaders
|
|
872
|
-
};
|
|
873
|
-
if (opts.inReplyTo) {
|
|
874
|
-
mailOpts.inReplyTo = opts.inReplyTo;
|
|
875
|
-
mailOpts.references = opts.inReplyTo;
|
|
876
|
-
}
|
|
877
|
-
if (opts.attachments?.length) {
|
|
878
|
-
mailOpts.attachments = opts.attachments.map((a) => ({
|
|
879
|
-
filename: a.filename,
|
|
880
|
-
content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content,
|
|
881
|
-
contentType: a.contentType
|
|
882
|
-
}));
|
|
883
|
-
}
|
|
884
|
-
if (this.shouldUseHttpFallback(opts.to)) {
|
|
885
|
-
await this.sendViaHttp({
|
|
886
|
-
to: opts.to,
|
|
887
|
-
subject: mailOpts.subject,
|
|
888
|
-
text: mailOpts.text,
|
|
889
|
-
aampHeaders,
|
|
890
|
-
attachments: opts.attachments?.map((a) => ({
|
|
891
|
-
filename: a.filename,
|
|
892
|
-
contentType: a.contentType,
|
|
893
|
-
content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content
|
|
894
|
-
}))
|
|
895
|
-
});
|
|
896
|
-
return;
|
|
897
|
-
}
|
|
898
|
-
await this.transport.sendMail(mailOpts);
|
|
899
|
-
}
|
|
900
|
-
/**
|
|
901
|
-
* Send a task.help email when the agent is blocked
|
|
902
|
-
*/
|
|
903
|
-
async sendHelp(opts) {
|
|
904
|
-
const aampHeaders = buildHelpHeaders({
|
|
905
|
-
taskId: opts.taskId,
|
|
906
|
-
question: opts.question,
|
|
907
|
-
blockedReason: opts.blockedReason,
|
|
908
|
-
suggestedOptions: opts.suggestedOptions
|
|
909
|
-
});
|
|
910
|
-
const helpMailOpts = {
|
|
911
|
-
from: this.config.user,
|
|
912
|
-
to: opts.to,
|
|
913
|
-
subject: `[AAMP Help] Task ${opts.taskId} needs assistance`,
|
|
914
|
-
text: [
|
|
915
|
-
`AAMP Task Help Request`,
|
|
916
|
-
``,
|
|
917
|
-
`Task ID: ${opts.taskId}`,
|
|
918
|
-
``,
|
|
919
|
-
`Question: ${opts.question}`,
|
|
920
|
-
``,
|
|
921
|
-
`Blocked reason: ${opts.blockedReason}`,
|
|
922
|
-
``,
|
|
923
|
-
opts.suggestedOptions.length ? `Suggested options:
|
|
924
|
-
${opts.suggestedOptions.map((o, i) => ` ${i + 1}. ${o}`).join("\n")}` : ""
|
|
925
|
-
].filter(Boolean).join("\n"),
|
|
926
|
-
headers: aampHeaders
|
|
927
|
-
};
|
|
928
|
-
if (opts.inReplyTo) {
|
|
929
|
-
helpMailOpts.inReplyTo = opts.inReplyTo;
|
|
930
|
-
helpMailOpts.references = opts.inReplyTo;
|
|
931
|
-
}
|
|
932
|
-
if (opts.attachments?.length) {
|
|
933
|
-
helpMailOpts.attachments = opts.attachments.map((a) => ({
|
|
934
|
-
filename: a.filename,
|
|
935
|
-
content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content,
|
|
936
|
-
contentType: a.contentType
|
|
937
|
-
}));
|
|
938
|
-
}
|
|
939
|
-
if (this.shouldUseHttpFallback(opts.to)) {
|
|
940
|
-
await this.sendViaHttp({
|
|
941
|
-
to: opts.to,
|
|
942
|
-
subject: helpMailOpts.subject,
|
|
943
|
-
text: helpMailOpts.text,
|
|
944
|
-
aampHeaders,
|
|
945
|
-
attachments: opts.attachments?.map((a) => ({
|
|
946
|
-
filename: a.filename,
|
|
947
|
-
contentType: a.contentType,
|
|
948
|
-
content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content
|
|
949
|
-
}))
|
|
950
|
-
});
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
await this.transport.sendMail(helpMailOpts);
|
|
954
|
-
}
|
|
955
|
-
/**
|
|
956
|
-
* Send a task.ack email to confirm receipt of a dispatch
|
|
957
|
-
*/
|
|
958
|
-
async sendAck(opts) {
|
|
959
|
-
const aampHeaders = buildAckHeaders({ taskId: opts.taskId });
|
|
960
|
-
const mailOpts = {
|
|
961
|
-
from: this.config.user,
|
|
962
|
-
to: opts.to,
|
|
963
|
-
subject: `[AAMP ACK] Task ${opts.taskId}`,
|
|
964
|
-
text: "",
|
|
965
|
-
headers: aampHeaders
|
|
966
|
-
};
|
|
967
|
-
if (opts.inReplyTo) {
|
|
968
|
-
mailOpts.inReplyTo = opts.inReplyTo;
|
|
969
|
-
mailOpts.references = opts.inReplyTo;
|
|
970
|
-
}
|
|
971
|
-
if (this.shouldUseHttpFallback(opts.to)) {
|
|
972
|
-
await this.sendViaHttp({
|
|
973
|
-
to: opts.to,
|
|
974
|
-
subject: mailOpts.subject,
|
|
975
|
-
text: mailOpts.text,
|
|
976
|
-
aampHeaders
|
|
977
|
-
});
|
|
978
|
-
return;
|
|
979
|
-
}
|
|
980
|
-
await this.transport.sendMail(mailOpts);
|
|
981
|
-
}
|
|
982
|
-
/**
|
|
983
|
-
* Verify SMTP connection
|
|
984
|
-
*/
|
|
985
|
-
async verify() {
|
|
986
|
-
try {
|
|
987
|
-
await this.transport.verify();
|
|
988
|
-
return true;
|
|
989
|
-
} catch {
|
|
990
|
-
return false;
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
close() {
|
|
994
|
-
this.transport.close();
|
|
995
|
-
}
|
|
996
|
-
};
|
|
997
|
-
|
|
998
|
-
// node_modules/aamp-sdk/dist/client.js
|
|
999
|
-
var AampClient = class extends EventEmitter2 {
|
|
1000
|
-
jmapClient;
|
|
1001
|
-
smtpSender;
|
|
1002
|
-
config;
|
|
1003
|
-
constructor(config) {
|
|
1004
|
-
super();
|
|
1005
|
-
this.config = config;
|
|
1006
|
-
let password;
|
|
1007
|
-
try {
|
|
1008
|
-
const decoded = Buffer.from(config.jmapToken, "base64").toString("utf-8");
|
|
1009
|
-
const colonIdx = decoded.indexOf(":");
|
|
1010
|
-
if (colonIdx < 0)
|
|
1011
|
-
throw new Error("Invalid jmapToken format: expected base64(email:password)");
|
|
1012
|
-
password = decoded.slice(colonIdx + 1);
|
|
1013
|
-
if (!password)
|
|
1014
|
-
throw new Error("Invalid jmapToken: empty password");
|
|
1015
|
-
} catch (err) {
|
|
1016
|
-
if (err instanceof Error && err.message.startsWith("Invalid jmapToken"))
|
|
1017
|
-
throw err;
|
|
1018
|
-
throw new Error(`Failed to decode jmapToken: ${err.message}`);
|
|
1019
|
-
}
|
|
1020
|
-
this.jmapClient = new JmapPushClient({
|
|
1021
|
-
email: config.email,
|
|
1022
|
-
password: password ?? config.smtpPassword,
|
|
1023
|
-
jmapUrl: config.jmapUrl,
|
|
1024
|
-
reconnectInterval: config.reconnectInterval ?? 5e3,
|
|
1025
|
-
rejectUnauthorized: config.rejectUnauthorized
|
|
1026
|
-
});
|
|
1027
|
-
this.smtpSender = new SmtpSender({
|
|
1028
|
-
host: config.smtpHost,
|
|
1029
|
-
port: config.smtpPort ?? 587,
|
|
1030
|
-
user: config.email,
|
|
1031
|
-
password: config.smtpPassword,
|
|
1032
|
-
httpBaseUrl: config.httpSendBaseUrl ?? config.jmapUrl,
|
|
1033
|
-
authToken: config.jmapToken,
|
|
1034
|
-
rejectUnauthorized: config.rejectUnauthorized
|
|
1035
|
-
});
|
|
1036
|
-
this.jmapClient.on("task.dispatch", (task) => {
|
|
1037
|
-
this.emit("task.dispatch", task);
|
|
1038
|
-
});
|
|
1039
|
-
this.jmapClient.on("task.result", (result) => {
|
|
1040
|
-
this.emit("task.result", result);
|
|
1041
|
-
});
|
|
1042
|
-
this.jmapClient.on("task.help", (help) => {
|
|
1043
|
-
this.emit("task.help", help);
|
|
1044
|
-
});
|
|
1045
|
-
this.jmapClient.on("task.ack", (ack) => {
|
|
1046
|
-
this.emit("task.ack", ack);
|
|
1047
|
-
});
|
|
1048
|
-
this.jmapClient.on("_autoAck", async ({ to, taskId, messageId }) => {
|
|
1049
|
-
try {
|
|
1050
|
-
await this.smtpSender.sendAck({ to, taskId, inReplyTo: messageId });
|
|
1051
|
-
} catch (err) {
|
|
1052
|
-
console.warn(`[AAMP] Failed to send ACK for task ${taskId}: ${err.message}`);
|
|
1053
|
-
}
|
|
1054
|
-
});
|
|
1055
|
-
this.jmapClient.on("reply", (reply) => {
|
|
1056
|
-
this.emit("reply", reply);
|
|
1057
|
-
});
|
|
1058
|
-
this.jmapClient.on("connected", () => {
|
|
1059
|
-
this.emit("connected");
|
|
1060
|
-
});
|
|
1061
|
-
this.jmapClient.on("disconnected", (reason) => {
|
|
1062
|
-
this.emit("disconnected", reason);
|
|
1063
|
-
});
|
|
1064
|
-
this.jmapClient.on("error", (err) => {
|
|
1065
|
-
this.emit("error", err);
|
|
1066
|
-
});
|
|
1067
|
-
}
|
|
1068
|
-
// =====================================================
|
|
1069
|
-
// Type-safe event emitter methods
|
|
1070
|
-
// =====================================================
|
|
1071
|
-
on(event, listener) {
|
|
1072
|
-
return super.on(event, listener);
|
|
1073
|
-
}
|
|
1074
|
-
once(event, listener) {
|
|
1075
|
-
return super.once(event, listener);
|
|
1076
|
-
}
|
|
1077
|
-
off(event, listener) {
|
|
1078
|
-
return super.off(event, listener);
|
|
1079
|
-
}
|
|
1080
|
-
// =====================================================
|
|
1081
|
-
// Lifecycle
|
|
1082
|
-
// =====================================================
|
|
1083
|
-
/**
|
|
1084
|
-
* Connect to JMAP and start listening for tasks
|
|
1085
|
-
*/
|
|
1086
|
-
async connect() {
|
|
1087
|
-
await this.jmapClient.start();
|
|
1088
|
-
}
|
|
1089
|
-
/**
|
|
1090
|
-
* Disconnect and clean up
|
|
1091
|
-
*/
|
|
1092
|
-
disconnect() {
|
|
1093
|
-
this.jmapClient.stop();
|
|
1094
|
-
this.smtpSender.close();
|
|
1095
|
-
}
|
|
1096
|
-
/**
|
|
1097
|
-
* Returns true if the JMAP connection is active
|
|
1098
|
-
*/
|
|
1099
|
-
isConnected() {
|
|
1100
|
-
return this.jmapClient.isConnected();
|
|
1101
|
-
}
|
|
1102
|
-
isUsingPollingFallback() {
|
|
1103
|
-
return this.jmapClient.isUsingPollingFallback();
|
|
1104
|
-
}
|
|
1105
|
-
// =====================================================
|
|
1106
|
-
// Sending
|
|
1107
|
-
// =====================================================
|
|
1108
|
-
/**
|
|
1109
|
-
* Send a task.dispatch email to an agent.
|
|
1110
|
-
* Returns the generated taskId and the SMTP Message-ID.
|
|
1111
|
-
* Store messageId → taskId in Redis/DB to support In-Reply-To thread routing
|
|
1112
|
-
* for human replies that arrive without X-AAMP headers.
|
|
1113
|
-
*/
|
|
1114
|
-
async sendTask(opts) {
|
|
1115
|
-
return this.smtpSender.sendTask(opts);
|
|
1116
|
-
}
|
|
1117
|
-
/**
|
|
1118
|
-
* Send a task.result email (agent → system/dispatcher)
|
|
1119
|
-
*/
|
|
1120
|
-
async sendResult(opts) {
|
|
1121
|
-
return this.smtpSender.sendResult(opts);
|
|
1122
|
-
}
|
|
1123
|
-
/**
|
|
1124
|
-
* Send a task.help email when the agent needs human assistance
|
|
1125
|
-
*/
|
|
1126
|
-
async sendHelp(opts) {
|
|
1127
|
-
return this.smtpSender.sendHelp(opts);
|
|
1128
|
-
}
|
|
1129
|
-
/**
|
|
1130
|
-
* Download a blob (attachment) by its JMAP blobId.
|
|
1131
|
-
* Use this to retrieve attachment content from received TaskDispatch or TaskResult messages.
|
|
1132
|
-
* Returns the raw binary content as a Buffer.
|
|
1133
|
-
*/
|
|
1134
|
-
async downloadBlob(blobId, filename) {
|
|
1135
|
-
return this.jmapClient.downloadBlob(blobId, filename);
|
|
1136
|
-
}
|
|
1137
|
-
/**
|
|
1138
|
-
* Reconcile recent mailbox contents via JMAP HTTP to catch messages missed by
|
|
1139
|
-
* a flaky WebSocket path. Safe to call periodically; duplicate processing is
|
|
1140
|
-
* suppressed by the JMAP push client.
|
|
1141
|
-
*/
|
|
1142
|
-
async reconcileRecentEmails(limit) {
|
|
1143
|
-
return this.jmapClient.reconcileRecentEmails(limit);
|
|
1144
|
-
}
|
|
1145
|
-
/**
|
|
1146
|
-
* Verify SMTP connectivity
|
|
1147
|
-
*/
|
|
1148
|
-
async verifySmtp() {
|
|
1149
|
-
return this.smtpSender.verify();
|
|
1150
|
-
}
|
|
1151
|
-
get email() {
|
|
1152
|
-
return this.config.email;
|
|
1153
|
-
}
|
|
1154
|
-
};
|
|
1155
|
-
|
|
1156
|
-
// src/index.ts
|
|
1
|
+
import { AampClient } from "aamp-sdk";
|
|
2
|
+
import {
|
|
3
|
+
defaultCredentialsPath,
|
|
4
|
+
ensureDir,
|
|
5
|
+
loadCachedIdentity,
|
|
6
|
+
readBinaryFile,
|
|
7
|
+
saveCachedIdentity,
|
|
8
|
+
writeBinaryFile
|
|
9
|
+
} from "./file-store.js";
|
|
1157
10
|
function baseUrl(aampHost) {
|
|
1158
11
|
if (aampHost.startsWith("http://") || aampHost.startsWith("https://")) {
|
|
1159
12
|
return aampHost.replace(/\/$/, "");
|
|
1160
13
|
}
|
|
1161
14
|
return `https://${aampHost}`;
|
|
1162
15
|
}
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
return join(homedir(), ".openclaw", "extensions", "aamp", ".credentials.json");
|
|
1177
|
-
}
|
|
1178
|
-
function loadCachedIdentity(cfg) {
|
|
1179
|
-
const file = cfg.credentialsFile ?? defaultCredentialsPath();
|
|
1180
|
-
if (!existsSync(file))
|
|
1181
|
-
return null;
|
|
1182
|
-
try {
|
|
1183
|
-
const parsed = JSON.parse(readFileSync(file, "utf-8"));
|
|
1184
|
-
if (!parsed.email || !parsed.jmapToken || !parsed.smtpPassword) {
|
|
1185
|
-
return null;
|
|
1186
|
-
}
|
|
1187
|
-
return parsed;
|
|
1188
|
-
} catch {
|
|
1189
|
-
return null;
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
function saveCachedIdentity(cfg, identity) {
|
|
1193
|
-
const file = cfg.credentialsFile ?? defaultCredentialsPath();
|
|
1194
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
1195
|
-
writeFileSync(file, JSON.stringify(identity, null, 2), "utf-8");
|
|
1196
|
-
}
|
|
16
|
+
const pendingTasks = /* @__PURE__ */ new Map();
|
|
17
|
+
const dispatchedSubtasks = /* @__PURE__ */ new Map();
|
|
18
|
+
const shownNotifications = /* @__PURE__ */ new Set();
|
|
19
|
+
const waitingDispatches = /* @__PURE__ */ new Map();
|
|
20
|
+
let aampClient = null;
|
|
21
|
+
let agentEmail = "";
|
|
22
|
+
let lastConnectionError = "";
|
|
23
|
+
let lastDisconnectReason = "";
|
|
24
|
+
let lastTransportMode = "disconnected";
|
|
25
|
+
let reconcileTimer = null;
|
|
26
|
+
let currentSessionKey = "agent:main:main";
|
|
27
|
+
let channelRuntime = null;
|
|
28
|
+
let channelCfg = null;
|
|
1197
29
|
async function registerNode(cfg) {
|
|
1198
30
|
const slug = (cfg.slug ?? "openclaw-agent").toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
1199
31
|
const base = baseUrl(cfg.aampHost);
|
|
@@ -1222,15 +54,15 @@ async function registerNode(cfg) {
|
|
|
1222
54
|
};
|
|
1223
55
|
}
|
|
1224
56
|
async function resolveIdentity(cfg) {
|
|
1225
|
-
const cached = loadCachedIdentity(cfg);
|
|
57
|
+
const cached = loadCachedIdentity(cfg.credentialsFile ?? defaultCredentialsPath());
|
|
1226
58
|
if (cached)
|
|
1227
59
|
return cached;
|
|
1228
60
|
const identity = await registerNode(cfg);
|
|
1229
|
-
saveCachedIdentity(
|
|
61
|
+
saveCachedIdentity(identity, cfg.credentialsFile ?? defaultCredentialsPath());
|
|
1230
62
|
return identity;
|
|
1231
63
|
}
|
|
1232
64
|
var src_default = {
|
|
1233
|
-
id: "aamp",
|
|
65
|
+
id: "aamp-openclaw-plugin",
|
|
1234
66
|
name: "AAMP Agent Mail Protocol",
|
|
1235
67
|
configSchema: {
|
|
1236
68
|
type: "object",
|
|
@@ -1246,7 +78,7 @@ var src_default = {
|
|
|
1246
78
|
},
|
|
1247
79
|
credentialsFile: {
|
|
1248
80
|
type: "string",
|
|
1249
|
-
description: "Absolute path to cache AAMP credentials between gateway restarts. Default: ~/.openclaw/extensions/aamp/.credentials.json. Delete this file to force re-registration with a new mailbox."
|
|
81
|
+
description: "Absolute path to cache AAMP credentials between gateway restarts. Default: ~/.openclaw/extensions/aamp-openclaw-plugin/.credentials.json. Delete this file to force re-registration with a new mailbox."
|
|
1250
82
|
},
|
|
1251
83
|
allowedSenders: {
|
|
1252
84
|
type: "array",
|
|
@@ -1266,7 +98,7 @@ var src_default = {
|
|
|
1266
98
|
listAccountIds: () => cfg.aampHost ? ["default"] : [],
|
|
1267
99
|
resolveAccount: () => ({ aampHost: cfg.aampHost }),
|
|
1268
100
|
isEnabled: () => !!cfg.aampHost,
|
|
1269
|
-
isConfigured: () => !!loadCachedIdentity(cfg)
|
|
101
|
+
isConfigured: () => !!loadCachedIdentity(cfg.credentialsFile ?? defaultCredentialsPath())
|
|
1270
102
|
},
|
|
1271
103
|
gateway: {
|
|
1272
104
|
startAccount: async (ctx) => {
|
|
@@ -1357,14 +189,13 @@ var src_default = {
|
|
|
1357
189
|
const downloadPromise = (async () => {
|
|
1358
190
|
if (!result.attachments?.length)
|
|
1359
191
|
return;
|
|
1360
|
-
const { mkdirSync: mkdirSync2, writeFileSync: writeFileSync2 } = await import("node:fs");
|
|
1361
192
|
const dir = "/tmp/aamp-files";
|
|
1362
|
-
|
|
193
|
+
ensureDir(dir);
|
|
1363
194
|
for (const att of result.attachments) {
|
|
1364
195
|
try {
|
|
1365
196
|
const buffer = await aampClient.downloadBlob(att.blobId, att.filename);
|
|
1366
197
|
const filepath = `${dir}/${att.filename}`;
|
|
1367
|
-
|
|
198
|
+
writeBinaryFile(filepath, buffer);
|
|
1368
199
|
downloadedFiles.push({ filename: att.filename, path: filepath, size: buffer.length });
|
|
1369
200
|
api.logger.info(`[AAMP] Pre-downloaded: ${att.filename} (${(buffer.length / 1024).toFixed(1)} KB) \u2192 ${filepath}`);
|
|
1370
201
|
} catch (dlErr) {
|
|
@@ -1578,7 +409,7 @@ ${notifyBody?.bodyText ?? help.question}`;
|
|
|
1578
409
|
}
|
|
1579
410
|
api.registerTool({
|
|
1580
411
|
name: "aamp_connect",
|
|
1581
|
-
description: "Connect this agent to the AAMP service. Call this once at the start of a session to obtain an email identity and begin receiving AAMP tasks via JMAP WebSocket Push. Credentials are cached to disk so the same mailbox address is reused after a gateway restart. Delete the credentials file (default: ~/.openclaw/extensions/aamp/.credentials.json) to get a fresh identity.",
|
|
412
|
+
description: "Connect this agent to the AAMP service. Call this once at the start of a session to obtain an email identity and begin receiving AAMP tasks via JMAP WebSocket Push. Credentials are cached to disk so the same mailbox address is reused after a gateway restart. Delete the credentials file (default: ~/.openclaw/extensions/aamp-openclaw-plugin/.credentials.json) to get a fresh identity.",
|
|
1582
413
|
parameters: {
|
|
1583
414
|
type: "object",
|
|
1584
415
|
properties: {
|
|
@@ -1630,7 +461,7 @@ ${notifyBody?.bodyText ?? help.question}`;
|
|
|
1630
461
|
api.logger.info("[AAMP] aampHost not configured \u2014 skipping auto-connect");
|
|
1631
462
|
return;
|
|
1632
463
|
}
|
|
1633
|
-
const cached = loadCachedIdentity(cfg);
|
|
464
|
+
const cached = loadCachedIdentity(cfg.credentialsFile ?? defaultCredentialsPath());
|
|
1634
465
|
if (!cached) {
|
|
1635
466
|
api.logger.info("[AAMP] No cached credentials \u2014 call aamp_connect to register");
|
|
1636
467
|
return;
|
|
@@ -1860,11 +691,10 @@ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
|
|
|
1860
691
|
})}`);
|
|
1861
692
|
let attachments;
|
|
1862
693
|
if (p.attachments?.length) {
|
|
1863
|
-
const { readFileSync: readFileSync2 } = await import("node:fs");
|
|
1864
694
|
attachments = p.attachments.map((a) => ({
|
|
1865
695
|
filename: a.filename,
|
|
1866
696
|
contentType: a.contentType ?? "application/octet-stream",
|
|
1867
|
-
content:
|
|
697
|
+
content: readBinaryFile(a.path)
|
|
1868
698
|
}));
|
|
1869
699
|
}
|
|
1870
700
|
await aampClient.sendResult({
|
|
@@ -2013,11 +843,10 @@ ${lines.join("\n")}`
|
|
|
2013
843
|
try {
|
|
2014
844
|
let attachments;
|
|
2015
845
|
if (params.attachments?.length) {
|
|
2016
|
-
const { readFileSync: readFileSync2 } = await import("node:fs");
|
|
2017
846
|
attachments = params.attachments.map((a) => ({
|
|
2018
847
|
filename: a.filename,
|
|
2019
848
|
contentType: a.contentType ?? "application/octet-stream",
|
|
2020
|
-
content:
|
|
849
|
+
content: readBinaryFile(a.path)
|
|
2021
850
|
}));
|
|
2022
851
|
}
|
|
2023
852
|
const result = await aampClient.sendTask({
|
|
@@ -2050,12 +879,11 @@ ${lines.join("\n")}`
|
|
|
2050
879
|
let attachmentLines = "";
|
|
2051
880
|
if (r.attachments?.length) {
|
|
2052
881
|
api.logger.info(`[AAMP] Downloading ${r.attachments.length} attachment(s) from sync reply...`);
|
|
2053
|
-
const { mkdirSync: mkdirSync2, writeFileSync: writeFileSync2 } = await import("node:fs");
|
|
2054
882
|
const dir = "/tmp/aamp-files";
|
|
2055
|
-
|
|
883
|
+
ensureDir(dir);
|
|
2056
884
|
const downloaded = [];
|
|
2057
885
|
const base = baseUrl(cfg.aampHost);
|
|
2058
|
-
const identity = loadCachedIdentity(cfg);
|
|
886
|
+
const identity = loadCachedIdentity(cfg.credentialsFile ?? defaultCredentialsPath());
|
|
2059
887
|
const authHeader = identity ? `Basic ${Buffer.from(identity.email + ":" + identity.smtpPassword).toString("base64")}` : "";
|
|
2060
888
|
for (const att of r.attachments) {
|
|
2061
889
|
try {
|
|
@@ -2066,7 +894,7 @@ ${lines.join("\n")}`
|
|
|
2066
894
|
throw new Error(`HTTP ${dlRes.status}`);
|
|
2067
895
|
const buffer = Buffer.from(await dlRes.arrayBuffer());
|
|
2068
896
|
const filepath = `${dir}/${att.filename}`;
|
|
2069
|
-
|
|
897
|
+
writeBinaryFile(filepath, buffer);
|
|
2070
898
|
downloaded.push(`${att.filename} (${(buffer.length / 1024).toFixed(1)} KB) \u2192 ${filepath}`);
|
|
2071
899
|
api.logger.info(`[AAMP] Downloaded: ${att.filename} (${(buffer.length / 1024).toFixed(1)} KB)`);
|
|
2072
900
|
} catch (dlErr) {
|
|
@@ -2170,12 +998,11 @@ Question: ${h.question}`,
|
|
|
2170
998
|
return { content: [{ type: "text", text: "Error: AAMP client is not connected." }] };
|
|
2171
999
|
}
|
|
2172
1000
|
const dir = params.saveTo ?? "/tmp/aamp-files";
|
|
2173
|
-
|
|
2174
|
-
mkdirSync2(dir, { recursive: true });
|
|
1001
|
+
ensureDir(dir);
|
|
2175
1002
|
try {
|
|
2176
1003
|
const buffer = await aampClient.downloadBlob(params.blobId, params.filename);
|
|
2177
1004
|
const filepath = `${dir}/${params.filename}`;
|
|
2178
|
-
|
|
1005
|
+
writeBinaryFile(filepath, buffer);
|
|
2179
1006
|
return {
|
|
2180
1007
|
content: [{
|
|
2181
1008
|
type: "text",
|
|
@@ -2203,7 +1030,7 @@ Question: ${h.question}`,
|
|
|
2203
1030
|
`Host: ${cfg.aampHost || "(not configured)"}`,
|
|
2204
1031
|
`Identity: ${agentEmail || "(not yet registered)"}`,
|
|
2205
1032
|
`Connection: ${connectionLine}`,
|
|
2206
|
-
`Cached: ${loadCachedIdentity(cfg) ? "yes" : "no"}`,
|
|
1033
|
+
`Cached: ${loadCachedIdentity(cfg.credentialsFile ?? defaultCredentialsPath()) ? "yes" : "no"}`,
|
|
2207
1034
|
lastConnectionError ? `Last error: ${lastConnectionError}` : "",
|
|
2208
1035
|
lastDisconnectReason ? `Last disconnect: ${lastDisconnectReason}` : "",
|
|
2209
1036
|
`Pending: ${pendingTasks.size} task(s)`,
|