@agenticmail/api 0.5.59 → 0.5.61

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 (2) hide show
  1. package/dist/index.js +91 -48
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -264,17 +264,17 @@ function createAccountRoutes(accountManager, db, config) {
264
264
  const router = Router2();
265
265
  const deletionService = new AgentDeletionService(db, accountManager, config);
266
266
  router.post("/accounts", requireMaster, async (req, res, next) => {
267
+ if (!req.body || typeof req.body !== "object") {
268
+ res.status(400).json({ error: "Request body must be JSON" });
269
+ return;
270
+ }
271
+ const { name: accountName, domain, password, metadata, role, persistent } = req.body;
267
272
  try {
268
- if (!req.body || typeof req.body !== "object") {
269
- res.status(400).json({ error: "Request body must be JSON" });
270
- return;
271
- }
272
- const { name: name2, domain, password, metadata, role, persistent } = req.body;
273
- if (!name2 || typeof name2 !== "string") {
273
+ if (!accountName || typeof accountName !== "string") {
274
274
  res.status(400).json({ error: "name is required and must be a string" });
275
275
  return;
276
276
  }
277
- if (name2.length > 64) {
277
+ if (accountName.length > 64) {
278
278
  res.status(400).json({ error: "name must be 64 characters or fewer" });
279
279
  return;
280
280
  }
@@ -293,7 +293,7 @@ function createAccountRoutes(accountManager, db, config) {
293
293
  const cleanMeta = metadata ? Object.fromEntries(
294
294
  Object.entries(metadata).filter(([k]) => !k.startsWith("_"))
295
295
  ) : void 0;
296
- const agent = await accountManager.create({ name: name2, domain, password: password || void 0, metadata: cleanMeta, role });
296
+ const agent = await accountManager.create({ name: accountName, domain, password: password || void 0, metadata: cleanMeta, role });
297
297
  try {
298
298
  db.prepare("UPDATE agents SET last_activity_at = datetime('now') WHERE id = ?").run(agent.id);
299
299
  } catch {
@@ -310,7 +310,7 @@ function createAccountRoutes(accountManager, db, config) {
310
310
  } catch (err) {
311
311
  const msg = err.message ?? "";
312
312
  if (msg.includes("UNIQUE") || msg.includes("unique") || msg.includes("already exists") || msg.includes("duplicate") || msg.includes("fieldAlreadyExists") || msg.toLowerCase().includes("alreadyexists")) {
313
- res.status(409).json({ error: "Account already exists", name });
313
+ res.status(409).json({ error: "Account already exists", name: accountName });
314
314
  return;
315
315
  }
316
316
  next(err);
@@ -644,13 +644,13 @@ function createFeatureRoutes(db, _accountManager, config, gatewayManager) {
644
644
  });
645
645
  router.post("/contacts", requireAgent, async (req, res, next) => {
646
646
  try {
647
- const { name: name2, email, notes } = req.body || {};
647
+ const { name, email, notes } = req.body || {};
648
648
  if (!email) {
649
649
  res.status(400).json({ error: "email is required" });
650
650
  return;
651
651
  }
652
652
  const id = uuidv4();
653
- db.prepare("INSERT OR REPLACE INTO contacts (id, agent_id, name, email, notes) VALUES (?, ?, ?, ?, ?)").run(id, req.agent.id, name2 || null, email, notes || null);
653
+ db.prepare("INSERT OR REPLACE INTO contacts (id, agent_id, name, email, notes) VALUES (?, ?, ?, ?, ?)").run(id, req.agent.id, name || null, email, notes || null);
654
654
  res.json({ ok: true, id, email });
655
655
  } catch (err) {
656
656
  next(err);
@@ -795,8 +795,8 @@ function createFeatureRoutes(db, _accountManager, config, gatewayManager) {
795
795
  });
796
796
  router.post("/signatures", requireAgent, async (req, res, next) => {
797
797
  try {
798
- const { name: name2, text, html, isDefault } = req.body || {};
799
- if (!name2) {
798
+ const { name, text, html, isDefault } = req.body || {};
799
+ if (!name) {
800
800
  res.status(400).json({ error: "name is required" });
801
801
  return;
802
802
  }
@@ -804,7 +804,7 @@ function createFeatureRoutes(db, _accountManager, config, gatewayManager) {
804
804
  if (isDefault) {
805
805
  db.prepare("UPDATE signatures SET is_default = 0 WHERE agent_id = ?").run(req.agent.id);
806
806
  }
807
- db.prepare("INSERT OR REPLACE INTO signatures (id, agent_id, name, text_content, html_content, is_default) VALUES (?, ?, ?, ?, ?, ?)").run(id, req.agent.id, name2, text || null, html || null, isDefault ? 1 : 0);
807
+ db.prepare("INSERT OR REPLACE INTO signatures (id, agent_id, name, text_content, html_content, is_default) VALUES (?, ?, ?, ?, ?, ?)").run(id, req.agent.id, name, text || null, html || null, isDefault ? 1 : 0);
808
808
  res.json({ ok: true, id });
809
809
  } catch (err) {
810
810
  next(err);
@@ -832,13 +832,13 @@ function createFeatureRoutes(db, _accountManager, config, gatewayManager) {
832
832
  });
833
833
  router.post("/templates", requireAgent, async (req, res, next) => {
834
834
  try {
835
- const { name: name2, subject, text, html } = req.body || {};
836
- if (!name2) {
835
+ const { name, subject, text, html } = req.body || {};
836
+ if (!name) {
837
837
  res.status(400).json({ error: "name is required" });
838
838
  return;
839
839
  }
840
840
  const id = uuidv4();
841
- db.prepare("INSERT OR REPLACE INTO templates (id, agent_id, name, subject, text_body, html_body) VALUES (?, ?, ?, ?, ?, ?)").run(id, req.agent.id, name2, subject || null, text || null, html || null);
841
+ db.prepare("INSERT OR REPLACE INTO templates (id, agent_id, name, subject, text_body, html_body) VALUES (?, ?, ?, ?, ?, ?)").run(id, req.agent.id, name, subject || null, text || null, html || null);
842
842
  res.json({ ok: true, id });
843
843
  } catch (err) {
844
844
  next(err);
@@ -912,14 +912,14 @@ function createFeatureRoutes(db, _accountManager, config, gatewayManager) {
912
912
  });
913
913
  router.post("/tags", requireAgent, async (req, res, next) => {
914
914
  try {
915
- const { name: name2, color } = req.body || {};
916
- if (!name2) {
915
+ const { name, color } = req.body || {};
916
+ if (!name) {
917
917
  res.status(400).json({ error: "name is required" });
918
918
  return;
919
919
  }
920
920
  const id = uuidv4();
921
- db.prepare("INSERT OR IGNORE INTO tags (id, agent_id, name, color) VALUES (?, ?, ?, ?)").run(id, req.agent.id, name2.trim(), color || "#888888");
922
- res.json({ ok: true, id, name: name2.trim(), color: color || "#888888" });
921
+ db.prepare("INSERT OR IGNORE INTO tags (id, agent_id, name, color) VALUES (?, ?, ?, ?)").run(id, req.agent.id, name.trim(), color || "#888888");
922
+ res.json({ ok: true, id, name: name.trim(), color: color || "#888888" });
923
923
  } catch (err) {
924
924
  next(err);
925
925
  }
@@ -1049,16 +1049,16 @@ function createFeatureRoutes(db, _accountManager, config, gatewayManager) {
1049
1049
  });
1050
1050
  router.post("/rules", requireAgent, async (req, res, next) => {
1051
1051
  try {
1052
- const { name: name2, conditions, actions, priority, enabled } = req.body || {};
1053
- if (!name2) {
1052
+ const { name, conditions, actions, priority, enabled } = req.body || {};
1053
+ if (!name) {
1054
1054
  res.status(400).json({ error: "name is required" });
1055
1055
  return;
1056
1056
  }
1057
1057
  const id = uuidv4();
1058
1058
  db.prepare(
1059
1059
  "INSERT INTO email_rules (id, agent_id, name, priority, enabled, conditions, actions) VALUES (?, ?, ?, ?, ?, ?, ?)"
1060
- ).run(id, req.agent.id, name2, priority ?? 0, enabled !== false ? 1 : 0, JSON.stringify(conditions || {}), JSON.stringify(actions || {}));
1061
- res.status(201).json({ id, name: name2, conditions: conditions || {}, actions: actions || {}, priority: priority ?? 0, enabled: enabled !== false });
1060
+ ).run(id, req.agent.id, name, priority ?? 0, enabled !== false ? 1 : 0, JSON.stringify(conditions || {}), JSON.stringify(actions || {}));
1061
+ res.status(201).json({ id, name, conditions: conditions || {}, actions: actions || {}, priority: priority ?? 0, enabled: enabled !== false });
1062
1062
  } catch (err) {
1063
1063
  next(err);
1064
1064
  }
@@ -1539,7 +1539,31 @@ async function closeCaches() {
1539
1539
  }
1540
1540
  receiverCache.clear();
1541
1541
  }
1542
- async function notifyLocalRecipientsOfNewMail(accountManager, toField, ccField, bccField, fromAgent, subject, messageId) {
1542
+ async function findUidByMessageId(receiver, messageId, maxAttempts = 5) {
1543
+ const client = receiver.getImapClient();
1544
+ for (let i = 0; i < maxAttempts; i++) {
1545
+ try {
1546
+ const lock = await client.getMailboxLock("INBOX");
1547
+ try {
1548
+ const results = await client.search(
1549
+ { header: ["Message-ID", messageId] },
1550
+ { uid: true }
1551
+ );
1552
+ if (Array.isArray(results) && results.length > 0) {
1553
+ return results[results.length - 1];
1554
+ }
1555
+ } finally {
1556
+ lock.release();
1557
+ }
1558
+ } catch {
1559
+ }
1560
+ if (i < maxAttempts - 1) {
1561
+ await new Promise((r) => setTimeout(r, 200 * (i + 1)));
1562
+ }
1563
+ }
1564
+ return 0;
1565
+ }
1566
+ async function notifyLocalRecipientsOfNewMail(accountManager, toField, ccField, bccField, fromAgent, subject, messageId, config) {
1543
1567
  const collected = [];
1544
1568
  const push = (v) => {
1545
1569
  if (!v) return;
@@ -1574,12 +1598,29 @@ async function notifyLocalRecipientsOfNewMail(accountManager, toField, ccField,
1574
1598
  }
1575
1599
  if (!recipient || notified.has(recipient.id)) continue;
1576
1600
  notified.add(recipient.id);
1601
+ let uid = 0;
1602
+ let lookup = "no-message-id";
1603
+ if (messageId) {
1604
+ try {
1605
+ const recipientPassword = getAgentPassword(recipient);
1606
+ const receiver = await getReceiver(
1607
+ recipient.stalwartPrincipal,
1608
+ recipientPassword,
1609
+ config
1610
+ );
1611
+ uid = await findUidByMessageId(receiver, messageId);
1612
+ lookup = uid > 0 ? "resolved" : "failed";
1613
+ } catch {
1614
+ lookup = "failed";
1615
+ }
1616
+ }
1577
1617
  pushEventToAgent(recipient.id, {
1578
1618
  type: "new",
1579
- // uid is unknown without an IMAP fetch; use 0 as a sentinel —
1580
- // this matches the watcher's autoFetch=false path. SSE consumers
1581
- // that want full message detail can call /mail/inbox.
1582
- uid: 0,
1619
+ uid,
1620
+ // Tell consumers whether the UID is real or a sentinel — preserves
1621
+ // backwards compat (uid is still always a number) while giving
1622
+ // clients a reliable signal to fall back to /mail/inbox.
1623
+ uidLookup: lookup,
1583
1624
  internal: true,
1584
1625
  from: { name: fromAgent.name, address: fromAgent.email },
1585
1626
  subject,
@@ -1726,7 +1767,8 @@ function createMailRoutes(accountManager, config, db, gatewayManager) {
1726
1767
  bcc,
1727
1768
  agent,
1728
1769
  subject,
1729
- result.messageId
1770
+ result.messageId,
1771
+ config
1730
1772
  ).catch((err) => {
1731
1773
  console.warn(`[mail] Internal SSE notify failed: ${err.message}`);
1732
1774
  });
@@ -1988,19 +2030,19 @@ function createMailRoutes(accountManager, config, db, gatewayManager) {
1988
2030
  router.post("/mail/folders", requireAgent, async (req, res, next) => {
1989
2031
  try {
1990
2032
  const agent = req.agent;
1991
- const { name: name2 } = req.body || {};
1992
- if (!name2 || typeof name2 !== "string" || !name2.trim()) {
2033
+ const { name } = req.body || {};
2034
+ if (!name || typeof name !== "string" || !name.trim()) {
1993
2035
  res.status(400).json({ error: "name is required" });
1994
2036
  return;
1995
2037
  }
1996
- if (name2.length > 200 || /[\\*%]/.test(name2)) {
2038
+ if (name.length > 200 || /[\\*%]/.test(name)) {
1997
2039
  res.status(400).json({ error: "Invalid folder name" });
1998
2040
  return;
1999
2041
  }
2000
2042
  const password = getAgentPassword(agent);
2001
2043
  const receiver = await getReceiver(agent.stalwartPrincipal, password, config);
2002
- await receiver.createFolder(name2);
2003
- res.json({ ok: true, folder: name2 });
2044
+ await receiver.createFolder(name);
2045
+ res.json({ ok: true, folder: name });
2004
2046
  } catch (err) {
2005
2047
  next(err);
2006
2048
  }
@@ -2336,7 +2378,8 @@ function createMailRoutes(accountManager, config, db, gatewayManager) {
2336
2378
  mailOpts.bcc,
2337
2379
  agent,
2338
2380
  mailOpts.subject,
2339
- result.messageId
2381
+ result.messageId,
2382
+ config
2340
2383
  ).catch((err) => {
2341
2384
  console.warn(`[mail] Internal SSE notify (approve) failed: ${err.message}`);
2342
2385
  });
@@ -3478,7 +3521,7 @@ function buildColumnDDL(col, dialect) {
3478
3521
  if (typeof col.default === "string") {
3479
3522
  const trimmed = col.default.trim();
3480
3523
  const isSqlExpr = /\(.*\)/.test(trimmed) || /^CURRENT_(?:TIMESTAMP|DATE|TIME)$/i.test(trimmed);
3481
- val = isSqlExpr ? trimmed : `'${col.default.replace(/'/g, "''")}'`;
3524
+ val = isSqlExpr ? `(${trimmed})` : `'${col.default.replace(/'/g, "''")}'`;
3482
3525
  } else {
3483
3526
  val = col.default;
3484
3527
  }
@@ -3491,15 +3534,15 @@ function buildColumnDDL(col, dialect) {
3491
3534
  }
3492
3535
  return ddl;
3493
3536
  }
3494
- function safeTableName(agentId, name2, shared) {
3495
- const clean = name2.replace(/[^a-zA-Z0-9_]/g, "").substring(0, 64);
3537
+ function safeTableName(agentId, name, shared) {
3538
+ const clean = name.replace(/[^a-zA-Z0-9_]/g, "").substring(0, 64);
3496
3539
  if (!clean) throw new Error("Invalid table name");
3497
3540
  const prefix = shared ? "shared" : `agt_${agentId.replace(/[^a-zA-Z0-9]/g, "").substring(0, 16)}`;
3498
3541
  return `${prefix}_${clean}`;
3499
3542
  }
3500
- function resolveTable(agentId, name2) {
3501
- if (name2.startsWith("agt_") || name2.startsWith("shared_")) return name2;
3502
- return safeTableName(agentId, name2, false);
3543
+ function resolveTable(agentId, name) {
3544
+ if (name.startsWith("agt_") || name.startsWith("shared_")) return name;
3545
+ return safeTableName(agentId, name, false);
3503
3546
  }
3504
3547
  function isSafeTable(tableName) {
3505
3548
  return tableName.startsWith("agt_") || tableName.startsWith("shared_");
@@ -3657,8 +3700,8 @@ function createStorageRoutes(rawDb, accountManager, config, dialect = "sqlite")
3657
3700
  if (!agent) return;
3658
3701
  await ensureMetaTable();
3659
3702
  try {
3660
- const { name: name2, columns, indexes, shared, description, timestamps } = req.body;
3661
- if (!name2 || !columns?.length) return res.status(400).json({ error: "name and columns are required" });
3703
+ const { name, columns, indexes, shared, description, timestamps } = req.body;
3704
+ if (!name || !columns?.length) return res.status(400).json({ error: "name and columns are required" });
3662
3705
  const hasPK = columns.some((c) => c.primaryKey);
3663
3706
  const allCols = [...hasPK ? [] : [{ name: "id", type: "text", primaryKey: true }], ...columns];
3664
3707
  if (timestamps !== false) {
@@ -3669,9 +3712,9 @@ function createStorageRoutes(rawDb, accountManager, config, dialect = "sqlite")
3669
3712
  allCols.push({ name: "updated_at", type: "timestamp", default: nowExpr(dialect) });
3670
3713
  }
3671
3714
  }
3672
- const tableName = safeTableName(agent.id, name2, !!shared);
3715
+ const tableName = safeTableName(agent.id, name, !!shared);
3673
3716
  const existing = await db.get("SELECT table_name FROM agenticmail_storage_meta WHERE table_name = ?", [tableName]);
3674
- if (existing) return res.status(409).json({ error: `Table "${name2}" already exists`, table: tableName });
3717
+ if (existing) return res.status(409).json({ error: `Table "${name}" already exists`, table: tableName });
3675
3718
  const colDefs = allCols.map((c) => buildColumnDDL(c, dialect)).join(",\n ");
3676
3719
  await db.run(`CREATE TABLE IF NOT EXISTS ${tableName} (
3677
3720
  ${colDefs}
@@ -3692,7 +3735,7 @@ function createStorageRoutes(rawDb, accountManager, config, dialect = "sqlite")
3692
3735
  }
3693
3736
  await db.run(
3694
3737
  "INSERT INTO agenticmail_storage_meta (table_name, agent_id, display_name, description, shared, columns, indexes) VALUES (?, ?, ?, ?, ?, ?, ?)",
3695
- [tableName, agent.id, name2, description || "", shared ? 1 : 0, JSON.stringify(allCols), JSON.stringify(idxMeta)]
3738
+ [tableName, agent.id, name, description || "", shared ? 1 : 0, JSON.stringify(allCols), JSON.stringify(idxMeta)]
3696
3739
  );
3697
3740
  res.json({ ok: true, table: tableName, columns: allCols, indexes: idxMeta });
3698
3741
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/api",
3
- "version": "0.5.59",
3
+ "version": "0.5.61",
4
4
  "description": "REST API server for AgenticMail — email and SMS endpoints for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -27,7 +27,7 @@
27
27
  "prepublishOnly": "npm run build"
28
28
  },
29
29
  "dependencies": {
30
- "@agenticmail/core": "^0.5.59",
30
+ "@agenticmail/core": "^0.5.61",
31
31
  "cors": "^2.8.5",
32
32
  "dotenv": "^16.4.7",
33
33
  "express": "^4.21.0",