[object Object]

← back to George Gmail

Snapshot-files cleanup: untrack server.js.bak-*, broaden .gitignore, add 404 guard

941feb15fc1750d690dab8c04e453c73c9b5bde6 · 2026-05-19 21:16:16 -0700 · Steve Abrams

- Untrack the 7-month-old server.js.bak-20260421-094351 snapshot that was
  tracked in the repo (still present on disk, now ignored).
- Broaden .gitignore to cover *.bak.*, *.bak-*, *.pre-*, *.orig, *.rej,
  *.swp, *~ so future snapshots don't slip back in.
- Add a static-snapshot 404 guard middleware that runs before
  express.static('public'); any URL matching .bak, .bak-*, .pre-*, .orig,
  .rej, .swp, or trailing ~ returns 404 regardless of disk state.
  Defense-in-depth on top of the existing basic-auth gate.

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

Files touched

Diff

commit 941feb15fc1750d690dab8c04e453c73c9b5bde6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue May 19 21:16:16 2026 -0700

    Snapshot-files cleanup: untrack server.js.bak-*, broaden .gitignore, add 404 guard
    
    - Untrack the 7-month-old server.js.bak-20260421-094351 snapshot that was
      tracked in the repo (still present on disk, now ignored).
    - Broaden .gitignore to cover *.bak.*, *.bak-*, *.pre-*, *.orig, *.rej,
      *.swp, *~ so future snapshots don't slip back in.
    - Add a static-snapshot 404 guard middleware that runs before
      express.static('public'); any URL matching .bak, .bak-*, .pre-*, .orig,
      .rej, .swp, or trailing ~ returns 404 regardless of disk state.
      Defense-in-depth on top of the existing basic-auth gate.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 .gitignore                    |    7 +
 server.js                     |   11 +
 server.js.bak-20260421-094351 | 1153 -----------------------------------------
 3 files changed, 18 insertions(+), 1153 deletions(-)

diff --git a/.gitignore b/.gitignore
index 311ecc8..2eefefc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,13 @@ dist/
 build/
 .next/
 *.bak
+*.bak.*
+*.bak-*
+*.pre-*
+*.orig
+*.rej
+*.swp
+*~
 # fleet-autoresponder runtime data (per-sender timestamps, run-log, state — not source)
 data/fleet-autoresponder-ledger.json
 data/fleet-autoresponder-runlog.jsonl
diff --git a/server.js b/server.js
index f06e90c..75bd59f 100644
--- a/server.js
+++ b/server.js
@@ -236,6 +236,17 @@ app.use((req, res, next) => {
   res.status(401).json({ error: 'Invalid credentials' });
 });
 
+// ─── Static-snapshot 404 guard ───
+// Defense-in-depth: never serve *.bak / *.pre-* / *.orig / *.rej / *~ from
+// the static root even if a stray snapshot lands in public/. Runs before
+// express.static so the snapshot file is invisible regardless of disk state.
+app.use((req, res, next) => {
+  if (/\.(bak|orig|rej|swp)(\.|$)|\.bak-|\.pre-|~$/.test(req.path)) {
+    return res.status(404).send('Not found');
+  }
+  next();
+});
+
 app.use(express.static(path.join(__dirname, 'public')));
 
 // ─── OAuth Re-Authorization Flow ───
diff --git a/server.js.bak-20260421-094351 b/server.js.bak-20260421-094351
deleted file mode 100644
index e609ad3..0000000
--- a/server.js.bak-20260421-094351
+++ /dev/null
@@ -1,1153 +0,0 @@
-/**
- * George the Gmail Agent — Port 9889
- * Secure email access for DW-Agents ecosystem
- * Auth: admin / DWSecure2024!
- */
-const express = require('express');
-const path = require('path');
-const fs = require('fs');
-const { google } = require('googleapis');
-
-const PORT = process.env.PORT || 9850;
-const AGENT_NAME = 'George';
-const AGENT_CODENAME = 'Gmail';
-const AGENT_COLOR = '#EA4335';
-
-// ─── Gmail OAuth Credentials (loaded from DW-MCP .env) ───
-const ENV_PATH = '/root/Projects/Designer-Wallcoverings/DW-MCP/.env';
-const creds = {};
-try {
-  const envContent = fs.readFileSync(ENV_PATH, 'utf8');
-  envContent.split('\n').forEach(line => {
-    const match = line.match(/^([^#=]+)=(.+)/);
-    if (match) creds[match[1].trim()] = match[2].trim();
-  });
-  console.log(`[${AGENT_NAME}] Loaded credentials from ${ENV_PATH}`);
-} catch (e) {
-  console.error(`[${AGENT_NAME}] Failed to load env: ${e.message}`);
-}
-
-const GMAIL_CLIENT_ID = creds.GMAIL_CLIENT_ID || process.env.GMAIL_CLIENT_ID;
-const GMAIL_CLIENT_SECRET = creds.GMAIL_CLIENT_SECRET || process.env.GMAIL_CLIENT_SECRET;
-const GMAIL_REDIRECT_URI = 'http://localhost:9850/oauth2callback';
-const GMAIL_REFRESH_TOKEN = creds.GMAIL_REFRESH_TOKEN || process.env.GMAIL_REFRESH_TOKEN;
-
-// ─── Info@ Account Credentials ───
-const INFO_REDIRECT_URI = 'http://localhost:9850/oauth2callback';
-const INFO_REFRESH_TOKEN = creds.INFO_REFRESH_TOKEN || process.env.INFO_REFRESH_TOKEN;
-
-// ─── OAuth2 Clients ───
-const SCOPES = [
-  'https://www.googleapis.com/auth/gmail.modify',
-  'https://www.googleapis.com/auth/tasks'
-];
-
-// Steve@ account (primary)
-let oauth2Client;
-let gmail;
-let tasksApi;
-try {
-  oauth2Client = new google.auth.OAuth2(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REDIRECT_URI);
-  oauth2Client.setCredentials({ refresh_token: GMAIL_REFRESH_TOKEN });
-  gmail = google.gmail({ version: 'v1', auth: oauth2Client });
-  tasksApi = google.tasks({ version: 'v1', auth: oauth2Client });
-  console.log(`[${AGENT_NAME}] Gmail + Tasks API client initialized (steve@)`);
-} catch (e) {
-  console.error(`[${AGENT_NAME}] API client error: ${e.message}`);
-}
-
-// Info@ account (secondary — gmail + tasks + drive + docs)
-const INFO_SCOPES = [
-  'https://www.googleapis.com/auth/gmail.modify',
-  'https://www.googleapis.com/auth/tasks',
-  'https://www.googleapis.com/auth/drive',
-  'https://www.googleapis.com/auth/documents'
-];
-let infoOauth2Client;
-let infoGmail;
-let infoTasksApi;
-let infoDrive;
-try {
-  infoOauth2Client = new google.auth.OAuth2(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, INFO_REDIRECT_URI);
-  if (INFO_REFRESH_TOKEN) {
-    infoOauth2Client.setCredentials({ refresh_token: INFO_REFRESH_TOKEN });
-    infoGmail = google.gmail({ version: 'v1', auth: infoOauth2Client });
-    infoTasksApi = google.tasks({ version: 'v1', auth: infoOauth2Client });
-    infoDrive = google.drive({ version: 'v3', auth: infoOauth2Client });
-    console.log(`[${AGENT_NAME}] Info@ account initialized (Gmail + Tasks + Drive)`);
-  } else {
-    console.log(`[${AGENT_NAME}] Info@ not configured — visit /auth/info to authorize`);
-  }
-} catch (e) {
-  console.error(`[${AGENT_NAME}] Info@ client error: ${e.message}`);
-}
-
-// ─── Express App ───
-const app = express();
-app.use(express.json({ limit: '50mb' }));
-
-// ─── Basic Auth (skip health + OAuth callbacks) ───
-app.use((req, res, next) => {
-  if (req.path === '/health' || req.path === '/auth' || req.path === '/auth/info' || req.path === '/oauth2callback') return next();
-  const auth = req.headers.authorization;
-  if (!auth || !auth.startsWith('Basic ')) {
-    res.setHeader('WWW-Authenticate', 'Basic realm="George Gmail Agent"');
-    return res.status(401).json({ error: 'Authentication required' });
-  }
-  const decoded = Buffer.from(auth.slice(6), 'base64').toString();
-  const [user, pass] = decoded.split(':');
-  if (user === 'admin' && pass === 'DWSecure2024!') return next();
-  res.setHeader('WWW-Authenticate', 'Basic realm="George Gmail Agent"');
-  res.status(401).json({ error: 'Invalid credentials' });
-});
-
-app.use(express.static(path.join(__dirname, 'public')));
-
-// ─── OAuth Re-Authorization Flow ───
-const OAUTH_REDIRECT = 'http://localhost:9850/oauth2callback';
-
-app.get('/auth', (req, res) => {
-  if (!oauth2Client) return res.status(500).send('OAuth client not configured');
-  const url = oauth2Client.generateAuthUrl({
-    access_type: 'offline',
-    scope: SCOPES,
-    prompt: 'consent',
-    redirect_uri: OAUTH_REDIRECT,
-  });
-  res.redirect(url);
-});
-
-app.get('/oauth2callback', async (req, res) => {
-  try {
-    const { code, state } = req.query;
-    if (!code) return res.status(400).send('No authorization code received');
-
-    // Route to info@ handler if state=info
-    if (state === 'info') {
-      const { tokens } = await infoOauth2Client.getToken({ code, redirect_uri: INFO_REDIRECT_URI });
-      infoOauth2Client.setCredentials(tokens);
-      infoGmail = google.gmail({ version: 'v1', auth: infoOauth2Client });
-      infoTasksApi = google.tasks({ version: 'v1', auth: infoOauth2Client });
-      infoDrive = google.drive({ version: 'v3', auth: infoOauth2Client });
-
-      let msg = '<html><body style="font-family:sans-serif;background:#0f1117;color:#e4e4e7;padding:40px">';
-      msg += '<h2 style="color:#4ade80">Info@ Authorization Successful! (Gmail + Tasks + Drive)</h2>';
-      if (tokens.refresh_token) {
-        msg += `<p><strong>New Info@ Refresh Token:</strong></p><code style="background:#1a1d27;padding:10px;display:block;word-break:break-all;border-radius:8px">${tokens.refresh_token}</code>`;
-        msg += '<p style="color:#f59e0b">Save this token to your .env file as INFO_REFRESH_TOKEN</p>';
-        console.log(`[${AGENT_NAME}] Info@ refresh token obtained: ${tokens.refresh_token.substring(0, 20)}...`);
-        try {
-          let envContent = fs.readFileSync(ENV_PATH, 'utf8');
-          if (envContent.includes('INFO_REFRESH_TOKEN=')) {
-            envContent = envContent.replace(/INFO_REFRESH_TOKEN=.*/, `INFO_REFRESH_TOKEN=${tokens.refresh_token}`);
-          } else {
-            envContent += `\n# Info@ Account OAuth Token\nINFO_REFRESH_TOKEN=${tokens.refresh_token}\n`;
-          }
-          fs.writeFileSync(ENV_PATH, envContent);
-          msg += '<p style="color:#4ade80">Token auto-saved to .env!</p>';
-        } catch (e) {
-          msg += `<p style="color:#f87171">Could not auto-save: ${e.message}</p>`;
-        }
-      }
-      try {
-        const profile = await infoGmail.users.getProfile({ userId: 'me' });
-        msg += `<p style="color:#60a5fa">Connected as: <strong>${profile.data.emailAddress}</strong></p>`;
-      } catch {}
-      msg += '<p><a href="/" style="color:#60a5fa">Go to Dashboard</a></p></body></html>';
-      return res.send(msg);
-    }
-
-    // Default: steve@ account
-    const { tokens } = await oauth2Client.getToken({ code, redirect_uri: OAUTH_REDIRECT });
-    oauth2Client.setCredentials(tokens);
-    gmail = google.gmail({ version: 'v1', auth: oauth2Client });
-    tasksApi = google.tasks({ version: 'v1', auth: oauth2Client });
-
-    let msg = '<html><body style="font-family:sans-serif;background:#0f1117;color:#e4e4e7;padding:40px">';
-    msg += '<h2 style="color:#4ade80">Authorization Successful!</h2>';
-    if (tokens.refresh_token) {
-      msg += `<p><strong>New Refresh Token:</strong></p><code style="background:#1a1d27;padding:10px;display:block;word-break:break-all;border-radius:8px">${tokens.refresh_token}</code>`;
-      msg += '<p style="color:#f59e0b">Save this token to your .env file as GMAIL_REFRESH_TOKEN</p>';
-      console.log(`[${AGENT_NAME}] New refresh token obtained: ${tokens.refresh_token.substring(0, 20)}...`);
-    } else {
-      msg += '<p>Access token refreshed (existing refresh token reused).</p>';
-    }
-    msg += '<p style="color:#4ade80">Gmail API is now ready.</p>';
-    msg += '<p><a href="/" style="color:#60a5fa">Go to Dashboard</a></p></body></html>';
-    res.send(msg);
-  } catch (e) {
-    console.error(`[${AGENT_NAME}] OAuth callback error:`, e.message);
-    res.status(500).send(`<html><body style="font-family:sans-serif;background:#0f1117;color:#f87171;padding:40px"><h2>Authorization Failed</h2><p>${e.message}</p><p><a href="/auth" style="color:#60a5fa">Try Again</a></p></body></html>`);
-  }
-});
-
-// ─── Info@ OAuth Authorization Flow ───
-app.get('/auth/info', (req, res) => {
-  if (!infoOauth2Client) return res.status(500).send('OAuth client not configured');
-  const url = infoOauth2Client.generateAuthUrl({
-    access_type: 'offline',
-    scope: INFO_SCOPES,
-    prompt: 'consent',
-    redirect_uri: INFO_REDIRECT_URI,
-    login_hint: 'info@designerwallcoverings.com',
-    state: 'info',
-  });
-  res.redirect(url);
-});
-
-app.get('/api/auth-url/info', (req, res) => {
-  if (!infoOauth2Client) return res.status(500).json({ error: 'OAuth client not configured' });
-  const url = infoOauth2Client.generateAuthUrl({
-    access_type: 'offline',
-    scope: INFO_SCOPES,
-    prompt: 'consent',
-    redirect_uri: INFO_REDIRECT_URI,
-    login_hint: 'info@designerwallcoverings.com',
-    state: 'info',
-  });
-  res.json({ authUrl: url, account: 'info@designerwallcoverings.com', scopes: INFO_SCOPES, instructions: 'Visit this URL in a browser where you are logged into info@designerwallcoverings.com. Approve access, then copy the code from the redirect URL and pass to /api/exchange-code/info?code=YOUR_CODE' });
-});
-
-// oauth2callback-info is handled by the unified /oauth2callback via state=info
-
-app.get('/api/exchange-code/info', async (req, res) => {
-  try {
-    const { code } = req.query;
-    if (!code) return res.status(400).json({ error: 'code parameter required' });
-    const { tokens } = await infoOauth2Client.getToken({ code, redirect_uri: INFO_REDIRECT_URI });
-    infoOauth2Client.setCredentials(tokens);
-    infoGmail = google.gmail({ version: 'v1', auth: infoOauth2Client });
-    infoTasksApi = google.tasks({ version: 'v1', auth: infoOauth2Client });
-    infoDrive = google.drive({ version: 'v3', auth: infoOauth2Client });
-
-    const result = { success: true, account: 'info@', hasRefreshToken: !!tokens.refresh_token };
-    if (tokens.refresh_token) {
-      result.refreshToken = tokens.refresh_token;
-      try {
-        let envContent = fs.readFileSync(ENV_PATH, 'utf8');
-        if (envContent.includes('INFO_REFRESH_TOKEN=')) {
-          envContent = envContent.replace(/INFO_REFRESH_TOKEN=.*/, `INFO_REFRESH_TOKEN=${tokens.refresh_token}`);
-        } else {
-          envContent += `\n# Info@ Account OAuth Token\nINFO_REFRESH_TOKEN=${tokens.refresh_token}\n`;
-        }
-        fs.writeFileSync(ENV_PATH, envContent);
-        result.savedToEnv = true;
-      } catch (e) {
-        result.savedToEnv = false;
-        result.envError = e.message;
-      }
-    }
-    try {
-      const profile = await infoGmail.users.getProfile({ userId: 'me' });
-      result.email = profile.data.emailAddress;
-      result.ready = true;
-    } catch (e) {
-      result.ready = false;
-      result.testError = e.message;
-    }
-    res.json(result);
-  } catch (e) {
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// ─── Manual Code Exchange (for localhost redirect flow) ───
-app.get('/api/exchange-code', async (req, res) => {
-  try {
-    const { code } = req.query;
-    if (!code) return res.status(400).json({ error: 'code parameter required' });
-    const { tokens } = await oauth2Client.getToken({ code, redirect_uri: OAUTH_REDIRECT });
-    oauth2Client.setCredentials(tokens);
-    gmail = google.gmail({ version: 'v1', auth: oauth2Client });
-    tasksApi = google.tasks({ version: 'v1', auth: oauth2Client });
-
-    const result = { success: true, hasRefreshToken: !!tokens.refresh_token };
-    if (tokens.refresh_token) {
-      result.refreshToken = tokens.refresh_token;
-      // Auto-save refresh token to .env
-      try {
-        let envContent = fs.readFileSync(ENV_PATH, 'utf8');
-        if (envContent.includes('GMAIL_REFRESH_TOKEN=')) {
-          envContent = envContent.replace(/GMAIL_REFRESH_TOKEN=.*/, `GMAIL_REFRESH_TOKEN=${tokens.refresh_token}`);
-        } else {
-          envContent += `\nGMAIL_REFRESH_TOKEN=${tokens.refresh_token}\n`;
-        }
-        fs.writeFileSync(ENV_PATH, envContent);
-        result.savedToEnv = true;
-        console.log(`[${AGENT_NAME}] Refresh token saved to ${ENV_PATH}`);
-      } catch (e) {
-        result.savedToEnv = false;
-        result.envError = e.message;
-      }
-    }
-    // Quick test
-    try {
-      const profile = await gmail.users.getProfile({ userId: 'me' });
-      result.email = profile.data.emailAddress;
-      result.gmailReady = true;
-    } catch (e) {
-      result.gmailReady = false;
-      result.testError = e.message;
-    }
-    res.json(result);
-  } catch (e) {
-    console.error(`[${AGENT_NAME}] Code exchange error:`, e.message);
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// ─── Auth URL Generator (returns URL instead of redirecting) ───
-app.get('/api/auth-url', (req, res) => {
-  if (!oauth2Client) return res.status(500).json({ error: 'OAuth client not configured' });
-  const url = oauth2Client.generateAuthUrl({
-    access_type: 'offline',
-    scope: SCOPES,
-    prompt: 'consent',
-    redirect_uri: OAUTH_REDIRECT,
-  });
-  res.json({ authUrl: url, instructions: 'Visit this URL, approve access, then copy the full redirect URL from your browser address bar and pass the code parameter to /api/exchange-code?code=YOUR_CODE' });
-});
-
-// ─── Basic Auth (exempt health + OAuth routes) ───
-app.use((req, res, next) => {
-  if (req.path === '/api/health' || req.path === '/auth' || req.path === '/auth/info' || req.path === '/oauth2callback' || req.path === '/oauth2callback-info' || req.path === '/api/exchange-code' || req.path === '/api/exchange-code/info' || req.path === '/api/auth-url' || req.path === '/api/auth-url/info') return next();
-  const auth = req.headers.authorization;
-  if (!auth || !auth.startsWith('Basic ')) return res.status(401).json({ error: 'Auth required' });
-  const [user, pass] = Buffer.from(auth.split(' ')[1], 'base64').toString().split(':');
-  if (user !== 'admin' || pass !== 'DWSecure2024!') return res.status(401).json({ error: 'Invalid credentials' });
-  next();
-});
-
-// ─── Helper: decode email parts ───
-function decodeBody(parts) {
-  if (!parts) return '';
-  for (const part of parts) {
-    if (part.mimeType === 'text/plain' && part.body?.data) {
-      return Buffer.from(part.body.data, 'base64url').toString('utf8');
-    }
-    if (part.parts) {
-      const nested = decodeBody(part.parts);
-      if (nested) return nested;
-    }
-  }
-  return '';
-}
-
-function getHeader(headers, name) {
-  const h = (headers || []).find(h => h.name.toLowerCase() === name.toLowerCase());
-  return h ? h.value : '';
-}
-
-// ─── API: Health Check ───
-app.get('/api/health', (req, res) => {
-  res.json({
-    agent: AGENT_NAME,
-    codename: AGENT_CODENAME,
-    status: gmail ? 'ready' : 'no-gmail-client',
-    port: PORT,
-    hasCredentials: !!(GMAIL_CLIENT_ID && GMAIL_REFRESH_TOKEN),
-    uptime: process.uptime()
-  });
-});
-
-// ─── API: Status + Profile ───
-app.get('/api/status', (req, res) => {
-  res.json({
-    agent: AGENT_NAME,
-    codename: AGENT_CODENAME,
-    color: AGENT_COLOR,
-    port: PORT,
-    status: gmail ? 'online' : 'error',
-    hasCredentials: !!(GMAIL_CLIENT_ID && GMAIL_REFRESH_TOKEN),
-    capabilities: ['list', 'read', 'search', 'send', 'labels', 'attachments'],
-    envSource: ENV_PATH
-  });
-});
-
-// ─── API: Get Profile (email address, history) ───
-app.get('/api/profile', async (req, res) => {
-  try {
-    const profile = await gmail.users.getProfile({ userId: 'me' });
-    res.json(profile.data);
-  } catch (e) {
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// ─── API: List Labels ───
-app.get('/api/labels', async (req, res) => {
-  try {
-    const result = await gmail.users.labels.list({ userId: 'me' });
-    res.json(result.data.labels || []);
-  } catch (e) {
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// ─── API: List Messages ───
-app.get('/api/messages', async (req, res) => {
-  try {
-    const { q, maxResults, labelIds, pageToken } = req.query;
-    const params = { userId: 'me', maxResults: parseInt(maxResults) || 20 };
-    if (q) params.q = q;
-    if (labelIds) params.labelIds = labelIds.split(',');
-    if (pageToken) params.pageToken = pageToken;
-
-    const list = await gmail.users.messages.list(params);
-    const messages = list.data.messages || [];
-
-    // Fetch headers for each message (batch)
-    const detailed = await Promise.all(
-      messages.slice(0, parseInt(maxResults) || 20).map(async m => {
-        try {
-          const full = await gmail.users.messages.get({ userId: 'me', id: m.id, format: 'metadata', metadataHeaders: ['Subject', 'From', 'Date', 'To'] });
-          return {
-            id: full.data.id,
-            threadId: full.data.threadId,
-            snippet: full.data.snippet,
-            labelIds: full.data.labelIds,
-            subject: getHeader(full.data.payload?.headers, 'Subject'),
-            from: getHeader(full.data.payload?.headers, 'From'),
-            to: getHeader(full.data.payload?.headers, 'To'),
-            date: getHeader(full.data.payload?.headers, 'Date'),
-            internalDate: full.data.internalDate
-          };
-        } catch { return { id: m.id, error: 'fetch-failed' }; }
-      })
-    );
-
-    res.json({
-      messages: detailed,
-      nextPageToken: list.data.nextPageToken,
-      resultSizeEstimate: list.data.resultSizeEstimate
-    });
-  } catch (e) {
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// ─── API: Read Single Message ───
-app.get('/api/messages/:id', async (req, res) => {
-  try {
-    const msg = await gmail.users.messages.get({ userId: 'me', id: req.params.id, format: 'full' });
-    const headers = msg.data.payload?.headers || [];
-    const body = msg.data.payload?.body?.data
-      ? Buffer.from(msg.data.payload.body.data, 'base64url').toString('utf8')
-      : decodeBody(msg.data.payload?.parts);
-
-    const attachments = [];
-    function findAttachments(parts) {
-      if (!parts) return;
-      for (const p of parts) {
-        if (p.filename && p.body?.attachmentId) {
-          attachments.push({ filename: p.filename, mimeType: p.mimeType, size: p.body.size, attachmentId: p.body.attachmentId });
-        }
-        if (p.parts) findAttachments(p.parts);
-      }
-    }
-    findAttachments(msg.data.payload?.parts);
-
-    res.json({
-      id: msg.data.id,
-      threadId: msg.data.threadId,
-      labelIds: msg.data.labelIds,
-      snippet: msg.data.snippet,
-      subject: getHeader(headers, 'Subject'),
-      from: getHeader(headers, 'From'),
-      to: getHeader(headers, 'To'),
-      cc: getHeader(headers, 'Cc'),
-      date: getHeader(headers, 'Date'),
-      body,
-      attachments,
-      internalDate: msg.data.internalDate
-    });
-  } catch (e) {
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// ─── API: Search Messages ───
-app.get('/api/search', async (req, res) => {
-  try {
-    const { q, maxResults } = req.query;
-    if (!q) return res.status(400).json({ error: 'Query (q) required' });
-    const list = await gmail.users.messages.list({ userId: 'me', q, maxResults: parseInt(maxResults) || 10 });
-    const messages = list.data.messages || [];
-
-    const detailed = await Promise.all(
-      messages.map(async m => {
-        try {
-          const full = await gmail.users.messages.get({ userId: 'me', id: m.id, format: 'metadata', metadataHeaders: ['Subject', 'From', 'Date'] });
-          return {
-            id: full.data.id,
-            snippet: full.data.snippet,
-            subject: getHeader(full.data.payload?.headers, 'Subject'),
-            from: getHeader(full.data.payload?.headers, 'From'),
-            date: getHeader(full.data.payload?.headers, 'Date')
-          };
-        } catch { return { id: m.id, error: 'fetch-failed' }; }
-      })
-    );
-
-    res.json({ query: q, messages: detailed, total: list.data.resultSizeEstimate });
-  } catch (e) {
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// ─── API: Send Email ───
-app.post('/api/send', async (req, res) => {
-  try {
-    const { to, subject, body, cc, bcc } = req.body;
-    if (!to || !subject || !body) return res.status(400).json({ error: 'to, subject, and body required' });
-
-    const headers = [
-      `To: ${to}`,
-      cc ? `Cc: ${cc}` : '',
-      bcc ? `Bcc: ${bcc}` : '',
-      `Subject: ${subject}`,
-      'Content-Type: text/html; charset=utf-8',
-      '',
-      body
-    ].filter(Boolean).join('\r\n');
-
-    const encoded = Buffer.from(headers).toString('base64url');
-    const result = await gmail.users.messages.send({ userId: 'me', requestBody: { raw: encoded } });
-    res.json({ success: true, messageId: result.data.id, threadId: result.data.threadId });
-  } catch (e) {
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// ─── API: Send with Attachments ───
-// Body: { to, cc?, bcc?, subject, body (html), attachments: [{ filename, content_base64, mime_type? }] }
-app.post('/api/send-with-attachment', async (req, res) => {
-  try {
-    const { to, cc, bcc, subject, body, attachments } = req.body;
-    if (!to || !subject || !body) return res.status(400).json({ error: 'to, subject, and body required' });
-    if (!attachments || !Array.isArray(attachments) || attachments.length === 0) {
-      return res.status(400).json({ error: 'attachments array required' });
-    }
-    const boundary = `=_dw_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
-    const lines = [
-      `To: ${to}`,
-      cc ? `Cc: ${cc}` : '',
-      bcc ? `Bcc: ${bcc}` : '',
-      `Subject: ${subject}`,
-      'MIME-Version: 1.0',
-      `Content-Type: multipart/mixed; boundary="${boundary}"`,
-      '',
-      `--${boundary}`,
-      'Content-Type: text/html; charset=utf-8',
-      'Content-Transfer-Encoding: 7bit',
-      '',
-      body
-    ].filter(Boolean);
-    for (const att of attachments) {
-      const mime = att.mime_type || 'application/octet-stream';
-      const fname = att.filename || 'attachment.bin';
-      lines.push(
-        `--${boundary}`,
-        `Content-Type: ${mime}; name="${fname}"`,
-        'Content-Transfer-Encoding: base64',
-        `Content-Disposition: attachment; filename="${fname}"`,
-        '',
-        att.content_base64.replace(/(.{76})/g, '$1\r\n')
-      );
-    }
-    lines.push(`--${boundary}--`, '');
-    const raw = lines.join('\r\n');
-    const encoded = Buffer.from(raw).toString('base64url');
-    const result = await gmail.users.messages.send({ userId: 'me', requestBody: { raw: encoded } });
-    res.json({ success: true, messageId: result.data.id, threadId: result.data.threadId });
-  } catch (e) {
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// ─── API: Create Draft ───
-app.post('/api/drafts', 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 headers = [
-      to ? `To: ${to}` : '',
-      cc ? `Cc: ${cc}` : '',
-      bcc ? `Bcc: ${bcc}` : '',
-      `Subject: ${subject}`,
-      'Content-Type: text/html; charset=utf-8',
-      '',
-      body
-    ].filter(Boolean).join('\r\n');
-
-    const encoded = Buffer.from(headers).toString('base64url');
-    const result = await gmail.users.drafts.create({ userId: 'me', requestBody: { message: { raw: encoded } } });
-    res.json({ success: true, draftId: result.data.id, messageId: result.data.message?.id });
-  } catch (e) {
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// ─── API: List Drafts ───
-app.get('/api/drafts', async (req, res) => {
-  try {
-    const result = await gmail.users.drafts.list({ userId: 'me', maxResults: parseInt(req.query.maxResults) || 10 });
-    res.json(result.data.drafts || []);
-  } catch (e) {
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// ─── API: Get Attachment ───
-app.get('/api/messages/:messageId/attachments/:attachmentId', async (req, res) => {
-  try {
-    const att = await gmail.users.messages.attachments.get({
-      userId: 'me', messageId: req.params.messageId, id: req.params.attachmentId
-    });
-    res.json({ data: att.data.data, size: att.data.size });
-  } catch (e) {
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// ─── Slack Webhook for notifications ───
-const SLACK_WEBHOOK = 'https://hooks.slack.com/services/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';
-function slackNotify(text) {
-  try {
-    const https = require('https');
-    const url = new URL(SLACK_WEBHOOK);
-    const payload = JSON.stringify({ text });
-    const req = https.request({ hostname: url.hostname, path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) } });
-    req.write(payload);
-    req.end();
-  } catch {}
-}
-
-// ─── Google Tasks API ───
-app.get('/api/tasks/lists', async (req, res) => {
-  try {
-    if (!tasksApi) return res.status(503).json({ error: 'Tasks API not initialized — re-authorize at /auth' });
-    const result = await tasksApi.tasklists.list({ maxResults: 100 });
-    res.json({ lists: result.data.items || [] });
-  } catch (e) {
-    if (e.message.includes('insufficient')) {
-      return res.status(403).json({ error: 'Tasks scope not authorized. Visit /auth to grant permission.', authUrl: '/auth' });
-    }
-    res.status(500).json({ error: e.message });
-  }
-});
-
-app.get('/api/tasks/:listId', async (req, res) => {
-  try {
-    if (!tasksApi) return res.status(503).json({ error: 'Tasks API not initialized — re-authorize at /auth' });
-    const showCompleted = req.query.showCompleted !== 'false';
-    const showHidden = req.query.showHidden === 'true';
-    const result = await tasksApi.tasks.list({
-      tasklist: req.params.listId,
-      maxResults: parseInt(req.query.maxResults) || 100,
-      showCompleted,
-      showHidden,
-    });
-    const tasks = (result.data.items || []).map(t => ({
-      id: t.id,
-      title: t.title,
-      notes: t.notes || null,
-      status: t.status,
-      due: t.due || null,
-      completed: t.completed || null,
-      updated: t.updated,
-      parent: t.parent || null,
-      position: t.position,
-      links: t.links || [],
-    }));
-    res.json({ listId: req.params.listId, count: tasks.length, tasks });
-  } catch (e) {
-    if (e.message.includes('insufficient')) {
-      return res.status(403).json({ error: 'Tasks scope not authorized. Visit /auth to grant permission.', authUrl: '/auth' });
-    }
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// Convenience: get all tasks across all lists
-app.get('/api/tasks', async (req, res) => {
-  try {
-    if (!tasksApi) return res.status(503).json({ error: 'Tasks API not initialized — re-authorize at /auth' });
-    const listsResult = await tasksApi.tasklists.list({ maxResults: 100 });
-    const lists = listsResult.data.items || [];
-    const allTasks = [];
-    for (const list of lists) {
-      const tasksResult = await tasksApi.tasks.list({
-        tasklist: list.id,
-        maxResults: 100,
-        showCompleted: req.query.showCompleted !== 'false',
-      });
-      const items = (tasksResult.data.items || []).map(t => ({
-        id: t.id,
-        title: t.title,
-        notes: t.notes || null,
-        status: t.status,
-        due: t.due || null,
-        completed: t.completed || null,
-        updated: t.updated,
-        listId: list.id,
-        listTitle: list.title,
-      }));
-      allTasks.push(...items);
-    }
-    res.json({ totalLists: lists.length, totalTasks: allTasks.length, tasks: allTasks });
-  } catch (e) {
-    if (e.message.includes('insufficient')) {
-      return res.status(403).json({ error: 'Tasks scope not authorized. Visit /auth to grant permission.', authUrl: '/auth' });
-    }
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// ─── Info@ Tasks API ───
-app.get('/api/info/tasks/lists', async (req, res) => {
-  try {
-    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized — authorize at /auth/info', authUrl: '/api/auth-url/info' });
-    const result = await infoTasksApi.tasklists.list({ maxResults: 100 });
-    res.json({ account: 'info@designerwallcoverings.com', lists: result.data.items || [] });
-  } catch (e) {
-    if (e.message.includes('insufficient')) {
-      return res.status(403).json({ error: 'Info@ tasks scope not authorized. Visit /auth/info.', authUrl: '/auth/info' });
-    }
-    res.status(500).json({ error: e.message });
-  }
-});
-
-app.get('/api/info/tasks/:listId', async (req, res) => {
-  try {
-    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized — authorize at /auth/info', authUrl: '/api/auth-url/info' });
-    const showCompleted = req.query.showCompleted !== 'false';
-    const showHidden = req.query.showHidden === 'true';
-    const result = await infoTasksApi.tasks.list({
-      tasklist: req.params.listId,
-      maxResults: parseInt(req.query.maxResults) || 100,
-      showCompleted,
-      showHidden,
-    });
-    const tasks = (result.data.items || []).map(t => ({
-      id: t.id,
-      title: t.title,
-      notes: t.notes || null,
-      status: t.status,
-      due: t.due || null,
-      completed: t.completed || null,
-      updated: t.updated,
-      parent: t.parent || null,
-      position: t.position,
-      links: t.links || [],
-    }));
-    res.json({ account: 'info@designerwallcoverings.com', listId: req.params.listId, count: tasks.length, tasks });
-  } catch (e) {
-    if (e.message.includes('insufficient')) {
-      return res.status(403).json({ error: 'Info@ tasks scope not authorized.', authUrl: '/auth/info' });
-    }
-    res.status(500).json({ error: e.message });
-  }
-});
-
-app.get('/api/info/tasks', async (req, res) => {
-  try {
-    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized — authorize at /auth/info', authUrl: '/api/auth-url/info' });
-    const listsResult = await infoTasksApi.tasklists.list({ maxResults: 100 });
-    const lists = listsResult.data.items || [];
-    const allTasks = [];
-    for (const list of lists) {
-      const tasksResult = await infoTasksApi.tasks.list({
-        tasklist: list.id,
-        maxResults: 100,
-        showCompleted: req.query.showCompleted !== 'false',
-      });
-      const items = (tasksResult.data.items || []).map(t => ({
-        id: t.id,
-        title: t.title,
-        notes: t.notes || null,
-        status: t.status,
-        due: t.due || null,
-        completed: t.completed || null,
-        updated: t.updated,
-        listId: list.id,
-        listTitle: list.title,
-      }));
-      allTasks.push(...items);
-    }
-    res.json({ account: 'info@designerwallcoverings.com', totalLists: lists.length, totalTasks: allTasks.length, tasks: allTasks });
-  } catch (e) {
-    if (e.message.includes('insufficient')) {
-      return res.status(403).json({ error: 'Info@ tasks scope not authorized.', authUrl: '/auth/info' });
-    }
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// ─── Info@ Tasks Write Endpoints ───
-// Create a new task list
-app.post('/api/info/tasks/lists/create', async (req, res) => {
-  try {
-    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized', authUrl: '/auth/info' });
-    const { title } = req.body;
-    if (!title) return res.status(400).json({ error: 'title is required' });
-    const result = await infoTasksApi.tasklists.insert({ requestBody: { title } });
-    res.json({ success: true, list: result.data });
-  } catch (e) {
-    if (e.message.includes('insufficient')) {
-      return res.status(403).json({ error: 'Tasks write scope not authorized. Visit /auth/info to upgrade from tasks.readonly to tasks.', authUrl: '/auth/info' });
-    }
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// Find a task list by name, or create it if it doesn't exist
-app.post('/api/info/tasks/lists/find-or-create', async (req, res) => {
-  try {
-    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized', authUrl: '/auth/info' });
-    const { title } = req.body;
-    if (!title) return res.status(400).json({ error: 'title is required' });
-
-    // Search existing lists
-    const listsResult = await infoTasksApi.tasklists.list({ maxResults: 100 });
-    const existing = (listsResult.data.items || []).find(l => l.title === title);
-    if (existing) {
-      return res.json({ success: true, created: false, list: existing });
-    }
-
-    // Create new list
-    const result = await infoTasksApi.tasklists.insert({ requestBody: { title } });
-    res.json({ success: true, created: true, list: result.data });
-  } catch (e) {
-    if (e.message.includes('insufficient')) {
-      return res.status(403).json({ error: 'Tasks write scope not authorized. Visit /auth/info.', authUrl: '/auth/info' });
-    }
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// Move a task from one list to another (insert into dest + delete from source)
-// Google Tasks API has no cross-list move — we must copy then delete
-app.post('/api/info/tasks/move', async (req, res) => {
-  try {
-    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized', authUrl: '/auth/info' });
-    const { sourceListId, taskId, destListId, markCompleted } = req.body;
-    if (!sourceListId || !taskId || !destListId) {
-      return res.status(400).json({ error: 'sourceListId, taskId, and destListId are required' });
-    }
-
-    // 1. Get the task from source list
-    const taskResult = await infoTasksApi.tasks.get({ tasklist: sourceListId, task: taskId });
-    const task = taskResult.data;
-
-    // 2. Insert into destination list (preserve title, notes, due, links)
-    const newTaskBody = {
-      title: task.title,
-      notes: task.notes || undefined,
-      due: task.due || undefined,
-      status: markCompleted ? 'completed' : 'needsAction',
-    };
-    const insertResult = await infoTasksApi.tasks.insert({ tasklist: destListId, requestBody: newTaskBody });
-
-    // 3. Delete from source list
-    await infoTasksApi.tasks.delete({ tasklist: sourceListId, task: taskId });
-
-    res.json({
-      success: true,
-      moved: true,
-      sourceListId,
-      destListId,
-      originalTaskId: taskId,
-      newTaskId: insertResult.data.id,
-      title: task.title,
-    });
-  } catch (e) {
-    if (e.message.includes('insufficient')) {
-      return res.status(403).json({ error: 'Tasks write scope not authorized. Visit /auth/info.', authUrl: '/auth/info' });
-    }
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// Update a task's title (e.g., prepend "READY" status)
-app.post('/api/info/tasks/:listId/:taskId/update', async (req, res) => {
-  try {
-    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized', authUrl: '/auth/info' });
-    const { listId, taskId } = req.params;
-    const { title, notes } = req.body;
-    const body = {};
-    if (title) body.title = title;
-    if (notes !== undefined) body.notes = notes;
-    if (!Object.keys(body).length) return res.status(400).json({ error: 'title or notes required' });
-    const result = await infoTasksApi.tasks.patch({
-      tasklist: listId,
-      task: taskId,
-      requestBody: body
-    });
-    res.json({ success: true, task: result.data });
-  } catch (e) {
-    if (e.message.includes('insufficient')) {
-      return res.status(403).json({ error: 'Tasks write scope not authorized. Visit /auth/info.', authUrl: '/auth/info' });
-    }
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// Complete a task (mark as done without moving)
-app.post('/api/info/tasks/:listId/:taskId/complete', async (req, res) => {
-  try {
-    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized', authUrl: '/auth/info' });
-    const { listId, taskId } = req.params;
-    const result = await infoTasksApi.tasks.patch({
-      tasklist: listId,
-      task: taskId,
-      requestBody: { status: 'completed' }
-    });
-    res.json({ success: true, task: result.data });
-  } catch (e) {
-    if (e.message.includes('insufficient')) {
-      return res.status(403).json({ error: 'Tasks write scope not authorized. Visit /auth/info.', authUrl: '/auth/info' });
-    }
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// ─── Info@ Gmail Endpoints ───
-app.get('/api/info/profile', async (req, res) => {
-  try {
-    if (!infoGmail) return res.status(503).json({ error: 'Info@ Gmail not initialized — authorize at /auth/info', authUrl: '/api/auth-url/info' });
-    const profile = await infoGmail.users.getProfile({ userId: 'me' });
-    res.json({ account: 'info@designerwallcoverings.com', ...profile.data });
-  } catch (e) {
-    res.status(500).json({ error: e.message });
-  }
-});
-
-app.get('/api/info/messages', async (req, res) => {
-  try {
-    if (!infoGmail) return res.status(503).json({ error: 'Info@ Gmail not initialized — authorize at /auth/info', authUrl: '/api/auth-url/info' });
-    const { q, maxResults, labelIds, pageToken } = req.query;
-    const params = { userId: 'me', maxResults: parseInt(maxResults) || 20 };
-    if (q) params.q = q;
-    if (labelIds) params.labelIds = labelIds.split(',');
-    if (pageToken) params.pageToken = pageToken;
-
-    const list = await infoGmail.users.messages.list(params);
-    const messages = list.data.messages || [];
-    const detailed = await Promise.all(
-      messages.slice(0, parseInt(maxResults) || 20).map(async m => {
-        try {
-          const full = await infoGmail.users.messages.get({ userId: 'me', id: m.id, format: 'metadata', metadataHeaders: ['Subject', 'From', 'Date', 'To'] });
-          return {
-            id: full.data.id,
-            threadId: full.data.threadId,
-            snippet: full.data.snippet,
-            subject: getHeader(full.data.payload?.headers, 'Subject'),
-            from: getHeader(full.data.payload?.headers, 'From'),
-            to: getHeader(full.data.payload?.headers, 'To'),
-            date: getHeader(full.data.payload?.headers, 'Date'),
-          };
-        } catch { return { id: m.id, error: 'fetch-failed' }; }
-      })
-    );
-    res.json({ account: 'info@designerwallcoverings.com', messages: detailed, nextPageToken: list.data.nextPageToken });
-  } catch (e) {
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// ─── Info@ Google Drive Endpoints ───
-const DRIVE_NOT_READY = { error: 'Info@ Drive not initialized — authorize at /auth/info (needs drive.readonly scope)', authUrl: '/auth/info' };
-
-// List files (supports Drive query syntax in ?q= param)
-app.get('/api/info/drive', async (req, res) => {
-  try {
-    if (!infoDrive) return res.status(503).json(DRIVE_NOT_READY);
-    const { q, maxResults, pageToken, orderBy } = req.query;
-    const params = {
-      pageSize: parseInt(maxResults) || 25,
-      fields: 'nextPageToken, files(id, name, mimeType, size, modifiedTime, createdTime, webViewLink, iconLink, parents, shared)',
-      orderBy: orderBy || 'modifiedTime desc',
-    };
-    if (q) params.q = q;
-    if (pageToken) params.pageToken = pageToken;
-    const result = await infoDrive.files.list(params);
-    res.json({ account: 'info@', files: result.data.files || [], nextPageToken: result.data.nextPageToken });
-  } catch (e) {
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// Get file metadata by ID
-app.get('/api/info/drive/file/:fileId', async (req, res) => {
-  try {
-    if (!infoDrive) return res.status(503).json(DRIVE_NOT_READY);
-    const result = await infoDrive.files.get({
-      fileId: req.params.fileId,
-      fields: 'id, name, mimeType, size, modifiedTime, createdTime, webViewLink, webContentLink, iconLink, parents, shared, description',
-    });
-    res.json(result.data);
-  } catch (e) {
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// Search files by name
-app.get('/api/info/drive/search/:query', async (req, res) => {
-  try {
-    if (!infoDrive) return res.status(503).json(DRIVE_NOT_READY);
-    const searchQ = `name contains '${req.params.query.replace(/'/g, "\\'")}'`;
-    const result = await infoDrive.files.list({
-      q: searchQ,
-      pageSize: parseInt(req.query.maxResults) || 25,
-      fields: 'files(id, name, mimeType, size, modifiedTime, webViewLink, parents)',
-      orderBy: 'modifiedTime desc',
-    });
-    res.json({ account: 'info@', query: req.params.query, files: result.data.files || [] });
-  } catch (e) {
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// List shared drives
-app.get('/api/info/drive/shared', async (req, res) => {
-  try {
-    if (!infoDrive) return res.status(503).json(DRIVE_NOT_READY);
-    const result = await infoDrive.drives.list({ pageSize: 50 });
-    res.json({ account: 'info@', drives: result.data.drives || [] });
-  } catch (e) {
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// ─── Info@ Single Message (with HTML body) ───
-function decodeHtmlBody(parts) {
-  if (!parts) return { html: '', text: '' };
-  let html = '', text = '';
-  for (const part of parts) {
-    if (part.mimeType === 'text/html' && part.body?.data) {
-      html = Buffer.from(part.body.data, 'base64url').toString('utf8');
-    }
-    if (part.mimeType === 'text/plain' && part.body?.data) {
-      text = Buffer.from(part.body.data, 'base64url').toString('utf8');
-    }
-    if (part.parts) {
-      const nested = decodeHtmlBody(part.parts);
-      if (nested.html && !html) html = nested.html;
-      if (nested.text && !text) text = nested.text;
-    }
-  }
-  return { html, text };
-}
-
-app.get('/api/info/messages/:id', async (req, res) => {
-  try {
-    if (!infoGmail) return res.status(503).json({ error: 'Info@ Gmail not initialized', authUrl: '/auth/info' });
-    const msg = await infoGmail.users.messages.get({ userId: 'me', id: req.params.id, format: 'full' });
-    const headers = msg.data.payload?.headers || [];
-
-    // Decode both HTML and text bodies
-    const topBody = msg.data.payload?.body?.data
-      ? Buffer.from(msg.data.payload.body.data, 'base64url').toString('utf8')
-      : null;
-    const { html, text } = decodeHtmlBody(msg.data.payload?.parts);
-
-    const attachments = [];
-    function findAttachments(parts) {
-      if (!parts) return;
-      for (const p of parts) {
-        if (p.filename && p.body?.attachmentId) {
-          attachments.push({ filename: p.filename, mimeType: p.mimeType, size: p.body.size, attachmentId: p.body.attachmentId });
-        }
-        if (p.parts) findAttachments(p.parts);
-      }
-    }
-    findAttachments(msg.data.payload?.parts);
-
-    res.json({
-      id: msg.data.id,
-      threadId: msg.data.threadId,
-      labelIds: msg.data.labelIds,
-      snippet: msg.data.snippet,
-      subject: getHeader(headers, 'Subject'),
-      from: getHeader(headers, 'From'),
-      to: getHeader(headers, 'To'),
-      cc: getHeader(headers, 'Cc'),
-      date: getHeader(headers, 'Date'),
-      bodyHtml: html || topBody || '',
-      bodyText: text || topBody || '',
-      attachments,
-      internalDate: msg.data.internalDate
-    });
-  } catch (e) {
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// ─── Info@ Search Messages ───
-app.get('/api/info/search', async (req, res) => {
-  try {
-    if (!infoGmail) return res.status(503).json({ error: 'Info@ Gmail not initialized', authUrl: '/auth/info' });
-    const { q, maxResults } = req.query;
-    if (!q) return res.status(400).json({ error: 'Query (q) required' });
-    const list = await infoGmail.users.messages.list({ userId: 'me', q, maxResults: parseInt(maxResults) || 10 });
-    const messages = list.data.messages || [];
-
-    const detailed = await Promise.all(
-      messages.map(async m => {
-        try {
-          const full = await infoGmail.users.messages.get({ userId: 'me', id: m.id, format: 'metadata', metadataHeaders: ['Subject', 'From', 'Date', 'To'] });
-          return {
-            id: full.data.id,
-            snippet: full.data.snippet,
-            subject: getHeader(full.data.payload?.headers, 'Subject'),
-            from: getHeader(full.data.payload?.headers, 'From'),
-            to: getHeader(full.data.payload?.headers, 'To'),
-            date: getHeader(full.data.payload?.headers, 'Date')
-          };
-        } catch { return { id: m.id, error: 'fetch-failed' }; }
-      })
-    );
-
-    res.json({ account: 'info@designerwallcoverings.com', query: q, messages: detailed, total: list.data.resultSizeEstimate });
-  } catch (e) {
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// ─── Info@ Download Attachment ───
-app.get('/api/info/messages/:messageId/attachments/:attachmentId', async (req, res) => {
-  try {
-    if (!infoGmail) return res.status(503).json({ error: 'Info@ Gmail not initialized', authUrl: '/auth/info' });
-    const att = await infoGmail.users.messages.attachments.get({
-      userId: 'me', messageId: req.params.messageId, id: req.params.attachmentId
-    });
-    const buf = Buffer.from(att.data.data, 'base64');
-    res.set('Content-Length', buf.length);
-    res.send(buf);
-  } catch (e) {
-    res.status(500).json({ error: e.message });
-  }
-});
-
-// ─── Health Check ───
-app.get('/health', (req, res) => {
-  res.json({
-    status: 'ok',
-    agent: 'George',
-    port: PORT,
-    uptime: process.uptime(),
-    accounts: {
-      steve: { ready: !!gmail, email: 'steve@designerwallcoverings.com', scopes: SCOPES },
-      info: { ready: !!infoGmail, email: 'info@designerwallcoverings.com', scopes: INFO_SCOPES, drive: !!infoDrive, authUrl: infoGmail ? null : '/auth/info' },
-    }
-  });
-});
-
-// ─── Start ───
-app.listen(PORT, () => {
-  console.log(`[${AGENT_NAME}] Gmail Agent running on port ${PORT}`);
-  console.log(`[${AGENT_NAME}] Dashboard: http://45.61.58.125:${PORT}/`);
-  console.log(`[${AGENT_NAME}] API: /api/messages, /api/search, /api/send, /api/labels, /api/profile`);
-});

← c2bdfca add trash-fleet-mailtests.js — one-off cleanup of 89 fleet d  ·  back to George Gmail  ·  fix(public/index.html): wrap xfetch helper in <script> tags 630eea8 →