[object Object]

← back to George Gmail

feat(api): add /api/attachment-local — fetch a Gmail attachment by filename to /tmp (normal auth, /tmp-only)

e86cb675233d09504c1a230cb4f006d3a720dcaf · 2026-07-07 14:08:15 -0700 · Steve

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit e86cb675233d09504c1a230cb4f006d3a720dcaf
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jul 7 14:08:15 2026 -0700

    feat(api): add /api/attachment-local — fetch a Gmail attachment by filename to /tmp (normal auth, /tmp-only)
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 server.js | 35 +++++++++++++++++++++++++++++++++++
 1 file changed, 35 insertions(+)

diff --git a/server.js b/server.js
index 0f05b81..2a8e6d1 100644
--- a/server.js
+++ b/server.js
@@ -1021,6 +1021,41 @@ app.get('/api/messages/:id', async (req, res) => {
   }
 });
 
+// ─── API: Fetch attachment → save to /tmp (normal George auth) ───
+// Added 2026-07-07 (Steve-authorized): pull a Gmail attachment by filename
+// substring and write it under /tmp for local processing. Goes through the
+// standard Basic-Auth gate like every other /api route; only writes to /tmp/.
+app.get('/api/attachment-local', async (req, res) => {
+  try {
+    const { messageId, filename, save } = req.query;
+    if (!messageId || !filename || !save) return res.status(400).json({ error: 'messageId, filename, save required' });
+    const savePath = require('path').resolve(String(save));
+    if (!savePath.startsWith('/tmp/')) return res.status(400).json({ error: 'save path must be under /tmp/' });
+
+    const { gmail: gm, key: account } = resolveAccount(req);
+    if (!gm) return res.status(400).json({ error: `unknown account: ${account}` });
+
+    const msg = await gm.users.messages.get({ userId: 'me', id: String(messageId), format: 'full' });
+    let match = null;
+    (function walk(parts) {
+      if (!parts || match) return;
+      for (const p of parts) {
+        if (p.filename && p.body?.attachmentId && p.filename.toLowerCase().includes(String(filename).toLowerCase())) { match = p; return; }
+        if (p.parts) walk(p.parts);
+      }
+    })(msg.data.payload?.parts);
+    if (!match) return res.status(404).json({ error: `no attachment matching "${filename}"` });
+
+    const att = await gm.users.messages.attachments.get({ userId: 'me', messageId: String(messageId), id: match.body.attachmentId });
+    const buf = Buffer.from(att.data.data, 'base64url');
+    require('fs').writeFileSync(savePath, buf);
+    audit(account, 'attachment-local', { messageId, filename: match.filename, bytes: buf.length, save: savePath });
+    res.json({ saved: savePath, filename: match.filename, mimeType: match.mimeType, bytes: buf.length });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
 // ─── API: Search Messages ───
 app.get('/api/search', async (req, res) => {
   try {

← ee22604 chore: macstudio3 migration — reconcile from mac2 + repoint  ·  back to George Gmail  ·  auto-save: 2026-07-10T11:07:57 (1 files) — server.js b6e91b3 →