#!/usr/bin/env node import { google } from 'googleapis'; import minimist from 'minimist'; import { getAuthClient, parseAccountArg } from './auth/auth-utils.js'; import { logAction } from './action-logger.js'; async function findAndLabelPaywallEmails(args) { const limit = args.limit || 50; const dryRun = args['dry-run'] || false; // Get authenticated client for specified account (or default) const accountId = parseAccountArg(args); const oauth2Client = await getAuthClient(accountId); const gmail = google.gmail({ version: 'v1', auth: oauth2Client }); // Search for Every emails const searchResult = await gmail.users.messages.list({ userId: 'me', q: 'from:hello@every.to', maxResults: limit }); if (!searchResult.data.messages) { return { success: true, found: 0, paywalled: 0, labeled: 0 }; } const results = { paywalled: [], full: [], errors: [] }; console.error(`Analyzing ${searchResult.data.messages.length} Every emails...`); for (const msg of searchResult.data.messages) { try { // Get full message const message = await gmail.users.messages.get({ userId: 'me', id: msg.id, format: 'full' }); const headers = message.data.payload.headers; const getHeader = (name) => headers.find(h => h.name === name)?.value || ''; const subject = getHeader('Subject'); // Extract body let body = ''; const extractBody = (payload) => { if (payload.body && payload.body.data) { return Buffer.from(payload.body.data, 'base64').toString('utf-8'); } if (payload.parts) { for (const part of payload.parts) { if (part.mimeType === 'text/html' && part.body.data) { return Buffer.from(part.body.data, 'base64').toString('utf-8'); } if (part.parts) { const nested = extractBody(part); if (nested) return nested; } } } return ''; }; body = extractBody(message.data.payload); // Check for paywall indicators const paywallIndicators = [ 'This is a free preview of a subscribers-only post', 'Become a paid subscriber', 'paid subscribers only', 'Subscribe to continue reading', 'upgrade to read' ]; const isPaywalled = paywallIndicators.some(indicator => body.toLowerCase().includes(indicator.toLowerCase()) ); if (isPaywalled) { results.paywalled.push({ id: msg.id, subject: subject }); } else { results.full.push({ id: msg.id, subject: subject }); } } catch (error) { results.errors.push({ id: msg.id, error: error.message }); } } // Apply labels if not dry run let labeled = 0; if (!dryRun && results.paywalled.length > 0) { console.error(`\nLabeling ${results.paywalled.length} paywalled articles...`); // Get label ID const labelsResponse = await gmail.users.labels.list({ userId: 'me' }); const label = labelsResponse.data.labels.find(l => l.name === 'Every - Paywall'); if (!label) { throw new Error('Label "Every - Paywall" not found. Create it first.'); } for (const email of results.paywalled) { try { await gmail.users.messages.modify({ userId: 'me', id: email.id, requestBody: { addLabelIds: [label.id], removeLabelIds: ['INBOX'] } }); labeled++; } catch (error) { console.error(`Failed to label ${email.id}: ${error.message}`); } } } return { success: true, analyzed: searchResult.data.messages.length, paywalled: results.paywalled.length, full: results.full.length, errors: results.errors.length, labeled: labeled, dryRun: dryRun, paywalledEmails: results.paywalled, fullEmails: results.full }; } // Main execution const args = minimist(process.argv.slice(2), { boolean: ['dry-run'] }); const startTime = Date.now(); findAndLabelPaywallEmails(args) .then(result => { // Log the action logAction('paywall-cleanup', args, result, { account: args.account || 'default', duration: Date.now() - startTime }); console.log(JSON.stringify(result, null, 2)); }) .catch(error => { const errorResult = { success: false, error: error.message }; // Log the failed action logAction('paywall-cleanup', args, errorResult, { account: args.account || 'default', duration: Date.now() - startTime, failed: true }); console.error(JSON.stringify(errorResult, null, 2)); process.exit(1); });