← 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
M .gitignoreM server.jsD server.js.bak-20260421-094351
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 →