← 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 →