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