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.
Files changed (36) hide show
  1. package/lib/email-accounts.js +299 -0
  2. package/lib/email-mcp-server.js +646 -0
  3. package/lib/project-connection.js +26 -2
  4. package/lib/project-email.js +418 -0
  5. package/lib/project-sessions.js +16 -0
  6. package/lib/project-user-message.js +26 -5
  7. package/lib/project.js +72 -25
  8. package/lib/public/app.js +18 -5
  9. package/lib/public/css/filebrowser.css +80 -2
  10. package/lib/public/css/input.css +196 -0
  11. package/lib/public/css/notifications-center.css +3 -0
  12. package/lib/public/css/sidebar.css +77 -2
  13. package/lib/public/css/sticky-notes.css +0 -48
  14. package/lib/public/css/user-settings.css +85 -0
  15. package/lib/public/icons/email/gmail.svg +7 -0
  16. package/lib/public/icons/email/outlook.svg +35 -0
  17. package/lib/public/icons/email/yahoo.svg +1 -0
  18. package/lib/public/index.html +36 -3
  19. package/lib/public/modules/app-dm.js +4 -9
  20. package/lib/public/modules/app-messages.js +37 -7
  21. package/lib/public/modules/app-panels.js +2 -1
  22. package/lib/public/modules/context-sources.js +527 -1
  23. package/lib/public/modules/filebrowser.js +72 -0
  24. package/lib/public/modules/mate-sidebar.js +7 -0
  25. package/lib/public/modules/sidebar-mobile.js +1 -1
  26. package/lib/public/modules/sidebar.js +144 -2
  27. package/lib/public/modules/sticky-notes.js +1 -91
  28. package/lib/public/modules/terminal.js +0 -12
  29. package/lib/public/modules/theme.js +4 -0
  30. package/lib/public/modules/tools.js +23 -0
  31. package/lib/public/modules/user-settings.js +74 -0
  32. package/lib/sdk-bridge.js +16 -0
  33. package/lib/sdk-message-processor.js +33 -0
  34. package/lib/server-email.js +148 -0
  35. package/lib/server.js +5 -0
  36. package/package.json +3 -2
@@ -0,0 +1,299 @@
1
+ // Email account storage and CRUD for per-user email accounts.
2
+ // Each user's accounts stored at ~/.clay/email/{userId}.json
3
+ // Provides provider presets, connection testing, and account management.
4
+
5
+ var fs = require("fs");
6
+ var path = require("path");
7
+ var crypto = require("crypto");
8
+ var nodemailer = require("nodemailer");
9
+ var { CONFIG_DIR, chmodSafe } = require("./config");
10
+
11
+ var EMAIL_DIR = path.join(CONFIG_DIR, "email");
12
+
13
+ // --- Encryption key (derived from machine-specific secret) ---
14
+
15
+ var _encKey = null;
16
+ function getEncKey() {
17
+ if (_encKey) return _encKey;
18
+ // Derive a stable encryption key from CONFIG_DIR path + a salt file.
19
+ // If the salt file does not exist, create one.
20
+ var saltPath = path.join(CONFIG_DIR, ".email-salt");
21
+ var salt;
22
+ try {
23
+ salt = fs.readFileSync(saltPath, "utf8").trim();
24
+ } catch (e) {
25
+ salt = crypto.randomBytes(32).toString("hex");
26
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
27
+ fs.writeFileSync(saltPath, salt);
28
+ chmodSafe(saltPath, 0o600);
29
+ }
30
+ _encKey = crypto.scryptSync(salt, "clay-email-accounts", 32);
31
+ return _encKey;
32
+ }
33
+
34
+ function encrypt(text) {
35
+ var iv = crypto.randomBytes(16);
36
+ var cipher = crypto.createCipheriv("aes-256-gcm", getEncKey(), iv);
37
+ var encrypted = cipher.update(text, "utf8", "hex") + cipher.final("hex");
38
+ var tag = cipher.getAuthTag().toString("hex");
39
+ return iv.toString("hex") + ":" + tag + ":" + encrypted;
40
+ }
41
+
42
+ function decrypt(data) {
43
+ var parts = data.split(":");
44
+ if (parts.length !== 3) return data; // not encrypted (legacy), return as-is
45
+ var iv = Buffer.from(parts[0], "hex");
46
+ var tag = Buffer.from(parts[1], "hex");
47
+ var encrypted = parts[2];
48
+ var decipher = crypto.createDecipheriv("aes-256-gcm", getEncKey(), iv);
49
+ decipher.setAuthTag(tag);
50
+ return decipher.update(encrypted, "hex", "utf8") + decipher.final("utf8");
51
+ }
52
+
53
+ // --- Provider presets ---
54
+
55
+ var PROVIDER_PRESETS = {
56
+ gmail: {
57
+ label: "Gmail",
58
+ imap: { host: "imap.gmail.com", port: 993, tls: true },
59
+ smtp: { host: "smtp.gmail.com", port: 587 },
60
+ helpUrl: "https://support.google.com/accounts/answer/185833",
61
+ },
62
+ outlook: {
63
+ label: "Outlook",
64
+ imap: { host: "outlook.office365.com", port: 993, tls: true },
65
+ smtp: { host: "smtp.office365.com", port: 587 },
66
+ helpUrl: "https://support.microsoft.com/en-us/account-billing/using-app-passwords-with-apps-that-don-t-support-two-step-verification-5896ed9b-4263-e681-128a-a6f2979a7944",
67
+ },
68
+ yahoo: {
69
+ label: "Yahoo",
70
+ imap: { host: "imap.mail.yahoo.com", port: 993, tls: true },
71
+ smtp: { host: "smtp.mail.yahoo.com", port: 587 },
72
+ helpUrl: "https://help.yahoo.com/kb/generate-manage-third-party-passwords-sln15241.html",
73
+ },
74
+ };
75
+
76
+ // --- Storage helpers ---
77
+
78
+ function ensureEmailDir() {
79
+ fs.mkdirSync(EMAIL_DIR, { recursive: true });
80
+ chmodSafe(EMAIL_DIR, 0o700);
81
+ }
82
+
83
+ function userFilePath(userId) {
84
+ return path.join(EMAIL_DIR, userId + ".json");
85
+ }
86
+
87
+ function loadUserAccounts(userId) {
88
+ try {
89
+ var raw = fs.readFileSync(userFilePath(userId), "utf8");
90
+ var data = JSON.parse(raw);
91
+ return data.accounts || [];
92
+ } catch (e) {
93
+ return [];
94
+ }
95
+ }
96
+
97
+ function saveUserAccounts(userId, accounts) {
98
+ ensureEmailDir();
99
+ var filePath = userFilePath(userId);
100
+ var tmpPath = filePath + ".tmp";
101
+ fs.writeFileSync(tmpPath, JSON.stringify({ accounts: accounts }, null, 2));
102
+ chmodSafe(tmpPath, 0o600);
103
+ fs.renameSync(tmpPath, filePath);
104
+ chmodSafe(filePath, 0o600);
105
+ }
106
+
107
+ // --- Account CRUD ---
108
+
109
+ function generateAccountId() {
110
+ return "acc_" + crypto.randomBytes(8).toString("hex");
111
+ }
112
+
113
+ function listAccounts(userId) {
114
+ var accounts = loadUserAccounts(userId);
115
+ // Return accounts without exposing app passwords
116
+ return accounts.map(function (acc) {
117
+ return {
118
+ id: acc.id,
119
+ email: acc.email,
120
+ provider: acc.provider,
121
+ label: acc.label || "",
122
+ imap: acc.imap,
123
+ smtp: acc.smtp,
124
+ addedAt: acc.addedAt,
125
+ };
126
+ });
127
+ }
128
+
129
+ function getAccount(userId, accountId) {
130
+ var accounts = loadUserAccounts(userId);
131
+ for (var i = 0; i < accounts.length; i++) {
132
+ if (accounts[i].id === accountId) return accounts[i];
133
+ }
134
+ return null;
135
+ }
136
+
137
+ function getAccountByEmail(userId, email) {
138
+ var accounts = loadUserAccounts(userId);
139
+ for (var i = 0; i < accounts.length; i++) {
140
+ if (accounts[i].email === email) return accounts[i];
141
+ }
142
+ return null;
143
+ }
144
+
145
+ function getAccountDecrypted(userId, accountId) {
146
+ var acc = getAccount(userId, accountId);
147
+ if (!acc) return null;
148
+ return Object.assign({}, acc, { appPassword: decrypt(acc.appPassword) });
149
+ }
150
+
151
+ function getAccountByEmailDecrypted(userId, email) {
152
+ var acc = getAccountByEmail(userId, email);
153
+ if (!acc) return null;
154
+ return Object.assign({}, acc, { appPassword: decrypt(acc.appPassword) });
155
+ }
156
+
157
+ function addAccount(userId, opts) {
158
+ var accounts = loadUserAccounts(userId);
159
+
160
+ // Check for duplicate email
161
+ for (var i = 0; i < accounts.length; i++) {
162
+ if (accounts[i].email === opts.email) {
163
+ return { error: "Account with this email already exists" };
164
+ }
165
+ }
166
+
167
+ var provider = opts.provider || "custom";
168
+ var preset = PROVIDER_PRESETS[provider];
169
+
170
+ var account = {
171
+ id: generateAccountId(),
172
+ email: opts.email,
173
+ provider: provider,
174
+ imap: opts.imap || (preset ? Object.assign({}, preset.imap) : { host: "", port: 993, tls: true }),
175
+ smtp: opts.smtp || (preset ? Object.assign({}, preset.smtp) : { host: "", port: 587 }),
176
+ appPassword: encrypt(opts.appPassword),
177
+ addedAt: Date.now(),
178
+ label: opts.label || (preset ? preset.label : "Custom"),
179
+ };
180
+
181
+ accounts.push(account);
182
+ saveUserAccounts(userId, accounts);
183
+
184
+ return {
185
+ ok: true,
186
+ account: {
187
+ id: account.id,
188
+ email: account.email,
189
+ provider: account.provider,
190
+ label: account.label,
191
+ imap: account.imap,
192
+ smtp: account.smtp,
193
+ addedAt: account.addedAt,
194
+ },
195
+ };
196
+ }
197
+
198
+ function removeAccount(userId, accountId) {
199
+ var accounts = loadUserAccounts(userId);
200
+ var filtered = accounts.filter(function (acc) {
201
+ return acc.id !== accountId;
202
+ });
203
+ if (filtered.length === accounts.length) {
204
+ return { error: "Account not found" };
205
+ }
206
+ saveUserAccounts(userId, filtered);
207
+ return { ok: true };
208
+ }
209
+
210
+ // --- Connection testing ---
211
+
212
+ function testSmtpConnection(account) {
213
+ var password = account.appPassword;
214
+ // If encrypted, decrypt
215
+ if (password.indexOf(":") !== -1 && password.length > 50) {
216
+ try { password = decrypt(password); } catch (e) {}
217
+ }
218
+
219
+ var transport = nodemailer.createTransport({
220
+ host: account.smtp.host,
221
+ port: account.smtp.port || 587,
222
+ secure: false,
223
+ auth: { user: account.email, pass: password },
224
+ connectionTimeout: 10000,
225
+ greetingTimeout: 10000,
226
+ });
227
+
228
+ return transport.verify().then(function () {
229
+ try { transport.close(); } catch (e) {}
230
+ return { ok: true };
231
+ }).catch(function (err) {
232
+ try { transport.close(); } catch (e) {}
233
+ return { ok: false, error: err.message || "SMTP connection failed" };
234
+ });
235
+ }
236
+
237
+ function testImapConnection(account) {
238
+ var password = account.appPassword;
239
+ if (password.indexOf(":") !== -1 && password.length > 50) {
240
+ try { password = decrypt(password); } catch (e) {}
241
+ }
242
+
243
+ var ImapFlow;
244
+ try {
245
+ ImapFlow = require("imapflow").ImapFlow;
246
+ } catch (e) {
247
+ return Promise.resolve({ ok: false, error: "imapflow module not installed" });
248
+ }
249
+
250
+ var client = new ImapFlow({
251
+ host: account.imap.host,
252
+ port: account.imap.port || 993,
253
+ secure: account.imap.tls !== false,
254
+ auth: { user: account.email, pass: password },
255
+ logger: false,
256
+ });
257
+
258
+ return client.connect().then(function () {
259
+ return client.logout().then(function () {
260
+ return { ok: true };
261
+ });
262
+ }).catch(function (err) {
263
+ try { client.close(); } catch (e) {}
264
+ return { ok: false, error: err.message || "IMAP connection failed" };
265
+ });
266
+ }
267
+
268
+ function testConnection(account) {
269
+ return Promise.all([
270
+ testImapConnection(account),
271
+ testSmtpConnection(account),
272
+ ]).then(function (results) {
273
+ var imap = results[0];
274
+ var smtp = results[1];
275
+ return {
276
+ imap: imap,
277
+ smtp: smtp,
278
+ ok: imap.ok && smtp.ok,
279
+ };
280
+ });
281
+ }
282
+
283
+ // --- Exports ---
284
+
285
+ module.exports = {
286
+ PROVIDER_PRESETS: PROVIDER_PRESETS,
287
+ listAccounts: listAccounts,
288
+ getAccount: getAccount,
289
+ getAccountByEmail: getAccountByEmail,
290
+ getAccountDecrypted: getAccountDecrypted,
291
+ getAccountByEmailDecrypted: getAccountByEmailDecrypted,
292
+ addAccount: addAccount,
293
+ removeAccount: removeAccount,
294
+ testConnection: testConnection,
295
+ testImapConnection: testImapConnection,
296
+ testSmtpConnection: testSmtpConnection,
297
+ encrypt: encrypt,
298
+ decrypt: decrypt,
299
+ };