clay-server 2.30.0-beta.2 → 2.31.0-beta.1
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/lib/email-accounts.js +299 -0
- package/lib/email-mcp-server.js +646 -0
- package/lib/project-connection.js +26 -2
- package/lib/project-email.js +418 -0
- package/lib/project-sessions.js +16 -0
- package/lib/project-user-message.js +26 -5
- package/lib/project.js +72 -25
- package/lib/public/app.js +18 -5
- package/lib/public/css/filebrowser.css +80 -2
- package/lib/public/css/input.css +196 -0
- package/lib/public/css/notifications-center.css +3 -0
- package/lib/public/css/sidebar.css +77 -2
- package/lib/public/css/sticky-notes.css +0 -48
- package/lib/public/css/user-settings.css +85 -0
- package/lib/public/icons/email/gmail.svg +7 -0
- package/lib/public/icons/email/outlook.svg +35 -0
- package/lib/public/icons/email/yahoo.svg +1 -0
- package/lib/public/index.html +36 -3
- package/lib/public/modules/app-dm.js +4 -9
- package/lib/public/modules/app-messages.js +37 -7
- package/lib/public/modules/app-panels.js +2 -1
- package/lib/public/modules/context-sources.js +527 -1
- package/lib/public/modules/filebrowser.js +72 -0
- package/lib/public/modules/mate-sidebar.js +7 -0
- package/lib/public/modules/sidebar-mobile.js +1 -1
- package/lib/public/modules/sidebar.js +144 -2
- package/lib/public/modules/sticky-notes.js +1 -91
- package/lib/public/modules/terminal.js +0 -12
- package/lib/public/modules/theme.js +4 -0
- package/lib/public/modules/tools.js +23 -0
- package/lib/public/modules/user-settings.js +74 -0
- package/lib/sdk-bridge.js +16 -0
- package/lib/sdk-message-processor.js +33 -0
- package/lib/server-email.js +148 -0
- package/lib/server.js +5 -0
- package/package.json +3 -2
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
// Email MCP Server for Clay (in-process SDK version)
|
|
2
|
+
// Provides email tools (send, read, search, reply, list labels, mark read)
|
|
3
|
+
// to Claude via createSdkMcpServer.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// var emailMcp = require("./email-mcp-server");
|
|
7
|
+
// var mcpConfig = emailMcp.create(getEmailAccount, getCheckedAccounts, smtpConfig);
|
|
8
|
+
// // Pass mcpConfig to sdk-bridge opts.mcpServers
|
|
9
|
+
|
|
10
|
+
var z;
|
|
11
|
+
try { z = require("zod"); } catch (e) { z = null; }
|
|
12
|
+
|
|
13
|
+
function buildShape(props, required) {
|
|
14
|
+
if (!z) return {};
|
|
15
|
+
var shape = {};
|
|
16
|
+
var keys = Object.keys(props);
|
|
17
|
+
for (var i = 0; i < keys.length; i++) {
|
|
18
|
+
var k = keys[i];
|
|
19
|
+
var p = props[k];
|
|
20
|
+
var field;
|
|
21
|
+
if (p.type === "number") field = z.number();
|
|
22
|
+
else if (p.type === "boolean") field = z.boolean();
|
|
23
|
+
else if (p.enum) field = z.enum(p.enum);
|
|
24
|
+
else if (p.type === "array") field = z.array(z.string());
|
|
25
|
+
else field = z.string();
|
|
26
|
+
if (p.description) field = field.describe(p.description);
|
|
27
|
+
if (!required || required.indexOf(k) === -1) field = field.optional();
|
|
28
|
+
shape[k] = field;
|
|
29
|
+
}
|
|
30
|
+
return shape;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// deps:
|
|
34
|
+
// getAccountForTool(email) -> decrypted account object or null
|
|
35
|
+
// getCheckedAccounts() -> array of decrypted accounts (context source checked)
|
|
36
|
+
// getServerSmtp() -> { sendMail(to, subject, html), from } or null
|
|
37
|
+
// appendAuditLog(entry) -> void
|
|
38
|
+
function create(deps) {
|
|
39
|
+
var sdk;
|
|
40
|
+
try { sdk = require("@anthropic-ai/claude-agent-sdk"); } catch (e) {
|
|
41
|
+
console.error("[email-mcp] Failed to load SDK:", e.message);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
var createSdkMcpServer = sdk.createSdkMcpServer;
|
|
46
|
+
var tool = sdk.tool;
|
|
47
|
+
if (!createSdkMcpServer || !tool) {
|
|
48
|
+
console.error("[email-mcp] SDK missing createSdkMcpServer or tool helper");
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
var nodemailer = require("nodemailer");
|
|
53
|
+
var tools = [];
|
|
54
|
+
|
|
55
|
+
// --- Helper: get SMTP transport for a personal account ---
|
|
56
|
+
function getPersonalTransport(account) {
|
|
57
|
+
return nodemailer.createTransport({
|
|
58
|
+
host: account.smtp.host,
|
|
59
|
+
port: account.smtp.port || 587,
|
|
60
|
+
secure: false,
|
|
61
|
+
auth: { user: account.email, pass: account.appPassword },
|
|
62
|
+
connectionTimeout: 10000,
|
|
63
|
+
greetingTimeout: 10000,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- Helper: get IMAP client for a personal account ---
|
|
68
|
+
function getImapClient(account) {
|
|
69
|
+
var ImapFlow;
|
|
70
|
+
try { ImapFlow = require("imapflow").ImapFlow; } catch (e) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return new ImapFlow({
|
|
74
|
+
host: account.imap.host,
|
|
75
|
+
port: account.imap.port || 993,
|
|
76
|
+
secure: account.imap.tls !== false,
|
|
77
|
+
auth: { user: account.email, pass: account.appPassword },
|
|
78
|
+
logger: false,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- clay_send_email ---
|
|
83
|
+
tools.push(tool(
|
|
84
|
+
"clay_send_email",
|
|
85
|
+
"Send an email. Use via='server' for auditable server SMTP, via='personal' for user's own account, or omit for auto-detect (personal if available, otherwise server).",
|
|
86
|
+
buildShape({
|
|
87
|
+
to: { type: "string", description: "Comma-separated recipient email addresses" },
|
|
88
|
+
subject: { type: "string", description: "Email subject line" },
|
|
89
|
+
body: { type: "string", description: "Email body (plain text)" },
|
|
90
|
+
account: { type: "string", description: "Sender email address (for personal mode). If omitted, uses first checked account." },
|
|
91
|
+
via: { type: "string", description: "Send mode: 'server' (admin SMTP, audited) or 'personal' (user account). Omit for auto-detect." },
|
|
92
|
+
cc: { type: "string", description: "Comma-separated CC addresses" },
|
|
93
|
+
bcc: { type: "string", description: "Comma-separated BCC addresses" },
|
|
94
|
+
}, ["to", "subject", "body"]),
|
|
95
|
+
function (args) {
|
|
96
|
+
var via = args.via || "auto";
|
|
97
|
+
var recipients = args.to.split(",").map(function (s) { return s.trim(); }).filter(Boolean);
|
|
98
|
+
|
|
99
|
+
if (via === "server" || (via === "auto" && !deps.getCheckedAccounts().length)) {
|
|
100
|
+
// Server SMTP mode
|
|
101
|
+
var serverSmtp = deps.getServerSmtp();
|
|
102
|
+
if (!serverSmtp) {
|
|
103
|
+
return Promise.resolve({ content: [{ type: "text", text: "Error: Server SMTP is not configured. Ask the admin to set it up, or connect a personal email account." }] });
|
|
104
|
+
}
|
|
105
|
+
return serverSmtp.sendMail(recipients.join(", "), args.subject, args.body).then(function (info) {
|
|
106
|
+
// Audit log
|
|
107
|
+
deps.appendAuditLog({
|
|
108
|
+
ts: Date.now(),
|
|
109
|
+
to: recipients,
|
|
110
|
+
subject: args.subject,
|
|
111
|
+
status: "sent",
|
|
112
|
+
messageId: info.messageId || null,
|
|
113
|
+
});
|
|
114
|
+
return { content: [{ type: "text", text: "Email sent via server SMTP to " + recipients.join(", ") + ". Subject: " + args.subject }] };
|
|
115
|
+
}).catch(function (err) {
|
|
116
|
+
return { content: [{ type: "text", text: "Error sending via server SMTP: " + (err.message || "Unknown error") }] };
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Personal account mode
|
|
121
|
+
var account;
|
|
122
|
+
if (args.account) {
|
|
123
|
+
account = deps.getAccountForTool(args.account);
|
|
124
|
+
} else {
|
|
125
|
+
var checked = deps.getCheckedAccounts();
|
|
126
|
+
account = checked.length > 0 ? checked[0] : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!account) {
|
|
130
|
+
return Promise.resolve({ content: [{ type: "text", text: "Error: No personal email account available. The user needs to add and check an email account in Context Sources." }] });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
var transport = getPersonalTransport(account);
|
|
134
|
+
var mailOpts = {
|
|
135
|
+
from: account.email,
|
|
136
|
+
to: recipients.join(", "),
|
|
137
|
+
subject: args.subject,
|
|
138
|
+
text: args.body,
|
|
139
|
+
};
|
|
140
|
+
if (args.cc) mailOpts.cc = args.cc;
|
|
141
|
+
if (args.bcc) mailOpts.bcc = args.bcc;
|
|
142
|
+
|
|
143
|
+
return transport.sendMail(mailOpts).then(function (info) {
|
|
144
|
+
try { transport.close(); } catch (e) {}
|
|
145
|
+
return { content: [{ type: "text", text: "Email sent from " + account.email + " to " + recipients.join(", ") + ". Subject: " + args.subject }] };
|
|
146
|
+
}).catch(function (err) {
|
|
147
|
+
try { transport.close(); } catch (e) {}
|
|
148
|
+
return { content: [{ type: "text", text: "Error sending from " + account.email + ": " + (err.message || "Unknown error") }] };
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
));
|
|
152
|
+
|
|
153
|
+
// --- clay_read_email ---
|
|
154
|
+
tools.push(tool(
|
|
155
|
+
"clay_read_email",
|
|
156
|
+
"Read recent emails from inbox or a specified folder. Returns message list with snippets. Use clay_read_email_body to get full content.",
|
|
157
|
+
buildShape({
|
|
158
|
+
account: { type: "string", description: "Email address of the account to read from. If omitted, uses first checked account." },
|
|
159
|
+
folder: { type: "string", description: "Folder/mailbox name (default: INBOX)" },
|
|
160
|
+
limit: { type: "number", description: "Number of messages to fetch (default 10, max 50)" },
|
|
161
|
+
unread_only: { type: "boolean", description: "Only fetch unread messages (default false)" },
|
|
162
|
+
}, []),
|
|
163
|
+
function (args) {
|
|
164
|
+
var account;
|
|
165
|
+
if (args.account) {
|
|
166
|
+
account = deps.getAccountForTool(args.account);
|
|
167
|
+
} else {
|
|
168
|
+
var checked = deps.getCheckedAccounts();
|
|
169
|
+
account = checked.length > 0 ? checked[0] : null;
|
|
170
|
+
}
|
|
171
|
+
if (!account) {
|
|
172
|
+
return Promise.resolve({ content: [{ type: "text", text: "Error: No email account available. The user needs to add and check an email account." }] });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
var client = getImapClient(account);
|
|
176
|
+
if (!client) {
|
|
177
|
+
return Promise.resolve({ content: [{ type: "text", text: "Error: IMAP module (imapflow) not available." }] });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
var folder = args.folder || "INBOX";
|
|
181
|
+
var limit = Math.min(Math.max(args.limit || 10, 1), 50);
|
|
182
|
+
var unreadOnly = !!args.unread_only;
|
|
183
|
+
|
|
184
|
+
return client.connect().then(function () {
|
|
185
|
+
return client.getMailboxLock(folder);
|
|
186
|
+
}).then(function (lock) {
|
|
187
|
+
var searchCriteria = unreadOnly ? { seen: false } : { all: true };
|
|
188
|
+
return client.search(searchCriteria, { uid: true }).then(function (uids) {
|
|
189
|
+
if (!uids || uids.length === 0) {
|
|
190
|
+
lock.release();
|
|
191
|
+
return client.logout().then(function () {
|
|
192
|
+
return { content: [{ type: "text", text: JSON.stringify({ messages: [], total: 0, unread: 0 }, null, 2) }] };
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Get the most recent N UIDs
|
|
197
|
+
var recentUids = uids.slice(-limit).reverse();
|
|
198
|
+
|
|
199
|
+
// client.fetch is async*, returns AsyncGenerator directly
|
|
200
|
+
var fetchIter = client.fetch(recentUids, {
|
|
201
|
+
uid: true,
|
|
202
|
+
envelope: true,
|
|
203
|
+
flags: true,
|
|
204
|
+
bodyStructure: true,
|
|
205
|
+
source: { start: 0, maxLength: 500 },
|
|
206
|
+
}, { uid: true });
|
|
207
|
+
var messages = [];
|
|
208
|
+
|
|
209
|
+
function collectMessages() {
|
|
210
|
+
return fetchIter.next().then(function (result) {
|
|
211
|
+
if (result.done) return;
|
|
212
|
+
var msg = result.value;
|
|
213
|
+
var env = msg.envelope || {};
|
|
214
|
+
var fromAddr = env.from && env.from[0] ? (env.from[0].address || "") : "";
|
|
215
|
+
var toAddrs = (env.to || []).map(function (a) { return a.address || ""; });
|
|
216
|
+
var snippet = "";
|
|
217
|
+
if (msg.source) {
|
|
218
|
+
snippet = msg.source.toString("utf8").replace(/\r?\n/g, " ").substring(0, 200);
|
|
219
|
+
}
|
|
220
|
+
messages.push({
|
|
221
|
+
uid: msg.uid,
|
|
222
|
+
from: fromAddr,
|
|
223
|
+
to: toAddrs,
|
|
224
|
+
subject: env.subject || "(no subject)",
|
|
225
|
+
date: env.date ? env.date.toISOString() : null,
|
|
226
|
+
snippet: snippet,
|
|
227
|
+
unread: !msg.flags || !msg.flags.has("\\Seen"),
|
|
228
|
+
});
|
|
229
|
+
return collectMessages();
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return collectMessages().then(function () {
|
|
234
|
+
lock.release();
|
|
235
|
+
return client.logout().then(function () {
|
|
236
|
+
return {
|
|
237
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
238
|
+
messages: messages,
|
|
239
|
+
total: uids.length,
|
|
240
|
+
account: account.email,
|
|
241
|
+
folder: folder,
|
|
242
|
+
}, null, 2) }],
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
}).catch(function (err) {
|
|
248
|
+
try { client.close(); } catch (e) {}
|
|
249
|
+
return { content: [{ type: "text", text: "Error reading email from " + account.email + ": " + (err.message || "Unknown error") }] };
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
));
|
|
253
|
+
|
|
254
|
+
// --- clay_read_email_body ---
|
|
255
|
+
tools.push(tool(
|
|
256
|
+
"clay_read_email_body",
|
|
257
|
+
"Read the full body of a specific email by UID. Returns plain text content (HTML stripped). Truncated at 10,000 characters.",
|
|
258
|
+
buildShape({
|
|
259
|
+
account: { type: "string", description: "Email address of the account. If omitted, uses first checked account." },
|
|
260
|
+
uid: { type: "number", description: "Message UID (from clay_read_email results)" },
|
|
261
|
+
folder: { type: "string", description: "Folder/mailbox name (default: INBOX)" },
|
|
262
|
+
}, ["uid"]),
|
|
263
|
+
function (args) {
|
|
264
|
+
var account;
|
|
265
|
+
if (args.account) {
|
|
266
|
+
account = deps.getAccountForTool(args.account);
|
|
267
|
+
} else {
|
|
268
|
+
var checked = deps.getCheckedAccounts();
|
|
269
|
+
account = checked.length > 0 ? checked[0] : null;
|
|
270
|
+
}
|
|
271
|
+
if (!account) {
|
|
272
|
+
return Promise.resolve({ content: [{ type: "text", text: "Error: No email account available." }] });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
var client = getImapClient(account);
|
|
276
|
+
if (!client) {
|
|
277
|
+
return Promise.resolve({ content: [{ type: "text", text: "Error: IMAP module not available." }] });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
var folder = args.folder || "INBOX";
|
|
281
|
+
var MAX_BODY = 10000;
|
|
282
|
+
|
|
283
|
+
return client.connect().then(function () {
|
|
284
|
+
return client.getMailboxLock(folder);
|
|
285
|
+
}).then(function (lock) {
|
|
286
|
+
return client.fetchOne(args.uid, {
|
|
287
|
+
uid: true,
|
|
288
|
+
source: true,
|
|
289
|
+
}, { uid: true }).then(function (msg) {
|
|
290
|
+
lock.release();
|
|
291
|
+
return client.logout().then(function () {
|
|
292
|
+
if (!msg || !msg.source) {
|
|
293
|
+
return { content: [{ type: "text", text: "Message not found or empty." }] };
|
|
294
|
+
}
|
|
295
|
+
var raw = msg.source.toString("utf8");
|
|
296
|
+
// Simple extraction: try to get text content after headers
|
|
297
|
+
var bodyStart = raw.indexOf("\r\n\r\n");
|
|
298
|
+
if (bodyStart === -1) bodyStart = raw.indexOf("\n\n");
|
|
299
|
+
var body = bodyStart !== -1 ? raw.substring(bodyStart + (raw[bodyStart + 2] === "\n" ? 4 : 2)) : raw;
|
|
300
|
+
// Strip HTML tags for a rough plain text extraction
|
|
301
|
+
body = body.replace(/<[^>]+>/g, "").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
302
|
+
// Clean up whitespace
|
|
303
|
+
body = body.replace(/\r\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
304
|
+
if (body.length > MAX_BODY) {
|
|
305
|
+
body = body.substring(0, MAX_BODY) + "\n\n[Truncated at " + MAX_BODY + " characters]";
|
|
306
|
+
}
|
|
307
|
+
return { content: [{ type: "text", text: body }] };
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
}).catch(function (err) {
|
|
311
|
+
try { client.close(); } catch (e) {}
|
|
312
|
+
return { content: [{ type: "text", text: "Error reading email body: " + (err.message || "Unknown error") }] };
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
));
|
|
316
|
+
|
|
317
|
+
// --- clay_search_email ---
|
|
318
|
+
tools.push(tool(
|
|
319
|
+
"clay_search_email",
|
|
320
|
+
"Search emails using IMAP search criteria. Supports basic criteria like from, subject, since, before, and text content.",
|
|
321
|
+
buildShape({
|
|
322
|
+
account: { type: "string", description: "Email address of the account. If omitted, uses first checked account." },
|
|
323
|
+
from: { type: "string", description: "Filter by sender address (partial match)" },
|
|
324
|
+
subject: { type: "string", description: "Filter by subject text (partial match)" },
|
|
325
|
+
text: { type: "string", description: "Search in entire message body and headers" },
|
|
326
|
+
since: { type: "string", description: "Messages after this date (ISO 8601 format, e.g. 2026-04-15)" },
|
|
327
|
+
before: { type: "string", description: "Messages before this date (ISO 8601 format)" },
|
|
328
|
+
folder: { type: "string", description: "Folder/mailbox name (default: INBOX)" },
|
|
329
|
+
limit: { type: "number", description: "Max results (default 20, max 50)" },
|
|
330
|
+
}, []),
|
|
331
|
+
function (args) {
|
|
332
|
+
var account;
|
|
333
|
+
if (args.account) {
|
|
334
|
+
account = deps.getAccountForTool(args.account);
|
|
335
|
+
} else {
|
|
336
|
+
var checked = deps.getCheckedAccounts();
|
|
337
|
+
account = checked.length > 0 ? checked[0] : null;
|
|
338
|
+
}
|
|
339
|
+
if (!account) {
|
|
340
|
+
return Promise.resolve({ content: [{ type: "text", text: "Error: No email account available." }] });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
var client = getImapClient(account);
|
|
344
|
+
if (!client) {
|
|
345
|
+
return Promise.resolve({ content: [{ type: "text", text: "Error: IMAP module not available." }] });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
var folder = args.folder || "INBOX";
|
|
349
|
+
var limit = Math.min(Math.max(args.limit || 20, 1), 50);
|
|
350
|
+
|
|
351
|
+
// Build IMAP search criteria
|
|
352
|
+
var criteria = {};
|
|
353
|
+
if (args.from) criteria.from = args.from;
|
|
354
|
+
if (args.subject) criteria.subject = args.subject;
|
|
355
|
+
if (args.text) criteria.body = args.text;
|
|
356
|
+
if (args.since) criteria.since = new Date(args.since);
|
|
357
|
+
if (args.before) criteria.before = new Date(args.before);
|
|
358
|
+
if (Object.keys(criteria).length === 0) criteria.all = true;
|
|
359
|
+
|
|
360
|
+
return client.connect().then(function () {
|
|
361
|
+
return client.getMailboxLock(folder);
|
|
362
|
+
}).then(function (lock) {
|
|
363
|
+
return client.search(criteria, { uid: true }).then(function (uids) {
|
|
364
|
+
if (!uids || uids.length === 0) {
|
|
365
|
+
lock.release();
|
|
366
|
+
return client.logout().then(function () {
|
|
367
|
+
return { content: [{ type: "text", text: JSON.stringify({ messages: [], total: 0 }, null, 2) }] };
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
var recentUids = uids.slice(-limit).reverse();
|
|
372
|
+
|
|
373
|
+
var fetchIter2 = client.fetch(recentUids, {
|
|
374
|
+
uid: true,
|
|
375
|
+
envelope: true,
|
|
376
|
+
flags: true,
|
|
377
|
+
}, { uid: true });
|
|
378
|
+
var messages = [];
|
|
379
|
+
|
|
380
|
+
function collect() {
|
|
381
|
+
return fetchIter2.next().then(function (result) {
|
|
382
|
+
if (result.done) return;
|
|
383
|
+
var msg = result.value;
|
|
384
|
+
var env = msg.envelope || {};
|
|
385
|
+
messages.push({
|
|
386
|
+
uid: msg.uid,
|
|
387
|
+
from: env.from && env.from[0] ? env.from[0].address : "",
|
|
388
|
+
subject: env.subject || "(no subject)",
|
|
389
|
+
date: env.date ? env.date.toISOString() : null,
|
|
390
|
+
unread: !msg.flags || !msg.flags.has("\\Seen"),
|
|
391
|
+
});
|
|
392
|
+
return collect();
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return collect().then(function () {
|
|
397
|
+
lock.release();
|
|
398
|
+
return client.logout().then(function () {
|
|
399
|
+
return { content: [{ type: "text", text: JSON.stringify({ messages: messages, total: uids.length, account: account.email }, null, 2) }] };
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
}).catch(function (err) {
|
|
404
|
+
try { client.close(); } catch (e) {}
|
|
405
|
+
return { content: [{ type: "text", text: "Error searching email: " + (err.message || "Unknown error") }] };
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
));
|
|
409
|
+
|
|
410
|
+
// --- clay_reply_email ---
|
|
411
|
+
tools.push(tool(
|
|
412
|
+
"clay_reply_email",
|
|
413
|
+
"Reply to an email by UID. Automatically sets In-Reply-To and References headers to preserve threading.",
|
|
414
|
+
buildShape({
|
|
415
|
+
account: { type: "string", description: "Email address of the account. If omitted, uses first checked account." },
|
|
416
|
+
uid: { type: "number", description: "Message UID to reply to (from clay_read_email results)" },
|
|
417
|
+
body: { type: "string", description: "Reply body text" },
|
|
418
|
+
reply_all: { type: "boolean", description: "Reply to all recipients (default false)" },
|
|
419
|
+
folder: { type: "string", description: "Folder/mailbox name (default: INBOX)" },
|
|
420
|
+
}, ["uid", "body"]),
|
|
421
|
+
function (args) {
|
|
422
|
+
var account;
|
|
423
|
+
if (args.account) {
|
|
424
|
+
account = deps.getAccountForTool(args.account);
|
|
425
|
+
} else {
|
|
426
|
+
var checked = deps.getCheckedAccounts();
|
|
427
|
+
account = checked.length > 0 ? checked[0] : null;
|
|
428
|
+
}
|
|
429
|
+
if (!account) {
|
|
430
|
+
return Promise.resolve({ content: [{ type: "text", text: "Error: No email account available." }] });
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
var client = getImapClient(account);
|
|
434
|
+
if (!client) {
|
|
435
|
+
return Promise.resolve({ content: [{ type: "text", text: "Error: IMAP module not available." }] });
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
var folder = args.folder || "INBOX";
|
|
439
|
+
|
|
440
|
+
return client.connect().then(function () {
|
|
441
|
+
return client.getMailboxLock(folder);
|
|
442
|
+
}).then(function (lock) {
|
|
443
|
+
return client.fetchOne(args.uid, {
|
|
444
|
+
uid: true,
|
|
445
|
+
envelope: true,
|
|
446
|
+
}, { uid: true }).then(function (msg) {
|
|
447
|
+
lock.release();
|
|
448
|
+
return client.logout().then(function () {
|
|
449
|
+
if (!msg || !msg.envelope) {
|
|
450
|
+
return { content: [{ type: "text", text: "Original message not found." }] };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
var env = msg.envelope;
|
|
454
|
+
var replyTo = env.replyTo && env.replyTo[0] ? env.replyTo[0].address : (env.from && env.from[0] ? env.from[0].address : null);
|
|
455
|
+
if (!replyTo) {
|
|
456
|
+
return { content: [{ type: "text", text: "Cannot determine reply address from original message." }] };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
var mailOpts = {
|
|
460
|
+
from: account.email,
|
|
461
|
+
to: replyTo,
|
|
462
|
+
subject: "Re: " + (env.subject || ""),
|
|
463
|
+
text: args.body,
|
|
464
|
+
inReplyTo: env.messageId || undefined,
|
|
465
|
+
references: env.messageId || undefined,
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
if (args.reply_all) {
|
|
469
|
+
var allTo = (env.to || []).map(function (a) { return a.address; }).filter(function (a) {
|
|
470
|
+
return a && a !== account.email;
|
|
471
|
+
});
|
|
472
|
+
var allCc = (env.cc || []).map(function (a) { return a.address; }).filter(function (a) {
|
|
473
|
+
return a && a !== account.email;
|
|
474
|
+
});
|
|
475
|
+
if (allTo.length > 0) mailOpts.to = [replyTo].concat(allTo).join(", ");
|
|
476
|
+
if (allCc.length > 0) mailOpts.cc = allCc.join(", ");
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
var transport = getPersonalTransport(account);
|
|
480
|
+
return transport.sendMail(mailOpts).then(function () {
|
|
481
|
+
try { transport.close(); } catch (e) {}
|
|
482
|
+
return { content: [{ type: "text", text: "Reply sent from " + account.email + " to " + mailOpts.to + ". Subject: " + mailOpts.subject }] };
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
}).catch(function (err) {
|
|
487
|
+
try { client.close(); } catch (e) {}
|
|
488
|
+
return { content: [{ type: "text", text: "Error replying to email: " + (err.message || "Unknown error") }] };
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
));
|
|
492
|
+
|
|
493
|
+
// --- clay_list_labels ---
|
|
494
|
+
tools.push(tool(
|
|
495
|
+
"clay_list_labels",
|
|
496
|
+
"List all email folders/labels with message counts for an account.",
|
|
497
|
+
buildShape({
|
|
498
|
+
account: { type: "string", description: "Email address of the account. If omitted, uses first checked account." },
|
|
499
|
+
}, []),
|
|
500
|
+
function (args) {
|
|
501
|
+
var account;
|
|
502
|
+
if (args.account) {
|
|
503
|
+
account = deps.getAccountForTool(args.account);
|
|
504
|
+
} else {
|
|
505
|
+
var checked = deps.getCheckedAccounts();
|
|
506
|
+
account = checked.length > 0 ? checked[0] : null;
|
|
507
|
+
}
|
|
508
|
+
if (!account) {
|
|
509
|
+
return Promise.resolve({ content: [{ type: "text", text: "Error: No email account available." }] });
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
var client = getImapClient(account);
|
|
513
|
+
if (!client) {
|
|
514
|
+
return Promise.resolve({ content: [{ type: "text", text: "Error: IMAP module not available." }] });
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return client.connect().then(function () {
|
|
518
|
+
return client.list();
|
|
519
|
+
}).then(function (mailboxes) {
|
|
520
|
+
var folders = mailboxes.map(function (mb) {
|
|
521
|
+
return {
|
|
522
|
+
name: mb.name,
|
|
523
|
+
path: mb.path,
|
|
524
|
+
delimiter: mb.delimiter,
|
|
525
|
+
flags: Array.from(mb.flags || []),
|
|
526
|
+
specialUse: mb.specialUse || null,
|
|
527
|
+
};
|
|
528
|
+
});
|
|
529
|
+
return client.logout().then(function () {
|
|
530
|
+
return { content: [{ type: "text", text: JSON.stringify({ folders: folders, account: account.email }, null, 2) }] };
|
|
531
|
+
});
|
|
532
|
+
}).catch(function (err) {
|
|
533
|
+
try { client.close(); } catch (e) {}
|
|
534
|
+
return { content: [{ type: "text", text: "Error listing folders: " + (err.message || "Unknown error") }] };
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
));
|
|
538
|
+
|
|
539
|
+
// --- clay_mark_read ---
|
|
540
|
+
tools.push(tool(
|
|
541
|
+
"clay_mark_read",
|
|
542
|
+
"Mark one or more emails as read by UID.",
|
|
543
|
+
buildShape({
|
|
544
|
+
account: { type: "string", description: "Email address of the account. If omitted, uses first checked account." },
|
|
545
|
+
uids: { type: "string", description: "Comma-separated message UIDs to mark as read" },
|
|
546
|
+
folder: { type: "string", description: "Folder/mailbox name (default: INBOX)" },
|
|
547
|
+
}, ["uids"]),
|
|
548
|
+
function (args) {
|
|
549
|
+
var account;
|
|
550
|
+
if (args.account) {
|
|
551
|
+
account = deps.getAccountForTool(args.account);
|
|
552
|
+
} else {
|
|
553
|
+
var checked = deps.getCheckedAccounts();
|
|
554
|
+
account = checked.length > 0 ? checked[0] : null;
|
|
555
|
+
}
|
|
556
|
+
if (!account) {
|
|
557
|
+
return Promise.resolve({ content: [{ type: "text", text: "Error: No email account available." }] });
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
var client = getImapClient(account);
|
|
561
|
+
if (!client) {
|
|
562
|
+
return Promise.resolve({ content: [{ type: "text", text: "Error: IMAP module not available." }] });
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
var folder = args.folder || "INBOX";
|
|
566
|
+
var uidList = args.uids.split(",").map(function (s) { return parseInt(s.trim(), 10); }).filter(function (n) { return !isNaN(n); });
|
|
567
|
+
|
|
568
|
+
if (uidList.length === 0) {
|
|
569
|
+
return Promise.resolve({ content: [{ type: "text", text: "Error: No valid UIDs provided." }] });
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return client.connect().then(function () {
|
|
573
|
+
return client.getMailboxLock(folder);
|
|
574
|
+
}).then(function (lock) {
|
|
575
|
+
return client.messageFlagsAdd(uidList, ["\\Seen"], { uid: true }).then(function () {
|
|
576
|
+
lock.release();
|
|
577
|
+
return client.logout().then(function () {
|
|
578
|
+
return { content: [{ type: "text", text: "Marked " + uidList.length + " message(s) as read in " + account.email + "/" + folder }] };
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
}).catch(function (err) {
|
|
582
|
+
try { client.close(); } catch (e) {}
|
|
583
|
+
return { content: [{ type: "text", text: "Error marking as read: " + (err.message || "Unknown error") }] };
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
));
|
|
587
|
+
|
|
588
|
+
// --- clay_move_email ---
|
|
589
|
+
tools.push(tool(
|
|
590
|
+
"clay_move_email",
|
|
591
|
+
"Move one or more emails to a different folder/label by UID. Use clay_list_labels to see available folders. In Gmail, moving to a folder is equivalent to applying that label.",
|
|
592
|
+
buildShape({
|
|
593
|
+
account: { type: "string", description: "Email address of the account. If omitted, uses first checked account." },
|
|
594
|
+
uids: { type: "string", description: "Comma-separated message UIDs to move" },
|
|
595
|
+
from_folder: { type: "string", description: "Source folder (default: INBOX)" },
|
|
596
|
+
to_folder: { type: "string", description: "Destination folder/label name (e.g. 'Work', '[Gmail]/Trash', '[Gmail]/All Mail')" },
|
|
597
|
+
}, ["uids", "to_folder"]),
|
|
598
|
+
function (args) {
|
|
599
|
+
var account;
|
|
600
|
+
if (args.account) {
|
|
601
|
+
account = deps.getAccountForTool(args.account);
|
|
602
|
+
} else {
|
|
603
|
+
var checked = deps.getCheckedAccounts();
|
|
604
|
+
account = checked.length > 0 ? checked[0] : null;
|
|
605
|
+
}
|
|
606
|
+
if (!account) {
|
|
607
|
+
return Promise.resolve({ content: [{ type: "text", text: "Error: No email account available." }] });
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
var client = getImapClient(account);
|
|
611
|
+
if (!client) {
|
|
612
|
+
return Promise.resolve({ content: [{ type: "text", text: "Error: IMAP module not available." }] });
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
var fromFolder = args.from_folder || "INBOX";
|
|
616
|
+
var toFolder = args.to_folder;
|
|
617
|
+
var uidList = args.uids.split(",").map(function (s) { return parseInt(s.trim(), 10); }).filter(function (n) { return !isNaN(n); });
|
|
618
|
+
|
|
619
|
+
if (uidList.length === 0) {
|
|
620
|
+
return Promise.resolve({ content: [{ type: "text", text: "Error: No valid UIDs provided." }] });
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return client.connect().then(function () {
|
|
624
|
+
return client.getMailboxLock(fromFolder);
|
|
625
|
+
}).then(function (lock) {
|
|
626
|
+
return client.messageMove(uidList, toFolder, { uid: true }).then(function () {
|
|
627
|
+
lock.release();
|
|
628
|
+
return client.logout().then(function () {
|
|
629
|
+
return { content: [{ type: "text", text: "Moved " + uidList.length + " message(s) from " + fromFolder + " to " + toFolder + " in " + account.email }] };
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
}).catch(function (err) {
|
|
633
|
+
try { client.close(); } catch (e) {}
|
|
634
|
+
return { content: [{ type: "text", text: "Error moving email: " + (err.message || "Unknown error") }] };
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
));
|
|
638
|
+
|
|
639
|
+
return createSdkMcpServer({
|
|
640
|
+
name: "clay-email",
|
|
641
|
+
version: "1.0.0",
|
|
642
|
+
tools: tools,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
module.exports = { create: create };
|