← back to Rentv Adintel

src/connectors/gmail.js

177 lines

'use strict';

/**
 * Gmail API importer — spec §16.
 *
 * ADMIN-ONLY and DISABLED BY DEFAULT.
 * `isEnabled()` returns false unless GMAIL_IMPORT_ENABLED=true is set.
 *
 * This module documents the OAuth least-privilege query shape and the
 * intended import flow but does NOT implement live OAuth.  Implementing
 * live OAuth requires a human-driven consent flow (browser redirect) that
 * cannot run headlessly without the GMAIL_REFRESH_TOKEN being pre-obtained.
 *
 * Least-privilege scope: gmail.readonly — access existing messages and
 * settings, no compose, no delete.
 *
 * Example Gmail search queries (§16):
 *
 *   // RENTV-owned messages with advertising/sponsorship signals
 *   (from:(rentv.com) OR from:(shared1.ccsend.com) OR subject:(RENTV))
 *   (sponsor OR sponsored OR advertiser OR advertising OR "Property Spotlight" OR "CRE Talk")
 *
 *   // Collect only messages with attachments (creatives, PDFs, rate cards)
 *   from:(rentv.com) has:attachment
 *
 *   // Constant Contact delivery receipts / campaign archives
 *   from:(shared1.ccsend.com) subject:(RENTV)
 *
 * The importer NEVER mirrors the full mailbox.  Only messages matching the
 * configured GMAIL_IMPORT_QUERY are fetched, and only selected fields and
 * attachments are stored.
 *
 * @module src/connectors/gmail
 */

// ---------------------------------------------------------------------------
// Feature gate
// ---------------------------------------------------------------------------

/**
 * Returns true only when Gmail import is explicitly enabled by the admin.
 * @returns {boolean}
 */
function isEnabled() {
  return process.env.GMAIL_IMPORT_ENABLED === 'true';
}

// ---------------------------------------------------------------------------
// Disabled guard
// ---------------------------------------------------------------------------

function requireEnabled() {
  if (!isEnabled()) {
    throw new Error(
      'Gmail import is disabled. ' +
      'Set GMAIL_IMPORT_ENABLED=true and provide GMAIL_CLIENT_ID, ' +
      'GMAIL_CLIENT_SECRET, and GMAIL_REFRESH_TOKEN to enable. ' +
      'This feature is admin-only and requires explicit authorization. ' +
      'Live OAuth is not implemented — obtain a refresh token via the ' +
      'Google OAuth 2.0 Playground (https://developers.google.com/oauthplayground) ' +
      'with scope: https://www.googleapis.com/auth/gmail.readonly'
    );
  }
}

// ---------------------------------------------------------------------------
// Connection test (stub — no live OAuth)
// ---------------------------------------------------------------------------

/**
 * Test the Gmail import connection.
 * Throws a friendly error when disabled.
 *
 * TODO live implementation:
 *   const { google } = require('googleapis');  // not installed
 *   const auth = new google.auth.OAuth2(
 *     process.env.GMAIL_CLIENT_ID,
 *     process.env.GMAIL_CLIENT_SECRET
 *   );
 *   auth.setCredentials({ refresh_token: process.env.GMAIL_REFRESH_TOKEN });
 *   const gmail = google.gmail({ version: 'v1', auth });
 *   const profile = await gmail.users.getProfile({ userId: 'me' });
 *   return { connected: true, email: profile.data.emailAddress };
 *
 * @returns {{ connected: boolean, demo: boolean, error: string }}
 */
async function connectionTest() {
  requireEnabled();
  throw new Error('Gmail live OAuth not implemented — obtain a refresh token manually');
}

// ---------------------------------------------------------------------------
// Message search (stub)
// ---------------------------------------------------------------------------

/**
 * Search the authorized mailbox using the configured query.
 *
 * TODO live implementation:
 *   const messages = [];
 *   let pageToken;
 *   do {
 *     const res = await gmail.users.messages.list({
 *       userId: 'me',
 *       q: process.env.GMAIL_IMPORT_QUERY || DEFAULT_QUERY,
 *       maxResults: 500,
 *       pageToken,
 *     });
 *     messages.push(...(res.data.messages || []));
 *     pageToken = res.data.nextPageToken;
 *   } while (pageToken);
 *   return messages; // [{ id, threadId }]
 *
 * Default queries (§16):
 *   (from:(rentv.com) OR from:(shared1.ccsend.com) OR subject:(RENTV))
 *   (sponsor OR sponsored OR advertiser OR advertising OR "Property Spotlight" OR "CRE Talk")
 *
 * @returns {Promise<Array<{ id: string, threadId: string }>>}
 */
async function searchMessages() {
  requireEnabled();
  throw new Error('Gmail import disabled');
}

// ---------------------------------------------------------------------------
// Message fetch + selective field extraction (stub)
// ---------------------------------------------------------------------------

/**
 * Fetch a single message and extract evidence fields.
 *
 * TODO live implementation:
 *   const msg = await gmail.users.messages.get({
 *     userId: 'me',
 *     id: messageId,
 *     format: 'full',
 *   });
 *   // Extract headers
 *   const headers = msg.data.payload.headers;
 *   const from    = headers.find(h => h.name === 'From')?.value;
 *   const subject = headers.find(h => h.name === 'Subject')?.value;
 *   const date    = headers.find(h => h.name === 'Date')?.value;
 *   // Extract body (base64url decode)
 *   // Extract attachment metadata (do not auto-open tracking links)
 *   // Return structured evidence: { from, subject, date, bodyExcerpt, attachments }
 *
 * @param {string} messageId
 * @returns {Promise<object>}
 */
async function fetchMessage(messageId) {
  requireEnabled();
  throw new Error('Gmail import disabled');
}

// ---------------------------------------------------------------------------
// Full import (stub)
// ---------------------------------------------------------------------------

/**
 * Run a full Gmail import pass.
 * NEVER auto-opens email tracking links.
 *
 * @param {{ dryRun?: boolean, query?: string }} options
 */
async function importAll({ dryRun = false, query: gmailQuery } = {}) {
  requireEnabled();
  throw new Error('Gmail import disabled');
}

module.exports = {
  isEnabled,
  connectionTest,
  searchMessages,
  fetchMessage,
  importAll,
};