[object Object]

← back to George Gmail

george: add PUT /api/drafts/:id (drafts.update) — TK-11231

bf2fe4cabc3ecd7abb0cab1bfd615f6b1f5dc166 · 2026-09-10 07:47:26 -0700 · Steve Abrams

The create-only draft surface is what let agents compose DUPLICATE vendor asks
(Command x2, Maya Romanoff x2, Newmor x2): an agent that found its own stale
draft had no verb to revise it, so it composed a second one beside it.
drafts.update replaces in place, so revising is idempotent and leaves no
second artifact. Echoes the overwritten to/subject/date back as `previous`
so a bad edit is recoverable. Returns status=awaiting_owner_send /
delivered=false — a draft is never a delivered outcome.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTxE4MgnygQ9EY2rPZvtcK

Files touched

Diff

commit bf2fe4cabc3ecd7abb0cab1bfd615f6b1f5dc166
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 10 07:47:26 2026 -0700

    george: add PUT /api/drafts/:id (drafts.update) — TK-11231
    
    The create-only draft surface is what let agents compose DUPLICATE vendor asks
    (Command x2, Maya Romanoff x2, Newmor x2): an agent that found its own stale
    draft had no verb to revise it, so it composed a second one beside it.
    drafts.update replaces in place, so revising is idempotent and leaves no
    second artifact. Echoes the overwritten to/subject/date back as `previous`
    so a bad edit is recoverable. Returns status=awaiting_owner_send /
    delivered=false — a draft is never a delivered outcome.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01VTxE4MgnygQ9EY2rPZvtcK
---
 server.js | 48 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 48 insertions(+)

diff --git a/server.js b/server.js
index 9a11f25..f1b0073 100644
--- a/server.js
+++ b/server.js
@@ -1646,6 +1646,54 @@ app.delete('/api/drafts/:id', async (req, res) => {
     res.status(500).json({ error: e.message });
   }
 });
+// ─── API: Update (revise) an existing Draft ───  TK-11231
+// PUT /api/drafts/:id   body: { to, subject, body, cc, bcc }   (:id = the DRAFT id, e.g. "r-5804…", NOT the message id)
+// Rationale: the create-only surface is what let agents compose DUPLICATE vendor asks
+// (Command x2, Maya Romanoff x2, Newmor x2) — with no update verb an agent that finds
+// its own stale draft can only compose a second one beside it. drafts.update REPLACES
+// the draft in place, so revising is idempotent and leaves no second artifact.
+// Reversible: superseded content is echoed back in `previous` so a bad edit can be re-applied.
+app.put('/api/drafts/:id', async (req, res) => {
+  try {
+    const { to, subject, body, cc, bcc } = req.body;
+    if (!subject || !body) return res.status(400).json({ error: 'subject and body required' });
+
+    const { gmail: g, key: account } = resolveAccount(req);
+    if (!g) return res.status(400).json({ error: `unknown account: ${account}` });
+
+    // Read the draft first so the response can carry what was overwritten (undo material).
+    let previous = null;
+    try {
+      const before = await g.users.drafts.get({ userId: 'me', id: req.params.id, format: 'metadata' });
+      const hdrs = before.data.message?.payload?.headers || [];
+      const h = (n) => hdrs.find((x) => x.name.toLowerCase() === n)?.value || null;
+      previous = { messageId: before.data.message?.id, to: h('to'), subject: h('subject'), date: h('date') };
+    } catch (_) { /* non-fatal: a missing draft surfaces on the update below */ }
+
+    const source = inferSource(req.body, req);
+    const taggedBody = withSourceFooter(body, source);
+    const encoded = buildRawMessage({ to, cc, bcc, subject, body: taggedBody });
+    const result = await g.users.drafts.update({
+      userId: 'me',
+      id: req.params.id,
+      requestBody: { message: { raw: encoded } },
+    });
+
+    audit(account, 'draft-update', { draftId: req.params.id, to, subject, source, previous });
+    res.json({
+      success: true,
+      draftId: result.data.id,
+      messageId: result.data.message?.id,
+      account,
+      source,
+      previous,
+      status: 'awaiting_owner_send',
+      delivered: false,
+    });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
 
 // ─── API: Get Attachment (account-aware, raw binary) ───
 // Fixed 2026-07-10: was hardcoded to the steve-office `gmail` client AND

← 74ead01 auto-data-snapshot: 2026-09-10T04:15:51 (1 data files) — dat  ·  back to George Gmail  ·  george: draft contract + duplicate detection at the SHARED l efdf4ea →