@agenticmail/enterprise 0.5.151 → 0.5.153
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/dist/agenticmail-E4JLFCFR.js +19 -0
- package/dist/chunk-VEPWCYAL.js +1131 -0
- package/dist/cli-agent-7TWEYMAP.js +837 -0
- package/dist/cli-agent-F75N6PFL.js +836 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/agenticmail/providers/google.ts +6 -1
- package/src/cli-agent.ts +14 -5
|
@@ -0,0 +1,1131 @@
|
|
|
1
|
+
// src/agenticmail/providers/microsoft.ts
|
|
2
|
+
var GRAPH_BASE = "https://graph.microsoft.com/v1.0";
|
|
3
|
+
var MicrosoftEmailProvider = class {
|
|
4
|
+
provider = "microsoft";
|
|
5
|
+
identity = null;
|
|
6
|
+
get token() {
|
|
7
|
+
if (!this.identity) throw new Error("Not connected");
|
|
8
|
+
return this.identity.accessToken;
|
|
9
|
+
}
|
|
10
|
+
async refreshIfNeeded() {
|
|
11
|
+
if (this.identity?.refreshToken) {
|
|
12
|
+
this.identity.accessToken = await this.identity.refreshToken();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async graphFetch(path, opts) {
|
|
16
|
+
await this.refreshIfNeeded();
|
|
17
|
+
const res = await fetch(`${GRAPH_BASE}${path}`, {
|
|
18
|
+
...opts,
|
|
19
|
+
headers: {
|
|
20
|
+
Authorization: `Bearer ${this.token}`,
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
...opts?.headers
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
const text = await res.text().catch(() => "");
|
|
27
|
+
throw new Error(`Graph API ${res.status}: ${text}`);
|
|
28
|
+
}
|
|
29
|
+
if (res.status === 204) return null;
|
|
30
|
+
return res.json();
|
|
31
|
+
}
|
|
32
|
+
// ─── Connection ─────────────────────────────────────
|
|
33
|
+
async connect(identity) {
|
|
34
|
+
this.identity = identity;
|
|
35
|
+
await this.graphFetch("/me?$select=mail,displayName");
|
|
36
|
+
}
|
|
37
|
+
async disconnect() {
|
|
38
|
+
this.identity = null;
|
|
39
|
+
}
|
|
40
|
+
// ─── List / Read ────────────────────────────────────
|
|
41
|
+
async listMessages(folder, opts) {
|
|
42
|
+
const folderId = this.resolveFolderId(folder);
|
|
43
|
+
const top = opts?.limit || 20;
|
|
44
|
+
const skip = opts?.offset || 0;
|
|
45
|
+
const data = await this.graphFetch(
|
|
46
|
+
`/me/mailFolders/${folderId}/messages?$top=${top}&$skip=${skip}&$select=id,subject,from,toRecipients,receivedDateTime,isRead,flag,hasAttachments,bodyPreview&$orderby=receivedDateTime desc`
|
|
47
|
+
);
|
|
48
|
+
return (data.value || []).map((m) => this.toEnvelope(m));
|
|
49
|
+
}
|
|
50
|
+
async readMessage(uid) {
|
|
51
|
+
const data = await this.graphFetch(`/me/messages/${uid}?$select=id,subject,from,toRecipients,ccRecipients,bccRecipients,receivedDateTime,isRead,flag,hasAttachments,body,bodyPreview,replyTo,internetMessageId,internetMessageHeaders,conversationId`);
|
|
52
|
+
return this.toMessage(data);
|
|
53
|
+
}
|
|
54
|
+
async searchMessages(criteria) {
|
|
55
|
+
const filters = [];
|
|
56
|
+
if (criteria.from) filters.push(`from/emailAddress/address eq '${criteria.from}'`);
|
|
57
|
+
if (criteria.subject) filters.push(`contains(subject, '${criteria.subject}')`);
|
|
58
|
+
if (criteria.since) filters.push(`receivedDateTime ge ${criteria.since}`);
|
|
59
|
+
if (criteria.before) filters.push(`receivedDateTime lt ${criteria.before}`);
|
|
60
|
+
if (criteria.seen !== void 0) filters.push(`isRead eq ${criteria.seen}`);
|
|
61
|
+
let path = "/me/messages?$top=50&$select=id,subject,from,toRecipients,receivedDateTime,isRead,flag,hasAttachments,bodyPreview&$orderby=receivedDateTime desc";
|
|
62
|
+
if (filters.length) path += "&$filter=" + encodeURIComponent(filters.join(" and "));
|
|
63
|
+
if (criteria.text) path = `/me/messages?$search="${encodeURIComponent(criteria.text)}"&$top=50&$select=id,subject,from,toRecipients,receivedDateTime,isRead,flag,hasAttachments,bodyPreview`;
|
|
64
|
+
const data = await this.graphFetch(path);
|
|
65
|
+
return (data.value || []).map((m) => this.toEnvelope(m));
|
|
66
|
+
}
|
|
67
|
+
async listFolders() {
|
|
68
|
+
const data = await this.graphFetch("/me/mailFolders?$select=id,displayName,unreadItemCount,totalItemCount");
|
|
69
|
+
return (data.value || []).map((f) => ({
|
|
70
|
+
name: f.displayName,
|
|
71
|
+
path: f.id,
|
|
72
|
+
unread: f.unreadItemCount || 0,
|
|
73
|
+
total: f.totalItemCount || 0
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
async createFolder(name) {
|
|
77
|
+
await this.graphFetch("/me/mailFolders", {
|
|
78
|
+
method: "POST",
|
|
79
|
+
body: JSON.stringify({ displayName: name })
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// ─── Send ───────────────────────────────────────────
|
|
83
|
+
async send(options) {
|
|
84
|
+
const message = this.buildGraphMessage(options);
|
|
85
|
+
await this.graphFetch("/me/sendMail", {
|
|
86
|
+
method: "POST",
|
|
87
|
+
body: JSON.stringify({ message, saveToSentItems: true })
|
|
88
|
+
});
|
|
89
|
+
return { messageId: `graph-${Date.now()}` };
|
|
90
|
+
}
|
|
91
|
+
async reply(uid, body, replyAll = false) {
|
|
92
|
+
const endpoint = replyAll ? "replyAll" : "reply";
|
|
93
|
+
await this.graphFetch(`/me/messages/${uid}/${endpoint}`, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
body: JSON.stringify({ comment: body })
|
|
96
|
+
});
|
|
97
|
+
return { messageId: `graph-reply-${Date.now()}` };
|
|
98
|
+
}
|
|
99
|
+
async forward(uid, to, body) {
|
|
100
|
+
await this.graphFetch(`/me/messages/${uid}/forward`, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
body: JSON.stringify({
|
|
103
|
+
comment: body || "",
|
|
104
|
+
toRecipients: [{ emailAddress: { address: to } }]
|
|
105
|
+
})
|
|
106
|
+
});
|
|
107
|
+
return { messageId: `graph-fwd-${Date.now()}` };
|
|
108
|
+
}
|
|
109
|
+
// ─── Organize ───────────────────────────────────────
|
|
110
|
+
async moveMessage(uid, toFolder) {
|
|
111
|
+
const folderId = this.resolveFolderId(toFolder);
|
|
112
|
+
await this.graphFetch(`/me/messages/${uid}/move`, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
body: JSON.stringify({ destinationId: folderId })
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async deleteMessage(uid) {
|
|
118
|
+
await this.graphFetch(`/me/messages/${uid}`, { method: "DELETE" });
|
|
119
|
+
}
|
|
120
|
+
async markRead(uid) {
|
|
121
|
+
await this.graphFetch(`/me/messages/${uid}`, {
|
|
122
|
+
method: "PATCH",
|
|
123
|
+
body: JSON.stringify({ isRead: true })
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async markUnread(uid) {
|
|
127
|
+
await this.graphFetch(`/me/messages/${uid}`, {
|
|
128
|
+
method: "PATCH",
|
|
129
|
+
body: JSON.stringify({ isRead: false })
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async flagMessage(uid) {
|
|
133
|
+
await this.graphFetch(`/me/messages/${uid}`, {
|
|
134
|
+
method: "PATCH",
|
|
135
|
+
body: JSON.stringify({ flag: { flagStatus: "flagged" } })
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
async unflagMessage(uid) {
|
|
139
|
+
await this.graphFetch(`/me/messages/${uid}`, {
|
|
140
|
+
method: "PATCH",
|
|
141
|
+
body: JSON.stringify({ flag: { flagStatus: "notFlagged" } })
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
// ─── Batch ──────────────────────────────────────────
|
|
145
|
+
async batchMarkRead(uids) {
|
|
146
|
+
await Promise.all(uids.map((uid) => this.markRead(uid)));
|
|
147
|
+
}
|
|
148
|
+
async batchMarkUnread(uids) {
|
|
149
|
+
await Promise.all(uids.map((uid) => this.markUnread(uid)));
|
|
150
|
+
}
|
|
151
|
+
async batchMove(uids, toFolder) {
|
|
152
|
+
await Promise.all(uids.map((uid) => this.moveMessage(uid, toFolder)));
|
|
153
|
+
}
|
|
154
|
+
async batchDelete(uids) {
|
|
155
|
+
await Promise.all(uids.map((uid) => this.deleteMessage(uid)));
|
|
156
|
+
}
|
|
157
|
+
// ─── Helpers ────────────────────────────────────────
|
|
158
|
+
resolveFolderId(folder) {
|
|
159
|
+
const map = {
|
|
160
|
+
INBOX: "inbox",
|
|
161
|
+
inbox: "inbox",
|
|
162
|
+
Sent: "sentItems",
|
|
163
|
+
sent: "sentItems",
|
|
164
|
+
sentitems: "sentItems",
|
|
165
|
+
Drafts: "drafts",
|
|
166
|
+
drafts: "drafts",
|
|
167
|
+
Trash: "deletedItems",
|
|
168
|
+
trash: "deletedItems",
|
|
169
|
+
deleteditems: "deletedItems",
|
|
170
|
+
Junk: "junkemail",
|
|
171
|
+
junk: "junkemail",
|
|
172
|
+
spam: "junkemail",
|
|
173
|
+
Archive: "archive",
|
|
174
|
+
archive: "archive"
|
|
175
|
+
};
|
|
176
|
+
return map[folder] || folder;
|
|
177
|
+
}
|
|
178
|
+
buildGraphMessage(options) {
|
|
179
|
+
const msg = {
|
|
180
|
+
subject: options.subject,
|
|
181
|
+
body: { contentType: options.html ? "HTML" : "Text", content: options.html || options.body },
|
|
182
|
+
toRecipients: options.to.split(",").map((e) => ({ emailAddress: { address: e.trim() } }))
|
|
183
|
+
};
|
|
184
|
+
if (options.cc) msg.ccRecipients = options.cc.split(",").map((e) => ({ emailAddress: { address: e.trim() } }));
|
|
185
|
+
if (options.bcc) msg.bccRecipients = options.bcc.split(",").map((e) => ({ emailAddress: { address: e.trim() } }));
|
|
186
|
+
if (options.inReplyTo) msg.internetMessageHeaders = [{ name: "In-Reply-To", value: options.inReplyTo }];
|
|
187
|
+
return msg;
|
|
188
|
+
}
|
|
189
|
+
toEnvelope(m) {
|
|
190
|
+
return {
|
|
191
|
+
uid: m.id,
|
|
192
|
+
from: { name: m.from?.emailAddress?.name, email: m.from?.emailAddress?.address || "" },
|
|
193
|
+
to: (m.toRecipients || []).map((r) => ({ name: r.emailAddress?.name, email: r.emailAddress?.address || "" })),
|
|
194
|
+
subject: m.subject || "",
|
|
195
|
+
date: m.receivedDateTime || "",
|
|
196
|
+
read: !!m.isRead,
|
|
197
|
+
flagged: m.flag?.flagStatus === "flagged",
|
|
198
|
+
hasAttachments: !!m.hasAttachments,
|
|
199
|
+
preview: m.bodyPreview || ""
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
toMessage(m) {
|
|
203
|
+
return {
|
|
204
|
+
uid: m.id,
|
|
205
|
+
from: { name: m.from?.emailAddress?.name, email: m.from?.emailAddress?.address || "" },
|
|
206
|
+
to: (m.toRecipients || []).map((r) => ({ name: r.emailAddress?.name, email: r.emailAddress?.address || "" })),
|
|
207
|
+
cc: (m.ccRecipients || []).map((r) => ({ name: r.emailAddress?.name, email: r.emailAddress?.address || "" })),
|
|
208
|
+
subject: m.subject || "",
|
|
209
|
+
body: m.body?.contentType === "HTML" ? "" : m.body?.content || "",
|
|
210
|
+
html: m.body?.contentType === "HTML" ? m.body?.content : void 0,
|
|
211
|
+
date: m.receivedDateTime || "",
|
|
212
|
+
read: !!m.isRead,
|
|
213
|
+
flagged: m.flag?.flagStatus === "flagged",
|
|
214
|
+
folder: "inbox",
|
|
215
|
+
messageId: m.internetMessageId,
|
|
216
|
+
attachments: []
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// src/agenticmail/providers/google.ts
|
|
222
|
+
var GMAIL_BASE = "https://gmail.googleapis.com/gmail/v1";
|
|
223
|
+
var GoogleEmailProvider = class {
|
|
224
|
+
provider = "google";
|
|
225
|
+
identity = null;
|
|
226
|
+
userId = "me";
|
|
227
|
+
get token() {
|
|
228
|
+
if (!this.identity) throw new Error("Not connected");
|
|
229
|
+
return this.identity.accessToken;
|
|
230
|
+
}
|
|
231
|
+
async refreshIfNeeded() {
|
|
232
|
+
if (this.identity?.refreshToken) {
|
|
233
|
+
this.identity.accessToken = await this.identity.refreshToken();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async gmailFetch(path, opts) {
|
|
237
|
+
await this.refreshIfNeeded();
|
|
238
|
+
const res = await fetch(`${GMAIL_BASE}/users/${this.userId}${path}`, {
|
|
239
|
+
...opts,
|
|
240
|
+
headers: {
|
|
241
|
+
Authorization: `Bearer ${this.token}`,
|
|
242
|
+
"Content-Type": "application/json",
|
|
243
|
+
...opts?.headers
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
if (!res.ok) {
|
|
247
|
+
const text = await res.text().catch(() => "");
|
|
248
|
+
throw new Error(`Gmail API ${res.status}: ${text}`);
|
|
249
|
+
}
|
|
250
|
+
if (res.status === 204) return null;
|
|
251
|
+
return res.json();
|
|
252
|
+
}
|
|
253
|
+
// ─── Connection ─────────────────────────────────────
|
|
254
|
+
async connect(identity) {
|
|
255
|
+
this.identity = identity;
|
|
256
|
+
await this.gmailFetch("/profile");
|
|
257
|
+
}
|
|
258
|
+
async disconnect() {
|
|
259
|
+
this.identity = null;
|
|
260
|
+
}
|
|
261
|
+
// ─── List / Read ────────────────────────────────────
|
|
262
|
+
async listMessages(folder, opts) {
|
|
263
|
+
const labelId = this.resolveLabelId(folder);
|
|
264
|
+
const maxResults = opts?.limit || 20;
|
|
265
|
+
const q = labelId === "INBOX" ? "" : "";
|
|
266
|
+
const data = await this.gmailFetch(`/messages?labelIds=${labelId}&maxResults=${maxResults}${q ? "&q=" + encodeURIComponent(q) : ""}`);
|
|
267
|
+
if (!data.messages?.length) return [];
|
|
268
|
+
const envelopes = [];
|
|
269
|
+
let skipped = 0;
|
|
270
|
+
for (const msg of data.messages) {
|
|
271
|
+
try {
|
|
272
|
+
const detail = await this.gmailFetch(`/messages/${msg.id}?format=metadata&metadataHeaders=From&metadataHeaders=To&metadataHeaders=Subject&metadataHeaders=Date`);
|
|
273
|
+
envelopes.push(this.metadataToEnvelope(detail));
|
|
274
|
+
} catch (e) {
|
|
275
|
+
skipped++;
|
|
276
|
+
console.error(`[gmail] Failed to fetch metadata for ${msg.id}: ${e.message?.slice(0, 100)}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (skipped > 0) console.warn(`[gmail] Skipped ${skipped}/${data.messages.length} messages due to errors`);
|
|
280
|
+
return envelopes;
|
|
281
|
+
}
|
|
282
|
+
async readMessage(uid) {
|
|
283
|
+
const data = await this.gmailFetch(`/messages/${uid}?format=full`);
|
|
284
|
+
return this.fullToMessage(data);
|
|
285
|
+
}
|
|
286
|
+
async searchMessages(criteria) {
|
|
287
|
+
const parts = [];
|
|
288
|
+
if (criteria.from) parts.push(`from:${criteria.from}`);
|
|
289
|
+
if (criteria.to) parts.push(`to:${criteria.to}`);
|
|
290
|
+
if (criteria.subject) parts.push(`subject:${criteria.subject}`);
|
|
291
|
+
if (criteria.text) parts.push(criteria.text);
|
|
292
|
+
if (criteria.since) parts.push(`after:${criteria.since.split("T")[0]}`);
|
|
293
|
+
if (criteria.before) parts.push(`before:${criteria.before.split("T")[0]}`);
|
|
294
|
+
if (criteria.seen === true) parts.push("is:read");
|
|
295
|
+
if (criteria.seen === false) parts.push("is:unread");
|
|
296
|
+
const q = parts.join(" ");
|
|
297
|
+
const data = await this.gmailFetch(`/messages?q=${encodeURIComponent(q)}&maxResults=50`);
|
|
298
|
+
if (!data.messages?.length) return [];
|
|
299
|
+
const envelopes = [];
|
|
300
|
+
for (const msg of data.messages.slice(0, 20)) {
|
|
301
|
+
try {
|
|
302
|
+
const detail = await this.gmailFetch(`/messages/${msg.id}?format=metadata&metadataHeaders=From&metadataHeaders=To&metadataHeaders=Subject&metadataHeaders=Date`);
|
|
303
|
+
envelopes.push(this.metadataToEnvelope(detail));
|
|
304
|
+
} catch {
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return envelopes;
|
|
308
|
+
}
|
|
309
|
+
async listFolders() {
|
|
310
|
+
const data = await this.gmailFetch("/labels");
|
|
311
|
+
return (data.labels || []).map((l) => ({
|
|
312
|
+
name: l.name,
|
|
313
|
+
path: l.id,
|
|
314
|
+
unread: l.messagesUnread || 0,
|
|
315
|
+
total: l.messagesTotal || 0
|
|
316
|
+
}));
|
|
317
|
+
}
|
|
318
|
+
async createFolder(name) {
|
|
319
|
+
await this.gmailFetch("/labels", {
|
|
320
|
+
method: "POST",
|
|
321
|
+
body: JSON.stringify({ name, labelListVisibility: "labelShow", messageListVisibility: "show" })
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
// ─── Send ───────────────────────────────────────────
|
|
325
|
+
async send(options) {
|
|
326
|
+
const raw = this.buildRawEmail(options);
|
|
327
|
+
const sendBody = { raw };
|
|
328
|
+
if (options.threadId) sendBody.threadId = options.threadId;
|
|
329
|
+
const data = await this.gmailFetch("/messages/send", {
|
|
330
|
+
method: "POST",
|
|
331
|
+
body: JSON.stringify(sendBody)
|
|
332
|
+
});
|
|
333
|
+
return { messageId: data.id };
|
|
334
|
+
}
|
|
335
|
+
async reply(uid, body, replyAll = false) {
|
|
336
|
+
const originalData = await this.gmailFetch(`/messages/${uid}?format=full`);
|
|
337
|
+
const original = this.fullToMessage(originalData);
|
|
338
|
+
const threadId = originalData.threadId;
|
|
339
|
+
const to = replyAll ? [original.from.email, ...(original.to || []).map((t) => t.email), ...(original.cc || []).map((c) => c.email)].filter((e) => e !== this.identity?.email).join(", ") : original.from.email;
|
|
340
|
+
return this.send({
|
|
341
|
+
to,
|
|
342
|
+
subject: original.subject.startsWith("Re:") ? original.subject : `Re: ${original.subject}`,
|
|
343
|
+
body,
|
|
344
|
+
inReplyTo: original.messageId,
|
|
345
|
+
references: original.references ? [...original.references, original.messageId] : [original.messageId],
|
|
346
|
+
threadId
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
async forward(uid, to, body) {
|
|
350
|
+
const original = await this.readMessage(uid);
|
|
351
|
+
return this.send({
|
|
352
|
+
to,
|
|
353
|
+
subject: `Fwd: ${original.subject}`,
|
|
354
|
+
body: (body ? body + "\n\n" : "") + `---------- Forwarded message ----------
|
|
355
|
+
From: ${original.from.email}
|
|
356
|
+
Date: ${original.date}
|
|
357
|
+
Subject: ${original.subject}
|
|
358
|
+
|
|
359
|
+
${original.body}`
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
// ─── Organize ───────────────────────────────────────
|
|
363
|
+
async moveMessage(uid, toFolder, fromFolder) {
|
|
364
|
+
const addLabel = this.resolveLabelId(toFolder);
|
|
365
|
+
const removeLabel = fromFolder ? this.resolveLabelId(fromFolder) : "INBOX";
|
|
366
|
+
await this.gmailFetch(`/messages/${uid}/modify`, {
|
|
367
|
+
method: "POST",
|
|
368
|
+
body: JSON.stringify({ addLabelIds: [addLabel], removeLabelIds: [removeLabel] })
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
async deleteMessage(uid) {
|
|
372
|
+
await this.gmailFetch(`/messages/${uid}/trash`, { method: "POST" });
|
|
373
|
+
}
|
|
374
|
+
async markRead(uid) {
|
|
375
|
+
await this.gmailFetch(`/messages/${uid}/modify`, {
|
|
376
|
+
method: "POST",
|
|
377
|
+
body: JSON.stringify({ removeLabelIds: ["UNREAD"] })
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
async markUnread(uid) {
|
|
381
|
+
await this.gmailFetch(`/messages/${uid}/modify`, {
|
|
382
|
+
method: "POST",
|
|
383
|
+
body: JSON.stringify({ addLabelIds: ["UNREAD"] })
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
async flagMessage(uid) {
|
|
387
|
+
await this.gmailFetch(`/messages/${uid}/modify`, {
|
|
388
|
+
method: "POST",
|
|
389
|
+
body: JSON.stringify({ addLabelIds: ["STARRED"] })
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
async unflagMessage(uid) {
|
|
393
|
+
await this.gmailFetch(`/messages/${uid}/modify`, {
|
|
394
|
+
method: "POST",
|
|
395
|
+
body: JSON.stringify({ removeLabelIds: ["STARRED"] })
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
// ─── Batch ──────────────────────────────────────────
|
|
399
|
+
async batchMarkRead(uids) {
|
|
400
|
+
await this.gmailFetch("/messages/batchModify", {
|
|
401
|
+
method: "POST",
|
|
402
|
+
body: JSON.stringify({ ids: uids, removeLabelIds: ["UNREAD"] })
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
async batchMarkUnread(uids) {
|
|
406
|
+
await this.gmailFetch("/messages/batchModify", {
|
|
407
|
+
method: "POST",
|
|
408
|
+
body: JSON.stringify({ ids: uids, addLabelIds: ["UNREAD"] })
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
async batchMove(uids, toFolder, fromFolder) {
|
|
412
|
+
const addLabel = this.resolveLabelId(toFolder);
|
|
413
|
+
const removeLabel = fromFolder ? this.resolveLabelId(fromFolder) : "INBOX";
|
|
414
|
+
await this.gmailFetch("/messages/batchModify", {
|
|
415
|
+
method: "POST",
|
|
416
|
+
body: JSON.stringify({ ids: uids, addLabelIds: [addLabel], removeLabelIds: [removeLabel] })
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
async batchDelete(uids) {
|
|
420
|
+
await Promise.all(uids.map((uid) => this.deleteMessage(uid)));
|
|
421
|
+
}
|
|
422
|
+
// ─── Gmail Push Notifications ────────────────────────
|
|
423
|
+
/**
|
|
424
|
+
* Set up Gmail push notifications via Google Cloud Pub/Sub.
|
|
425
|
+
* Gmail will POST to your Pub/Sub topic when new emails arrive.
|
|
426
|
+
* Requires: Pub/Sub topic created, Gmail API granted publish permission.
|
|
427
|
+
* Returns: historyId and expiration (watch lasts ~7 days, must renew).
|
|
428
|
+
*/
|
|
429
|
+
async watchInbox(topicName) {
|
|
430
|
+
const data = await this.gmailFetch("/watch", {
|
|
431
|
+
method: "POST",
|
|
432
|
+
body: JSON.stringify({
|
|
433
|
+
topicName,
|
|
434
|
+
labelIds: ["INBOX"],
|
|
435
|
+
labelFilterBehavior: "INCLUDE"
|
|
436
|
+
})
|
|
437
|
+
});
|
|
438
|
+
return { historyId: data.historyId, expiration: data.expiration };
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Stop Gmail push notifications.
|
|
442
|
+
*/
|
|
443
|
+
async stopWatch() {
|
|
444
|
+
await this.gmailFetch("/stop", { method: "POST" });
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Get message history since a historyId (for processing push notification deltas).
|
|
448
|
+
*/
|
|
449
|
+
async getHistory(startHistoryId) {
|
|
450
|
+
const data = await this.gmailFetch(`/history?startHistoryId=${startHistoryId}&historyTypes=messageAdded&labelId=INBOX`);
|
|
451
|
+
const messages = [];
|
|
452
|
+
for (const h of data.history || []) {
|
|
453
|
+
for (const added of h.messagesAdded || []) {
|
|
454
|
+
if (added.message?.id) messages.push({ id: added.message.id, threadId: added.message.threadId });
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return { messages, historyId: data.historyId || startHistoryId };
|
|
458
|
+
}
|
|
459
|
+
// ─── Helpers ────────────────────────────────────────
|
|
460
|
+
resolveLabelId(folder) {
|
|
461
|
+
const map = {
|
|
462
|
+
INBOX: "INBOX",
|
|
463
|
+
inbox: "INBOX",
|
|
464
|
+
Sent: "SENT",
|
|
465
|
+
sent: "SENT",
|
|
466
|
+
Drafts: "DRAFT",
|
|
467
|
+
drafts: "DRAFT",
|
|
468
|
+
Trash: "TRASH",
|
|
469
|
+
trash: "TRASH",
|
|
470
|
+
Spam: "SPAM",
|
|
471
|
+
spam: "SPAM",
|
|
472
|
+
Junk: "SPAM",
|
|
473
|
+
junk: "SPAM",
|
|
474
|
+
Starred: "STARRED",
|
|
475
|
+
starred: "STARRED",
|
|
476
|
+
Important: "IMPORTANT",
|
|
477
|
+
important: "IMPORTANT"
|
|
478
|
+
};
|
|
479
|
+
return map[folder] || folder;
|
|
480
|
+
}
|
|
481
|
+
buildRawEmail(options) {
|
|
482
|
+
const fromAddr = this.identity?.email;
|
|
483
|
+
const fromName = this.identity?.name;
|
|
484
|
+
const lines = [
|
|
485
|
+
`MIME-Version: 1.0`,
|
|
486
|
+
fromAddr ? `From: ${fromName ? `"${fromName}" <${fromAddr}>` : fromAddr}` : "",
|
|
487
|
+
`To: ${options.to}`,
|
|
488
|
+
`Subject: ${options.subject}`,
|
|
489
|
+
`Content-Type: text/plain; charset=utf-8`,
|
|
490
|
+
`Content-Transfer-Encoding: 7bit`
|
|
491
|
+
].filter(Boolean);
|
|
492
|
+
if (options.cc) lines.splice(3, 0, `Cc: ${options.cc}`);
|
|
493
|
+
if (options.inReplyTo) lines.push(`In-Reply-To: ${options.inReplyTo}`);
|
|
494
|
+
if (options.references?.length) lines.push(`References: ${options.references.join(" ")}`);
|
|
495
|
+
lines.push("", options.body);
|
|
496
|
+
const raw = lines.join("\r\n");
|
|
497
|
+
return Buffer.from(raw, "utf-8").toString("base64url");
|
|
498
|
+
}
|
|
499
|
+
getHeader(msg, name) {
|
|
500
|
+
const headers = msg.payload?.headers || [];
|
|
501
|
+
const h = headers.find((h2) => h2.name.toLowerCase() === name.toLowerCase());
|
|
502
|
+
return h?.value || "";
|
|
503
|
+
}
|
|
504
|
+
metadataToEnvelope(msg) {
|
|
505
|
+
const from = this.getHeader(msg, "From");
|
|
506
|
+
const fromMatch = from.match(/^(.*?)\s*<(.+?)>$/) || [null, "", from];
|
|
507
|
+
return {
|
|
508
|
+
uid: msg.id,
|
|
509
|
+
from: { name: fromMatch[1]?.replace(/"/g, "").trim() || void 0, email: fromMatch[2] || from },
|
|
510
|
+
to: [{ email: this.getHeader(msg, "To") }],
|
|
511
|
+
subject: this.getHeader(msg, "Subject"),
|
|
512
|
+
date: this.getHeader(msg, "Date"),
|
|
513
|
+
read: !(msg.labelIds || []).includes("UNREAD"),
|
|
514
|
+
flagged: (msg.labelIds || []).includes("STARRED"),
|
|
515
|
+
hasAttachments: false,
|
|
516
|
+
preview: msg.snippet || ""
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
fullToMessage(msg) {
|
|
520
|
+
const from = this.getHeader(msg, "From");
|
|
521
|
+
const fromMatch = from.match(/^(.*?)\s*<(.+?)>$/) || [null, "", from];
|
|
522
|
+
let body = "";
|
|
523
|
+
let html;
|
|
524
|
+
const extractBody = (payload) => {
|
|
525
|
+
if (payload.mimeType === "text/plain" && payload.body?.data) {
|
|
526
|
+
body = Buffer.from(payload.body.data, "base64url").toString("utf-8");
|
|
527
|
+
}
|
|
528
|
+
if (payload.mimeType === "text/html" && payload.body?.data) {
|
|
529
|
+
html = Buffer.from(payload.body.data, "base64url").toString("utf-8");
|
|
530
|
+
}
|
|
531
|
+
if (payload.parts) payload.parts.forEach(extractBody);
|
|
532
|
+
};
|
|
533
|
+
if (msg.payload) extractBody(msg.payload);
|
|
534
|
+
return {
|
|
535
|
+
uid: msg.id,
|
|
536
|
+
from: { name: fromMatch[1]?.replace(/"/g, "").trim() || void 0, email: fromMatch[2] || from },
|
|
537
|
+
to: [{ email: this.getHeader(msg, "To") }],
|
|
538
|
+
cc: this.getHeader(msg, "Cc") ? [{ email: this.getHeader(msg, "Cc") }] : void 0,
|
|
539
|
+
subject: this.getHeader(msg, "Subject"),
|
|
540
|
+
body,
|
|
541
|
+
html,
|
|
542
|
+
date: this.getHeader(msg, "Date"),
|
|
543
|
+
read: !(msg.labelIds || []).includes("UNREAD"),
|
|
544
|
+
flagged: (msg.labelIds || []).includes("STARRED"),
|
|
545
|
+
folder: (msg.labelIds || []).includes("INBOX") ? "inbox" : "other",
|
|
546
|
+
messageId: this.getHeader(msg, "Message-ID"),
|
|
547
|
+
inReplyTo: this.getHeader(msg, "In-Reply-To") || void 0,
|
|
548
|
+
references: this.getHeader(msg, "References") ? this.getHeader(msg, "References").split(/\s+/) : void 0,
|
|
549
|
+
attachments: []
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
// src/agenticmail/providers/imap.ts
|
|
555
|
+
var IMAP_PRESETS = {
|
|
556
|
+
"microsoft365": { imapHost: "outlook.office365.com", imapPort: 993, smtpHost: "smtp.office365.com", smtpPort: 587 },
|
|
557
|
+
"office365": { imapHost: "outlook.office365.com", imapPort: 993, smtpHost: "smtp.office365.com", smtpPort: 587 },
|
|
558
|
+
"outlook": { imapHost: "outlook.office365.com", imapPort: 993, smtpHost: "smtp.office365.com", smtpPort: 587 },
|
|
559
|
+
"gmail": { imapHost: "imap.gmail.com", imapPort: 993, smtpHost: "smtp.gmail.com", smtpPort: 587 },
|
|
560
|
+
"google": { imapHost: "imap.gmail.com", imapPort: 993, smtpHost: "smtp.gmail.com", smtpPort: 587 },
|
|
561
|
+
"yahoo": { imapHost: "imap.mail.yahoo.com", imapPort: 993, smtpHost: "smtp.mail.yahoo.com", smtpPort: 465 },
|
|
562
|
+
"zoho": { imapHost: "imap.zoho.com", imapPort: 993, smtpHost: "smtp.zoho.com", smtpPort: 587 },
|
|
563
|
+
"fastmail": { imapHost: "imap.fastmail.com", imapPort: 993, smtpHost: "smtp.fastmail.com", smtpPort: 587 },
|
|
564
|
+
"icloud": { imapHost: "imap.mail.me.com", imapPort: 993, smtpHost: "smtp.mail.me.com", smtpPort: 587 }
|
|
565
|
+
};
|
|
566
|
+
function detectImapSettings(email) {
|
|
567
|
+
const domain = email.split("@")[1]?.toLowerCase();
|
|
568
|
+
if (!domain) return null;
|
|
569
|
+
for (const [key, preset] of Object.entries(IMAP_PRESETS)) {
|
|
570
|
+
if (domain.includes(key) || domain === "gmail.com" || domain === "outlook.com" || domain === "hotmail.com") {
|
|
571
|
+
if (domain === "gmail.com" || domain.endsWith(".google.com")) return IMAP_PRESETS.gmail;
|
|
572
|
+
if (domain === "outlook.com" || domain === "hotmail.com" || domain.endsWith(".onmicrosoft.com")) return IMAP_PRESETS.microsoft365;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
var ImapEmailProvider = class {
|
|
578
|
+
provider = "imap";
|
|
579
|
+
identity = null;
|
|
580
|
+
// IMAP connection (lazy-loaded to avoid bundling the dep if not used)
|
|
581
|
+
imapClient = null;
|
|
582
|
+
smtpClient = null;
|
|
583
|
+
getIdentity() {
|
|
584
|
+
if (!this.identity) throw new Error("Not connected \u2014 call connect() first");
|
|
585
|
+
return this.identity;
|
|
586
|
+
}
|
|
587
|
+
// ─── Connection ─────────────────────────────────────
|
|
588
|
+
async connect(identity) {
|
|
589
|
+
const imapIdentity = identity;
|
|
590
|
+
if (!imapIdentity.imapHost) throw new Error("IMAP host is required");
|
|
591
|
+
if (!imapIdentity.smtpHost) throw new Error("SMTP host is required");
|
|
592
|
+
this.identity = imapIdentity;
|
|
593
|
+
try {
|
|
594
|
+
const { ImapFlow } = await import("imapflow");
|
|
595
|
+
this.imapClient = new ImapFlow({
|
|
596
|
+
host: imapIdentity.imapHost,
|
|
597
|
+
port: imapIdentity.imapPort || 993,
|
|
598
|
+
secure: true,
|
|
599
|
+
auth: {
|
|
600
|
+
user: imapIdentity.email,
|
|
601
|
+
pass: imapIdentity.password || imapIdentity.accessToken
|
|
602
|
+
},
|
|
603
|
+
logger: false
|
|
604
|
+
});
|
|
605
|
+
await this.imapClient.connect();
|
|
606
|
+
} catch (err) {
|
|
607
|
+
if (err.code === "ERR_MODULE_NOT_FOUND" || err.message?.includes("Cannot find")) {
|
|
608
|
+
console.warn("[imap-provider] imapflow not installed \u2014 IMAP operations will fail. Install with: npm install imapflow");
|
|
609
|
+
this.imapClient = null;
|
|
610
|
+
} else {
|
|
611
|
+
throw new Error(`Failed to connect to IMAP server ${imapIdentity.imapHost}: ${err.message}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
async disconnect() {
|
|
616
|
+
if (this.imapClient) {
|
|
617
|
+
try {
|
|
618
|
+
await this.imapClient.logout();
|
|
619
|
+
} catch {
|
|
620
|
+
}
|
|
621
|
+
this.imapClient = null;
|
|
622
|
+
}
|
|
623
|
+
this.identity = null;
|
|
624
|
+
}
|
|
625
|
+
// ─── List / Read ────────────────────────────────────
|
|
626
|
+
async listMessages(folder, opts) {
|
|
627
|
+
if (!this.imapClient) throw new Error("IMAP not connected. Ensure imapflow is installed and connection succeeded.");
|
|
628
|
+
const limit = opts?.limit || 20;
|
|
629
|
+
const lock = await this.imapClient.getMailboxLock(folder || "INBOX");
|
|
630
|
+
try {
|
|
631
|
+
const envelopes = [];
|
|
632
|
+
const status = await this.imapClient.status(folder || "INBOX", { messages: true });
|
|
633
|
+
const total = status.messages || 0;
|
|
634
|
+
const start = Math.max(1, total - (opts?.offset || 0) - limit + 1);
|
|
635
|
+
const end = total - (opts?.offset || 0);
|
|
636
|
+
if (start > end || end < 1) return [];
|
|
637
|
+
for await (const msg of this.imapClient.fetch(`${start}:${end}`, { envelope: true, flags: true, bodyStructure: true })) {
|
|
638
|
+
envelopes.push({
|
|
639
|
+
uid: String(msg.uid),
|
|
640
|
+
from: {
|
|
641
|
+
name: msg.envelope?.from?.[0]?.name || void 0,
|
|
642
|
+
email: msg.envelope?.from?.[0]?.address || ""
|
|
643
|
+
},
|
|
644
|
+
to: (msg.envelope?.to || []).map((t) => ({
|
|
645
|
+
name: t.name || void 0,
|
|
646
|
+
email: t.address || ""
|
|
647
|
+
})),
|
|
648
|
+
subject: msg.envelope?.subject || "",
|
|
649
|
+
date: msg.envelope?.date?.toISOString() || "",
|
|
650
|
+
read: msg.flags?.has("\\Seen") || false,
|
|
651
|
+
flagged: msg.flags?.has("\\Flagged") || false,
|
|
652
|
+
hasAttachments: msg.bodyStructure?.childNodes?.length > 1 || false
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
return envelopes.reverse();
|
|
656
|
+
} finally {
|
|
657
|
+
lock.release();
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
async readMessage(uid, folder) {
|
|
661
|
+
if (!this.imapClient) throw new Error("IMAP not connected");
|
|
662
|
+
const lock = await this.imapClient.getMailboxLock(folder || "INBOX");
|
|
663
|
+
try {
|
|
664
|
+
const msg = await this.imapClient.fetchOne(uid, {
|
|
665
|
+
envelope: true,
|
|
666
|
+
flags: true,
|
|
667
|
+
source: true
|
|
668
|
+
// full RFC822 message
|
|
669
|
+
}, { uid: true });
|
|
670
|
+
if (!msg) throw new Error(`Message ${uid} not found`);
|
|
671
|
+
const source = msg.source?.toString("utf-8") || "";
|
|
672
|
+
const bodyMatch = source.match(/\r?\n\r?\n([\s\S]*)/);
|
|
673
|
+
const body = bodyMatch ? bodyMatch[1] : "";
|
|
674
|
+
return {
|
|
675
|
+
uid: String(msg.uid),
|
|
676
|
+
from: {
|
|
677
|
+
name: msg.envelope?.from?.[0]?.name || void 0,
|
|
678
|
+
email: msg.envelope?.from?.[0]?.address || ""
|
|
679
|
+
},
|
|
680
|
+
to: (msg.envelope?.to || []).map((t) => ({
|
|
681
|
+
name: t.name || void 0,
|
|
682
|
+
email: t.address || ""
|
|
683
|
+
})),
|
|
684
|
+
cc: (msg.envelope?.cc || []).map((c) => ({
|
|
685
|
+
name: c.name || void 0,
|
|
686
|
+
email: c.address || ""
|
|
687
|
+
})),
|
|
688
|
+
subject: msg.envelope?.subject || "",
|
|
689
|
+
body,
|
|
690
|
+
date: msg.envelope?.date?.toISOString() || "",
|
|
691
|
+
read: msg.flags?.has("\\Seen") || false,
|
|
692
|
+
flagged: msg.flags?.has("\\Flagged") || false,
|
|
693
|
+
folder: folder || "INBOX",
|
|
694
|
+
messageId: msg.envelope?.messageId || void 0,
|
|
695
|
+
inReplyTo: msg.envelope?.inReplyTo || void 0
|
|
696
|
+
};
|
|
697
|
+
} finally {
|
|
698
|
+
lock.release();
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
async searchMessages(criteria) {
|
|
702
|
+
if (!this.imapClient) throw new Error("IMAP not connected");
|
|
703
|
+
const lock = await this.imapClient.getMailboxLock("INBOX");
|
|
704
|
+
try {
|
|
705
|
+
const query = {};
|
|
706
|
+
if (criteria.from) query.from = criteria.from;
|
|
707
|
+
if (criteria.to) query.to = criteria.to;
|
|
708
|
+
if (criteria.subject) query.subject = criteria.subject;
|
|
709
|
+
if (criteria.text) query.body = criteria.text;
|
|
710
|
+
if (criteria.since) query.since = new Date(criteria.since);
|
|
711
|
+
if (criteria.before) query.before = new Date(criteria.before);
|
|
712
|
+
if (criteria.seen === true) query.seen = true;
|
|
713
|
+
if (criteria.seen === false) query.unseen = true;
|
|
714
|
+
const uids = await this.imapClient.search(query, { uid: true });
|
|
715
|
+
if (!uids.length) return [];
|
|
716
|
+
const envelopes = [];
|
|
717
|
+
const uidRange = uids.slice(-50).join(",");
|
|
718
|
+
for await (const msg of this.imapClient.fetch(uidRange, { envelope: true, flags: true }, { uid: true })) {
|
|
719
|
+
envelopes.push({
|
|
720
|
+
uid: String(msg.uid),
|
|
721
|
+
from: {
|
|
722
|
+
name: msg.envelope?.from?.[0]?.name || void 0,
|
|
723
|
+
email: msg.envelope?.from?.[0]?.address || ""
|
|
724
|
+
},
|
|
725
|
+
to: (msg.envelope?.to || []).map((t) => ({
|
|
726
|
+
name: t.name || void 0,
|
|
727
|
+
email: t.address || ""
|
|
728
|
+
})),
|
|
729
|
+
subject: msg.envelope?.subject || "",
|
|
730
|
+
date: msg.envelope?.date?.toISOString() || "",
|
|
731
|
+
read: msg.flags?.has("\\Seen") || false,
|
|
732
|
+
flagged: msg.flags?.has("\\Flagged") || false,
|
|
733
|
+
hasAttachments: false
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
return envelopes.reverse();
|
|
737
|
+
} finally {
|
|
738
|
+
lock.release();
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
async listFolders() {
|
|
742
|
+
if (!this.imapClient) throw new Error("IMAP not connected");
|
|
743
|
+
const folders = [];
|
|
744
|
+
const mailboxes = await this.imapClient.list();
|
|
745
|
+
for (const mb of mailboxes) {
|
|
746
|
+
try {
|
|
747
|
+
const status = await this.imapClient.status(mb.path, { messages: true, unseen: true });
|
|
748
|
+
folders.push({
|
|
749
|
+
name: mb.name,
|
|
750
|
+
path: mb.path,
|
|
751
|
+
unread: status.unseen || 0,
|
|
752
|
+
total: status.messages || 0
|
|
753
|
+
});
|
|
754
|
+
} catch {
|
|
755
|
+
folders.push({ name: mb.name, path: mb.path, unread: 0, total: 0 });
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return folders;
|
|
759
|
+
}
|
|
760
|
+
async createFolder(name) {
|
|
761
|
+
if (!this.imapClient) throw new Error("IMAP not connected");
|
|
762
|
+
await this.imapClient.mailboxCreate(name);
|
|
763
|
+
}
|
|
764
|
+
// ─── Send (via SMTP) ───────────────────────────────
|
|
765
|
+
async send(options) {
|
|
766
|
+
const identity = this.getIdentity();
|
|
767
|
+
try {
|
|
768
|
+
const nodemailer = await import("nodemailer");
|
|
769
|
+
const transporter = nodemailer.createTransport({
|
|
770
|
+
host: identity.smtpHost,
|
|
771
|
+
port: identity.smtpPort || 587,
|
|
772
|
+
secure: (identity.smtpPort || 587) === 465,
|
|
773
|
+
auth: {
|
|
774
|
+
user: identity.email,
|
|
775
|
+
pass: identity.password || identity.accessToken
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
const result = await transporter.sendMail({
|
|
779
|
+
from: identity.email,
|
|
780
|
+
to: options.to,
|
|
781
|
+
cc: options.cc,
|
|
782
|
+
bcc: options.bcc,
|
|
783
|
+
subject: options.subject,
|
|
784
|
+
text: options.body,
|
|
785
|
+
html: options.html,
|
|
786
|
+
inReplyTo: options.inReplyTo,
|
|
787
|
+
references: options.references?.join(" "),
|
|
788
|
+
attachments: options.attachments?.map((a) => ({
|
|
789
|
+
filename: a.filename,
|
|
790
|
+
content: a.content,
|
|
791
|
+
contentType: a.contentType,
|
|
792
|
+
encoding: a.encoding
|
|
793
|
+
}))
|
|
794
|
+
});
|
|
795
|
+
return { messageId: result.messageId || `smtp-${Date.now()}` };
|
|
796
|
+
} catch (err) {
|
|
797
|
+
if (err.code === "ERR_MODULE_NOT_FOUND" || err.message?.includes("Cannot find")) {
|
|
798
|
+
throw new Error("nodemailer is required for SMTP sending. Install with: npm install nodemailer");
|
|
799
|
+
}
|
|
800
|
+
throw new Error(`SMTP send failed: ${err.message}`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
async reply(uid, body, replyAll = false) {
|
|
804
|
+
const original = await this.readMessage(uid);
|
|
805
|
+
const to = replyAll ? [original.from.email, ...(original.to || []).map((t) => t.email), ...(original.cc || []).map((c) => c.email)].filter((e) => e !== this.identity?.email).join(", ") : original.from.email;
|
|
806
|
+
return this.send({
|
|
807
|
+
to,
|
|
808
|
+
subject: original.subject.startsWith("Re:") ? original.subject : `Re: ${original.subject}`,
|
|
809
|
+
body,
|
|
810
|
+
inReplyTo: original.messageId,
|
|
811
|
+
references: original.references ? [...original.references, original.messageId] : original.messageId ? [original.messageId] : void 0
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
async forward(uid, to, body) {
|
|
815
|
+
const original = await this.readMessage(uid);
|
|
816
|
+
return this.send({
|
|
817
|
+
to,
|
|
818
|
+
subject: `Fwd: ${original.subject}`,
|
|
819
|
+
body: (body ? body + "\n\n" : "") + `---------- Forwarded message ----------
|
|
820
|
+
From: ${original.from.email}
|
|
821
|
+
Date: ${original.date}
|
|
822
|
+
Subject: ${original.subject}
|
|
823
|
+
|
|
824
|
+
` + original.body
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
// ─── Organize ───────────────────────────────────────
|
|
828
|
+
async moveMessage(uid, toFolder, _fromFolder) {
|
|
829
|
+
if (!this.imapClient) throw new Error("IMAP not connected");
|
|
830
|
+
const lock = await this.imapClient.getMailboxLock(_fromFolder || "INBOX");
|
|
831
|
+
try {
|
|
832
|
+
await this.imapClient.messageMove(uid, toFolder, { uid: true });
|
|
833
|
+
} finally {
|
|
834
|
+
lock.release();
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
async deleteMessage(uid, folder) {
|
|
838
|
+
if (!this.imapClient) throw new Error("IMAP not connected");
|
|
839
|
+
const lock = await this.imapClient.getMailboxLock(folder || "INBOX");
|
|
840
|
+
try {
|
|
841
|
+
await this.imapClient.messageDelete(uid, { uid: true });
|
|
842
|
+
} finally {
|
|
843
|
+
lock.release();
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
async markRead(uid, folder) {
|
|
847
|
+
if (!this.imapClient) throw new Error("IMAP not connected");
|
|
848
|
+
const lock = await this.imapClient.getMailboxLock(folder || "INBOX");
|
|
849
|
+
try {
|
|
850
|
+
await this.imapClient.messageFlagsAdd(uid, ["\\Seen"], { uid: true });
|
|
851
|
+
} finally {
|
|
852
|
+
lock.release();
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
async markUnread(uid, folder) {
|
|
856
|
+
if (!this.imapClient) throw new Error("IMAP not connected");
|
|
857
|
+
const lock = await this.imapClient.getMailboxLock(folder || "INBOX");
|
|
858
|
+
try {
|
|
859
|
+
await this.imapClient.messageFlagsRemove(uid, ["\\Seen"], { uid: true });
|
|
860
|
+
} finally {
|
|
861
|
+
lock.release();
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
async flagMessage(uid, folder) {
|
|
865
|
+
if (!this.imapClient) throw new Error("IMAP not connected");
|
|
866
|
+
const lock = await this.imapClient.getMailboxLock(folder || "INBOX");
|
|
867
|
+
try {
|
|
868
|
+
await this.imapClient.messageFlagsAdd(uid, ["\\Flagged"], { uid: true });
|
|
869
|
+
} finally {
|
|
870
|
+
lock.release();
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
async unflagMessage(uid, folder) {
|
|
874
|
+
if (!this.imapClient) throw new Error("IMAP not connected");
|
|
875
|
+
const lock = await this.imapClient.getMailboxLock(folder || "INBOX");
|
|
876
|
+
try {
|
|
877
|
+
await this.imapClient.messageFlagsRemove(uid, ["\\Flagged"], { uid: true });
|
|
878
|
+
} finally {
|
|
879
|
+
lock.release();
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
// ─── Batch ──────────────────────────────────────────
|
|
883
|
+
async batchMarkRead(uids, folder) {
|
|
884
|
+
for (const uid of uids) await this.markRead(uid, folder);
|
|
885
|
+
}
|
|
886
|
+
async batchMarkUnread(uids, folder) {
|
|
887
|
+
for (const uid of uids) await this.markUnread(uid, folder);
|
|
888
|
+
}
|
|
889
|
+
async batchMove(uids, toFolder, fromFolder) {
|
|
890
|
+
for (const uid of uids) await this.moveMessage(uid, toFolder, fromFolder);
|
|
891
|
+
}
|
|
892
|
+
async batchDelete(uids, folder) {
|
|
893
|
+
for (const uid of uids) await this.deleteMessage(uid, folder);
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
// src/agenticmail/providers/index.ts
|
|
898
|
+
function createEmailProvider(provider) {
|
|
899
|
+
switch (provider) {
|
|
900
|
+
case "microsoft":
|
|
901
|
+
return new MicrosoftEmailProvider();
|
|
902
|
+
case "google":
|
|
903
|
+
return new GoogleEmailProvider();
|
|
904
|
+
case "imap":
|
|
905
|
+
return new ImapEmailProvider();
|
|
906
|
+
default:
|
|
907
|
+
throw new Error(`Unknown email provider: ${provider}`);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// src/agenticmail/manager.ts
|
|
912
|
+
var AgenticMailManager = class {
|
|
913
|
+
providers = /* @__PURE__ */ new Map();
|
|
914
|
+
identities = /* @__PURE__ */ new Map();
|
|
915
|
+
db;
|
|
916
|
+
constructor(opts) {
|
|
917
|
+
this.db = opts?.db;
|
|
918
|
+
}
|
|
919
|
+
setDb(db) {
|
|
920
|
+
this.db = db;
|
|
921
|
+
}
|
|
922
|
+
// ─── Agent Registration ─────────────────────────────
|
|
923
|
+
/**
|
|
924
|
+
* Register an agent's email identity from the org's OAuth/SSO.
|
|
925
|
+
* Called when an agent is created or when its OAuth token is refreshed.
|
|
926
|
+
*/
|
|
927
|
+
async registerAgent(identity) {
|
|
928
|
+
this.identities.set(identity.agentId, identity);
|
|
929
|
+
const provider = createEmailProvider(identity.provider);
|
|
930
|
+
await provider.connect(identity);
|
|
931
|
+
this.providers.set(identity.agentId, provider);
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Unregister an agent (on deletion or token revocation).
|
|
935
|
+
*/
|
|
936
|
+
async unregisterAgent(agentId) {
|
|
937
|
+
const provider = this.providers.get(agentId);
|
|
938
|
+
if (provider) {
|
|
939
|
+
await provider.disconnect().catch(() => {
|
|
940
|
+
});
|
|
941
|
+
this.providers.delete(agentId);
|
|
942
|
+
}
|
|
943
|
+
this.identities.delete(agentId);
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Get the email provider for an agent.
|
|
947
|
+
* Throws if agent is not registered.
|
|
948
|
+
*/
|
|
949
|
+
getProvider(agentId) {
|
|
950
|
+
const provider = this.providers.get(agentId);
|
|
951
|
+
if (!provider) throw new Error(`Agent ${agentId} has no email provider registered. Ensure the agent has been connected via org OAuth.`);
|
|
952
|
+
return provider;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Get the email identity for an agent.
|
|
956
|
+
*/
|
|
957
|
+
getIdentity(agentId) {
|
|
958
|
+
return this.identities.get(agentId);
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Check if an agent has email access.
|
|
962
|
+
*/
|
|
963
|
+
hasEmail(agentId) {
|
|
964
|
+
return this.providers.has(agentId);
|
|
965
|
+
}
|
|
966
|
+
// ─── Inter-Agent Messaging ──────────────────────────
|
|
967
|
+
// These use the enterprise DB directly, not email.
|
|
968
|
+
// Agents in the same org can message each other without email.
|
|
969
|
+
/**
|
|
970
|
+
* Send a message from one agent to another (internal, no email).
|
|
971
|
+
*/
|
|
972
|
+
async sendAgentMessage(from, to, subject, body, priority = "normal") {
|
|
973
|
+
const msg = {
|
|
974
|
+
id: crypto.randomUUID(),
|
|
975
|
+
from,
|
|
976
|
+
to,
|
|
977
|
+
subject,
|
|
978
|
+
body,
|
|
979
|
+
priority,
|
|
980
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
981
|
+
read: false
|
|
982
|
+
};
|
|
983
|
+
if (this.db) {
|
|
984
|
+
await this.db.execute(
|
|
985
|
+
`INSERT INTO agent_messages (id, from_agent, to_agent, subject, body, priority, created_at, read)
|
|
986
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 0)`,
|
|
987
|
+
[msg.id, msg.from, msg.to, msg.subject, msg.body, msg.priority, msg.createdAt]
|
|
988
|
+
).catch(() => {
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
return msg;
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Get unread messages for an agent.
|
|
995
|
+
*/
|
|
996
|
+
async getAgentMessages(agentId, opts) {
|
|
997
|
+
if (!this.db) return [];
|
|
998
|
+
try {
|
|
999
|
+
let sql = "SELECT * FROM agent_messages WHERE to_agent = ?";
|
|
1000
|
+
const params = [agentId];
|
|
1001
|
+
if (opts?.unreadOnly) {
|
|
1002
|
+
sql += " AND read = 0";
|
|
1003
|
+
}
|
|
1004
|
+
sql += " ORDER BY created_at DESC LIMIT ?";
|
|
1005
|
+
params.push(opts?.limit || 20);
|
|
1006
|
+
const rows = await this.db.query(sql, params);
|
|
1007
|
+
return rows.map((r) => ({
|
|
1008
|
+
id: r.id,
|
|
1009
|
+
from: r.from_agent,
|
|
1010
|
+
to: r.to_agent,
|
|
1011
|
+
subject: r.subject,
|
|
1012
|
+
body: r.body,
|
|
1013
|
+
priority: r.priority,
|
|
1014
|
+
createdAt: r.created_at,
|
|
1015
|
+
read: !!r.read
|
|
1016
|
+
}));
|
|
1017
|
+
} catch {
|
|
1018
|
+
return [];
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
// ─── Task Management ────────────────────────────────
|
|
1022
|
+
// Tasks also use the enterprise DB directly.
|
|
1023
|
+
/**
|
|
1024
|
+
* Create a task assigned to an agent.
|
|
1025
|
+
*/
|
|
1026
|
+
async createTask(assigner, assignee, title, description, priority = "normal") {
|
|
1027
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1028
|
+
const task = {
|
|
1029
|
+
id: crypto.randomUUID(),
|
|
1030
|
+
assigner,
|
|
1031
|
+
assignee,
|
|
1032
|
+
title,
|
|
1033
|
+
description,
|
|
1034
|
+
status: "pending",
|
|
1035
|
+
priority,
|
|
1036
|
+
createdAt: now,
|
|
1037
|
+
updatedAt: now
|
|
1038
|
+
};
|
|
1039
|
+
if (this.db) {
|
|
1040
|
+
await this.db.execute(
|
|
1041
|
+
`INSERT INTO agent_tasks (id, assigner, assignee, title, description, status, priority, created_at, updated_at)
|
|
1042
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1043
|
+
[task.id, task.assigner, task.assignee, task.title, task.description || null, task.status, task.priority, task.createdAt, task.updatedAt]
|
|
1044
|
+
).catch(() => {
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
return task;
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Get tasks for an agent.
|
|
1051
|
+
*/
|
|
1052
|
+
async getAgentTasks(agentId, direction = "incoming", status) {
|
|
1053
|
+
if (!this.db) return [];
|
|
1054
|
+
try {
|
|
1055
|
+
const col = direction === "incoming" ? "assignee" : "assigner";
|
|
1056
|
+
let sql = `SELECT * FROM agent_tasks WHERE ${col} = ?`;
|
|
1057
|
+
const params = [agentId];
|
|
1058
|
+
if (status) {
|
|
1059
|
+
sql += " AND status = ?";
|
|
1060
|
+
params.push(status);
|
|
1061
|
+
}
|
|
1062
|
+
sql += " ORDER BY created_at DESC LIMIT 50";
|
|
1063
|
+
const rows = await this.db.query(sql, params);
|
|
1064
|
+
return rows.map((r) => ({
|
|
1065
|
+
id: r.id,
|
|
1066
|
+
assigner: r.assigner,
|
|
1067
|
+
assignee: r.assignee,
|
|
1068
|
+
title: r.title,
|
|
1069
|
+
description: r.description,
|
|
1070
|
+
status: r.status,
|
|
1071
|
+
priority: r.priority,
|
|
1072
|
+
result: r.result ? JSON.parse(r.result) : void 0,
|
|
1073
|
+
createdAt: r.created_at,
|
|
1074
|
+
updatedAt: r.updated_at
|
|
1075
|
+
}));
|
|
1076
|
+
} catch {
|
|
1077
|
+
return [];
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Update task status.
|
|
1082
|
+
*/
|
|
1083
|
+
async updateTask(taskId, updates) {
|
|
1084
|
+
if (!this.db) return;
|
|
1085
|
+
const sets = ["updated_at = ?"];
|
|
1086
|
+
const params = [(/* @__PURE__ */ new Date()).toISOString()];
|
|
1087
|
+
if (updates.status) {
|
|
1088
|
+
sets.push("status = ?");
|
|
1089
|
+
params.push(updates.status);
|
|
1090
|
+
}
|
|
1091
|
+
if (updates.result !== void 0) {
|
|
1092
|
+
sets.push("result = ?");
|
|
1093
|
+
params.push(JSON.stringify(updates.result));
|
|
1094
|
+
}
|
|
1095
|
+
params.push(taskId);
|
|
1096
|
+
await this.db.execute(`UPDATE agent_tasks SET ${sets.join(", ")} WHERE id = ?`, params).catch(() => {
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
// ─── Lifecycle ──────────────────────────────────────
|
|
1100
|
+
/**
|
|
1101
|
+
* Get all registered agents and their email status.
|
|
1102
|
+
*/
|
|
1103
|
+
getRegisteredAgents() {
|
|
1104
|
+
const agents = [];
|
|
1105
|
+
for (const [agentId, identity] of this.identities) {
|
|
1106
|
+
agents.push({ agentId, email: identity.email, provider: identity.provider });
|
|
1107
|
+
}
|
|
1108
|
+
return agents;
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Shutdown — disconnect all providers.
|
|
1112
|
+
*/
|
|
1113
|
+
async shutdown() {
|
|
1114
|
+
for (const provider of this.providers.values()) {
|
|
1115
|
+
await provider.disconnect().catch(() => {
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
this.providers.clear();
|
|
1119
|
+
this.identities.clear();
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
|
|
1123
|
+
export {
|
|
1124
|
+
MicrosoftEmailProvider,
|
|
1125
|
+
GoogleEmailProvider,
|
|
1126
|
+
IMAP_PRESETS,
|
|
1127
|
+
detectImapSettings,
|
|
1128
|
+
ImapEmailProvider,
|
|
1129
|
+
createEmailProvider,
|
|
1130
|
+
AgenticMailManager
|
|
1131
|
+
};
|