← 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,
};