From 01ad275a1696becee004eafb614178096aa691be Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sat, 30 May 2026 15:29:39 +0000 Subject: [PATCH] feat: remove Gmail email channel, go Resend-only Deletes GmailProvider, gmail-oauth.ts, cryyer auth gmail, and all Gmail scaffolding from init.ts. EMAIL_PROVIDER is now Resend-only. Updates check.ts, .env.example, CLAUDE.md, and all affected tests. Closes #166 --- .env.example | 8 +- CLAUDE.md | 15 ++-- src/__tests__/check.test.ts | 27 ------ src/__tests__/email-provider.test.ts | 110 +----------------------- src/__tests__/init-workflows.test.ts | 13 --- src/__tests__/init.test.ts | 39 +-------- src/auth.ts | 122 +-------------------------- src/check.ts | 12 +-- src/cli.ts | 2 +- src/email-provider.ts | 95 +-------------------- src/gmail-oauth.ts | 5 -- src/init.ts | 17 +--- 12 files changed, 16 insertions(+), 449 deletions(-) delete mode 100644 src/gmail-oauth.ts diff --git a/.env.example b/.env.example index a9000e5..d18abcd 100644 --- a/.env.example +++ b/.env.example @@ -9,16 +9,12 @@ # GitHub personal access token (repo scope required) GITHUB_TOKEN=ghp_your_token_here -# Email provider: "resend" (default) or "gmail" +# Email provider: "resend" (only supported value) # EMAIL_PROVIDER=resend -# Resend API key (required when EMAIL_PROVIDER=resend or unset) +# Resend API key (required) RESEND_API_KEY=re_your_key_here -# Gmail OAuth refresh token (required when EMAIL_PROVIDER=gmail) -# Obtain via: npx @atriumn/cryyer auth gmail -# GMAIL_REFRESH_TOKEN=1//your_refresh_token_here - # Default sender email address FROM_EMAIL=updates@yourdomain.com diff --git a/CLAUDE.md b/CLAUDE.md index 5cfdcac..efe63d6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,9 +55,8 @@ Key modules: | `summarize.ts` | Builds prompt with product voice, calls LLM provider, parses JSON `{subject, body}` response | | `subscriber-store.ts` | SubscriberStore interface and factory; adapters for Supabase, JSON file, GitHub Gist, Google Sheets. Supports `getSubscribers`, `recordEmailSent`, `addSubscriber`, `removeSubscriber`. | | `mcp.ts` | MCP server entry point; 9 tools + 1 prompt for draft review and subscriber management | -| `email-provider.ts` | EmailProvider interface and factory; adapters for Resend, Gmail | -| `auth.ts` | `cryyer auth gmail` — OAuth 2.0 flow for Gmail authorization | -| `gmail-oauth.ts` | Google OAuth client ID/secret constants | +| `email-provider.ts` | EmailProvider interface and factory; Resend adapter | +| `auth.ts` | `cryyer auth linkedin` — OAuth 2.0 flow for LinkedIn authorization | | `send.ts` | Builds email messages (`sendEmails`), delegates sending to EmailProvider | | `draft-file.ts` | CLI: `cryyer draft-file` — gather activity → LLM draft → write YAML front matter file | | `send-file.ts` | CLI: `cryyer send-file` — read YAML front matter draft → send emails to subscribers | @@ -138,11 +137,8 @@ GITHUB_REPOSITORY # Set by GitHub Actions ### Email Provider Configuration ``` -EMAIL_PROVIDER # "resend" (default) or "gmail" -# Resend (default): -RESEND_API_KEY # Required when EMAIL_PROVIDER=resend (or unset) -# Gmail: -GMAIL_REFRESH_TOKEN # Required when EMAIL_PROVIDER=gmail; set via "cryyer auth gmail" +EMAIL_PROVIDER # "resend" (only supported value; default) +RESEND_API_KEY # Required ``` ### Subscriber Store Configuration @@ -233,9 +229,8 @@ Wraps `cryyer send-file`. Maps all credential inputs to env vars for email and s | `product` | yes | — | Product ID | | `draft-path` | yes | — | Path to the draft markdown file | | `from-email` | yes | — | Sender email address | -| `email-provider` | no | `resend` | Email provider (resend, gmail) | +| `email-provider` | no | `resend` | Email provider (resend) | | `email-api-key` | no | — | Resend API key | -| `gmail-refresh-token` | no | — | Gmail OAuth refresh token | | `from-name` | no | `Cryyer Updates` | Sender display name | | `subscriber-store` | no | `json` | Subscriber store (json, supabase, google-sheets) | | `supabase-url` | no | — | Supabase project URL | diff --git a/src/__tests__/check.test.ts b/src/__tests__/check.test.ts index 21edd73..2fa7861 100644 --- a/src/__tests__/check.test.ts +++ b/src/__tests__/check.test.ts @@ -280,33 +280,6 @@ describe('checkEmailConfig', () => { expect(result.message).toContain('FROM_EMAIL'); }); - it('passes when gmail vars are set', () => { - process.env['EMAIL_PROVIDER'] = 'gmail'; - process.env['GMAIL_REFRESH_TOKEN'] = 'token'; - process.env['FROM_EMAIL'] = 'from@test.com'; - const result = checkEmailConfig(); - expect(result.passed).toBe(true); - expect(result.message).toContain('GMAIL_REFRESH_TOKEN'); - }); - - it('fails when gmail refresh token is missing', () => { - process.env['EMAIL_PROVIDER'] = 'gmail'; - delete process.env['GMAIL_REFRESH_TOKEN']; - process.env['FROM_EMAIL'] = 'from@test.com'; - const result = checkEmailConfig(); - expect(result.passed).toBe(false); - expect(result.message).toContain('GMAIL_REFRESH_TOKEN'); - }); - - it('fails when gmail FROM_EMAIL is missing', () => { - process.env['EMAIL_PROVIDER'] = 'gmail'; - process.env['GMAIL_REFRESH_TOKEN'] = 'token'; - delete process.env['FROM_EMAIL']; - const result = checkEmailConfig(); - expect(result.passed).toBe(false); - expect(result.message).toContain('FROM_EMAIL'); - }); - it('fails for unknown email provider', () => { process.env['EMAIL_PROVIDER'] = 'mailgun'; const result = checkEmailConfig(); diff --git a/src/__tests__/email-provider.test.ts b/src/__tests__/email-provider.test.ts index 4b14c04..dbcc75b 100644 --- a/src/__tests__/email-provider.test.ts +++ b/src/__tests__/email-provider.test.ts @@ -8,14 +8,7 @@ vi.mock('resend', () => ({ }, })); -vi.mock('google-auth-library', () => ({ - OAuth2Client: class MockOAuth2Client { - setCredentials() {} - async getAccessToken() { return { token: 'mock-access-token' }; } - }, -})); - -import { ResendProvider, GmailProvider, createEmailProvider } from '../email-provider.js'; +import { ResendProvider, createEmailProvider } from '../email-provider.js'; import type { EmailMessage } from '../email-provider.js'; describe('ResendProvider', () => { @@ -85,115 +78,14 @@ describe('createEmailProvider', () => { expect(provider).toBeInstanceOf(ResendProvider); }); - it('creates gmail provider when EMAIL_PROVIDER=gmail', () => { - process.env.EMAIL_PROVIDER = 'gmail'; - process.env.GMAIL_REFRESH_TOKEN = 'test_refresh_token'; - const provider = createEmailProvider(); - expect(provider).toBeInstanceOf(GmailProvider); - }); - it('throws when resend key is missing', () => { delete process.env.RESEND_API_KEY; delete process.env.EMAIL_PROVIDER; expect(() => createEmailProvider()).toThrow('Missing RESEND_API_KEY'); }); - it('throws when gmail refresh token is missing', () => { - process.env.EMAIL_PROVIDER = 'gmail'; - delete process.env.GMAIL_REFRESH_TOKEN; - expect(() => createEmailProvider()).toThrow('Missing GMAIL_REFRESH_TOKEN'); - }); - it('throws on unknown provider', () => { process.env.EMAIL_PROVIDER = 'sendgrid'; expect(() => createEmailProvider()).toThrow('Unknown email provider'); }); - - it('accepts override provider', () => { - process.env.GMAIL_REFRESH_TOKEN = 'test_token'; - const provider = createEmailProvider({ provider: 'gmail' }); - expect(provider).toBeInstanceOf(GmailProvider); - }); -}); - -describe('GmailProvider', () => { - afterEach(() => { - vi.restoreAllMocks(); - }); - - it('sends emails via Gmail API and returns stats', async () => { - const mockFetch = vi.spyOn(globalThis, 'fetch').mockResolvedValue( - new Response(JSON.stringify({ id: 'msg-1' }), { status: 200 }) - ); - - const provider = new GmailProvider('mock-refresh-token'); - const emails: EmailMessage[] = [ - { from: 'sender@test.com', to: 'a@test.com', subject: 'Test', html: '

Hi

' }, - ]; - - const result = await provider.sendBatch(emails); - expect(result.sent).toBe(1); - expect(result.failed).toBe(0); - - expect(mockFetch).toHaveBeenCalledWith( - 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send', - expect.objectContaining({ - method: 'POST', - headers: expect.objectContaining({ Authorization: 'Bearer mock-access-token' }), - }) - ); - }); - - it('records failures when Gmail API returns error', async () => { - vi.spyOn(globalThis, 'fetch').mockResolvedValue( - new Response('Bad Request', { status: 400 }) - ); - - const provider = new GmailProvider('mock-refresh-token'); - const emails: EmailMessage[] = [ - { from: 'sender@test.com', to: 'a@test.com', subject: 'Test', html: '

Hi

' }, - ]; - - const result = await provider.sendBatch(emails); - expect(result.sent).toBe(0); - expect(result.failed).toBe(1); - expect(result.failures[0].error).toContain('400'); - }); - - it('records failures when fetch throws', async () => { - vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('Network down')); - - const provider = new GmailProvider('mock-refresh-token'); - const emails: EmailMessage[] = [ - { from: 'sender@test.com', to: 'a@test.com', subject: 'Test', html: '

Hi

' }, - ]; - - const result = await provider.sendBatch(emails); - expect(result.sent).toBe(0); - expect(result.failed).toBe(1); - expect(result.failures[0].error).toContain('Network down'); - }); - - it('includes replyTo and custom headers in RFC2822 message', async () => { - let capturedBody = ''; - vi.spyOn(globalThis, 'fetch').mockImplementation(async (_url, init) => { - capturedBody = JSON.parse((init as RequestInit).body as string).raw; - return new Response(JSON.stringify({ id: 'msg-1' }), { status: 200 }); - }); - - const provider = new GmailProvider('mock-refresh-token'); - await provider.sendBatch([{ - from: 'sender@test.com', - to: 'a@test.com', - subject: 'Test', - html: '

Hi

', - replyTo: 'reply@test.com', - headers: { 'X-Custom': 'value' }, - }]); - - // Decode base64url - const decoded = Buffer.from(capturedBody.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString(); - expect(decoded).toContain('Reply-To: reply@test.com'); - expect(decoded).toContain('X-Custom: value'); - }); }); diff --git a/src/__tests__/init-workflows.test.ts b/src/__tests__/init-workflows.test.ts index b3be2b1..8ce3eb9 100644 --- a/src/__tests__/init-workflows.test.ts +++ b/src/__tests__/init-workflows.test.ts @@ -86,13 +86,6 @@ describe('buildSendWorkflowContent', () => { expect(content).not.toContain('GMAIL_REFRESH_TOKEN'); }); - it('includes gmail credentials for gmail provider', () => { - const content = buildSendWorkflowContent('acme-cli', 'gmail', 'json'); - expect(content).toContain('email-provider: gmail'); - expect(content).toContain('secrets.GMAIL_REFRESH_TOKEN'); - expect(content).not.toContain('RESEND_API_KEY'); - }); - it('includes no store credentials for json store', () => { const content = buildSendWorkflowContent('acme-cli', 'resend', 'json'); expect(content).toContain('subscriber-store: json'); @@ -171,12 +164,6 @@ describe('buildSendUpdateWorkflowContent', () => { expect(content).not.toContain('GMAIL_REFRESH_TOKEN'); }); - it('includes gmail credentials for gmail provider', () => { - const content = buildSendUpdateWorkflowContent('acme-cli', 'gmail', 'json'); - expect(content).toContain('GMAIL_REFRESH_TOKEN'); - expect(content).not.toContain('RESEND_API_KEY'); - }); - it('includes supabase credentials for supabase store', () => { const content = buildSendUpdateWorkflowContent('acme-cli', 'resend', 'supabase'); expect(content).toContain('SUPABASE_URL'); diff --git a/src/__tests__/init.test.ts b/src/__tests__/init.test.ts index 159ae79..ce5e820 100644 --- a/src/__tests__/init.test.ts +++ b/src/__tests__/init.test.ts @@ -29,7 +29,7 @@ describe('parseInitFlags', () => { '--voice', 'Casual', '--llm', 'openai', '--subscriber-store', 'supabase', - '--email-provider', 'gmail', + '--email-provider', 'resend', '--from-email', 'hi@acme.dev', '--yes', '--pipeline', 'weekly', @@ -40,7 +40,7 @@ describe('parseInitFlags', () => { voice: 'Casual', llm: 'openai', subscriberStore: 'supabase', - emailProvider: 'gmail', + emailProvider: 'resend', fromEmail: 'hi@acme.dev', yes: true, pipeline: 'weekly', @@ -124,13 +124,6 @@ describe('buildEnvContent', () => { expect(content).not.toContain('GOOGLE_SHEETS'); }); - it('omits RESEND_API_KEY when email provider is gmail', () => { - const content = buildEnvContent({ ...baseAnswers, emailProvider: 'gmail', resendApiKey: undefined }); - expect(content).toContain('EMAIL_PROVIDER=gmail'); - expect(content).not.toContain('RESEND_API_KEY'); - expect(content).toContain('FROM_EMAIL=updates@acme.dev'); - }); - it('includes supabase vars when store is supabase', () => { const content = buildEnvContent({ ...baseAnswers, @@ -475,34 +468,6 @@ describe('main (init)', () => { await expect(main()).rejects.toThrow('GitHub token is required'); }); - it('handles Gmail email provider selection (skips Resend key)', async () => { - setupFreshDir(); - makeRl([ - 'Acme CLI', 'acme/acme-cli', 'Casual', - '1', 'sk-ant-test', // anthropic - '1', // JSON store - '2', // Gmail - 'ghp_test', // github token (no resend key prompt) - 'updates@acme.dev', // from email - 'n', // skip workflows - ]); - - await main(); - - expect(writeFileSync).toHaveBeenCalledWith( - expect.stringContaining('.env'), - expect.stringContaining('EMAIL_PROVIDER=gmail'), - 'utf-8' - ); - // Should not contain RESEND_API_KEY - const envCalls = (writeFileSync as Mock).mock.calls.filter( - (call) => typeof call[0] === 'string' && call[0].endsWith('.env') - ); - expect(envCalls.length).toBeGreaterThan(0); - const envContent = envCalls[0][1] as string; - expect(envContent).not.toContain('RESEND_API_KEY'); - }); - it('creates weekly workflow files when user selects weekly pipeline', async () => { setupFreshDir(); makeRl(freshAnswers); diff --git a/src/auth.ts b/src/auth.ts index fd6104d..a0776b1 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,13 +1,7 @@ import { createServer } from 'http'; -import { readFileSync, writeFileSync, existsSync } from 'fs'; -import { join } from 'path'; import { exec } from 'child_process'; -import { OAuth2Client } from 'google-auth-library'; -import { GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET } from './gmail-oauth.js'; import { saveLinkedInCredentials, getCredentialsPath } from './social/credentials.js'; -const GMAIL_SCOPE = 'https://www.googleapis.com/auth/gmail.send'; - const LINKEDIN_AUTH_URL = 'https://www.linkedin.com/oauth/v2/authorization'; const LINKEDIN_TOKEN_URL = 'https://www.linkedin.com/oauth/v2/accessToken'; const LINKEDIN_SCOPES = 'openid profile w_member_social'; @@ -22,113 +16,6 @@ function openBrowser(url: string): void { exec(`${cmd} "${url}"`); } -function updateEnvFile(key: string, value: string): void { - const envPath = join(process.cwd(), '.env'); - if (!existsSync(envPath)) { - writeFileSync(envPath, `${key}=${value}\n`, 'utf-8'); - return; - } - - const content = readFileSync(envPath, 'utf-8'); - const regex = new RegExp(`^${key}=.*$`, 'm'); - - if (regex.test(content)) { - writeFileSync(envPath, content.replace(regex, `${key}=${value}`), 'utf-8'); - } else { - const suffix = content.endsWith('\n') ? '' : '\n'; - writeFileSync(envPath, content + suffix + `${key}=${value}\n`, 'utf-8'); - } -} - -export async function authGmail(): Promise { - return new Promise((resolve, reject) => { - const server = createServer(async (req, res) => { - try { - const url = new URL(req.url ?? '/', `http://localhost`); - if (url.pathname !== '/callback') { - res.writeHead(404); - res.end('Not found'); - return; - } - - const code = url.searchParams.get('code'); - const error = url.searchParams.get('error'); - - if (error) { - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end('

Authorization denied

You can close this window.

'); - server.close(); - reject(new Error(`OAuth error: ${error}`)); - return; - } - - if (!code) { - res.writeHead(400, { 'Content-Type': 'text/html' }); - res.end('

Missing authorization code

'); - server.close(); - reject(new Error('No authorization code received')); - return; - } - - // Exchange code for tokens - const redirectUri = `http://localhost:${(server.address() as { port: number }).port}/callback`; - const tokenClient = new OAuth2Client(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, redirectUri); - const { tokens } = await tokenClient.getToken(code); - - if (!tokens.refresh_token) { - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end('

Error

No refresh token received. Try revoking access at myaccount.google.com/permissions and re-running this command.

'); - server.close(); - reject(new Error('No refresh token received. Revoke access and try again.')); - return; - } - - // Save to .env - updateEnvFile('GMAIL_REFRESH_TOKEN', tokens.refresh_token); - updateEnvFile('EMAIL_PROVIDER', 'gmail'); - - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end('

Gmail authorized!

Refresh token saved to .env. You can close this window.

'); - - console.log(''); - console.log(' Gmail authorized successfully!'); - console.log(' GMAIL_REFRESH_TOKEN saved to .env'); - console.log(' EMAIL_PROVIDER set to gmail'); - console.log(''); - - server.close(); - resolve(); - } catch (err) { - res.writeHead(500, { 'Content-Type': 'text/html' }); - res.end('

Error

Something went wrong.

'); - server.close(); - reject(err); - } - }); - - server.listen(0, () => { - const port = (server.address() as { port: number }).port; - const redirectUri = `http://localhost:${port}/callback`; - const authClient = new OAuth2Client(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, redirectUri); - - const authUrl = authClient.generateAuthUrl({ - access_type: 'offline', - prompt: 'consent', - scope: [GMAIL_SCOPE], - }); - - console.log(''); - console.log(' Opening browser for Gmail authorization...'); - console.log(` If it doesn't open, visit: ${authUrl}`); - console.log(''); - - openBrowser(authUrl); - }); - - server.on('error', reject); - }); -} - interface LinkedInTokenResponse { access_token: string; expires_in: number; @@ -292,27 +179,20 @@ export async function main(): Promise { cryyer auth — manage authentication Usage: - cryyer auth gmail Authorize Gmail via OAuth 2.0 cryyer auth linkedin Authorize LinkedIn via OAuth 2.0 -Gmail: Opens browser to authorize sending emails. Refresh token saved to .env. LinkedIn: Opens browser to authorize posting. Credentials saved to ~/.config/cryyer/credentials.json. Requires LINKEDIN_CLIENT_ID and LINKEDIN_CLIENT_SECRET env vars. `.trimStart()); return; } - if (subcommand === 'gmail') { - await authGmail(); - return; - } - if (subcommand === 'linkedin') { await authLinkedIn(); return; } console.error(`Unknown auth provider: ${subcommand}`); - console.error('Supported: gmail, linkedin'); + console.error('Supported: linkedin'); process.exitCode = 1; } diff --git a/src/check.ts b/src/check.ts index 0e43aa4..8f20e94 100644 --- a/src/check.ts +++ b/src/check.ts @@ -191,16 +191,6 @@ export async function checkSubscriberStore(): Promise { export function checkEmailConfig(): CheckResult { const provider = process.env['EMAIL_PROVIDER'] || 'resend'; - if (provider === 'gmail') { - const refreshToken = process.env['GMAIL_REFRESH_TOKEN']; - const fromEmail = process.env['FROM_EMAIL']; - const missing = [!refreshToken && 'GMAIL_REFRESH_TOKEN', !fromEmail && 'FROM_EMAIL'].filter(Boolean).join(', '); - if (missing) { - return { name: 'Email (Gmail)', passed: false, message: `Missing required variables: ${missing}. Run "cryyer auth gmail" to authenticate.` }; - } - return { name: 'Email (Gmail)', passed: true, message: 'GMAIL_REFRESH_TOKEN and FROM_EMAIL are set' }; - } - if (provider === 'resend') { const resendKey = process.env['RESEND_API_KEY']; const fromEmail = process.env['FROM_EMAIL']; @@ -214,7 +204,7 @@ export function checkEmailConfig(): CheckResult { return { name: 'Email config', passed: false, - message: `Unknown EMAIL_PROVIDER: '${provider}'. Supported: resend, gmail`, + message: `Unknown EMAIL_PROVIDER: '${provider}'. Supported: resend`, }; } diff --git a/src/cli.ts b/src/cli.ts index 23fba64..9416e48 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -22,7 +22,7 @@ Usage: Commands: init Interactive product setup - auth Manage authentication (gmail, linkedin) + auth Manage authentication (linkedin) check Validate config and connections run Full pipeline: gather → draft → send draft Generate drafts → create GitHub issues diff --git a/src/email-provider.ts b/src/email-provider.ts index a4a788c..36f49ae 100644 --- a/src/email-provider.ts +++ b/src/email-provider.ts @@ -1,6 +1,4 @@ import { Resend } from 'resend'; -import { OAuth2Client } from 'google-auth-library'; -import { GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET } from './gmail-oauth.js'; export interface EmailMessage { from: string; @@ -70,93 +68,9 @@ export class ResendProvider implements EmailProvider { } } -// --- GmailProvider --- - -function buildRfc2822Message(email: EmailMessage): string { - const lines: string[] = [ - `From: ${email.from}`, - `To: ${email.to}`, - `Subject: ${email.subject}`, - 'MIME-Version: 1.0', - 'Content-Type: text/html; charset=UTF-8', - ]; - - if (email.replyTo) { - lines.push(`Reply-To: ${email.replyTo}`); - } - - if (email.headers) { - for (const [key, value] of Object.entries(email.headers)) { - lines.push(`${key}: ${value}`); - } - } - - lines.push('', email.html); - return lines.join('\r\n'); -} - -function base64UrlEncode(str: string): string { - return Buffer.from(str) - .toString('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=+$/, ''); -} - -export class GmailProvider implements EmailProvider { - private refreshToken: string; - - constructor(refreshToken: string) { - this.refreshToken = refreshToken; - } - - private async getAccessToken(): Promise { - const oauth2Client = new OAuth2Client(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET); - oauth2Client.setCredentials({ refresh_token: this.refreshToken }); - const { token } = await oauth2Client.getAccessToken(); - if (!token) throw new Error('Failed to obtain Gmail access token from refresh token'); - return token; - } - - async sendBatch(emails: EmailMessage[]): Promise { - const stats: BatchResult = { sent: 0, failed: 0, failures: [] }; - const accessToken = await this.getAccessToken(); - - for (const email of emails) { - try { - const raw = base64UrlEncode(buildRfc2822Message(email)); - const response = await fetch( - 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send', - { - method: 'POST', - headers: { - Authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ raw }), - } - ); - - if (!response.ok) { - const body = await response.text(); - stats.failed++; - stats.failures.push({ email: email.to, error: `Gmail API ${response.status}: ${body}` }); - } else { - stats.sent++; - } - } catch (err) { - stats.failed++; - stats.failures.push({ email: email.to, error: String(err) }); - } - } - - return stats; - } -} - // --- Factory --- -export type EmailProviderType = 'resend' | 'gmail'; +export type EmailProviderType = 'resend'; export function createEmailProvider(overrides?: { provider?: EmailProviderType; @@ -169,12 +83,7 @@ export function createEmailProvider(overrides?: { if (!apiKey) throw new Error('Missing RESEND_API_KEY environment variable'); return new ResendProvider(apiKey); } - case 'gmail': { - const refreshToken = process.env.GMAIL_REFRESH_TOKEN; - if (!refreshToken) throw new Error('Missing GMAIL_REFRESH_TOKEN environment variable. Run "cryyer auth gmail" to authenticate.'); - return new GmailProvider(refreshToken); - } default: - throw new Error(`Unknown email provider: ${providerName}. Supported: resend, gmail`); + throw new Error(`Unknown email provider: ${providerName}. Supported: resend`); } } diff --git a/src/gmail-oauth.ts b/src/gmail-oauth.ts deleted file mode 100644 index b8de9af..0000000 --- a/src/gmail-oauth.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Google OAuth 2.0 client credentials for cryyer Gmail integration. -// These are placeholder values — replace with real credentials after creating -// a Google Cloud project with the Gmail API enabled. -export const GMAIL_CLIENT_ID = 'PLACEHOLDER.apps.googleusercontent.com'; -export const GMAIL_CLIENT_SECRET = 'PLACEHOLDER'; diff --git a/src/init.ts b/src/init.ts index bd58029..4774d2b 100644 --- a/src/init.ts +++ b/src/init.ts @@ -43,10 +43,9 @@ const LLM_KEY_PROMPTS: Record = { gemini: 'Gemini API key (aistudio.google.com/apikey)', }; -const EMAIL_PROVIDERS = ['resend', 'gmail'] as const; +const EMAIL_PROVIDERS = ['resend'] as const; const EMAIL_PROVIDER_LABELS: Record = { resend: 'Resend', - gmail: 'Gmail', }; const SUBSCRIBER_STORES = ['json', 'supabase', 'google-sheets'] as const; @@ -130,11 +129,6 @@ export function buildSubscribersJson(productId: string): string { return JSON.stringify(data, null, 2) + '\n'; } -const EMAIL_SECRET_NAMES: Record = { - resend: 'RESEND_API_KEY', - gmail: 'GMAIL_REFRESH_TOKEN', -}; - const GITIGNORE_ENTRIES = ['.env', 'subscribers.json', 'email-log.json']; const GITIGNORE_HEADER = '# cryyer'; @@ -208,8 +202,6 @@ export function buildSendWorkflowContent( const emailInputs: string[] = []; if (emailProvider === 'resend') { emailInputs.push(` email-api-key: \${{ secrets.RESEND_API_KEY }}`); - } else if (emailProvider === 'gmail') { - emailInputs.push(` gmail-refresh-token: \${{ secrets.GMAIL_REFRESH_TOKEN }}`); } const storeInputs: string[] = []; @@ -301,8 +293,6 @@ export function buildSendUpdateWorkflowContent( if (emailProvider === 'resend') { envLines.push(` RESEND_API_KEY: \${{ secrets.RESEND_API_KEY }}`); - } else if (emailProvider === 'gmail') { - envLines.push(` GMAIL_REFRESH_TOKEN: \${{ secrets.GMAIL_REFRESH_TOKEN }}`); } if (subscriberStore === 'supabase') { @@ -720,7 +710,6 @@ export async function main(): Promise { workflowSecrets.push(LLM_KEY_NAMES[llmProvider]); workflowSecrets.push('FROM_EMAIL'); if (emailProvider === 'resend') workflowSecrets.push('RESEND_API_KEY'); - if (emailProvider === 'gmail') workflowSecrets.push('GMAIL_REFRESH_TOKEN'); if (subscriberStore === 'supabase') workflowSecrets.push('SUPABASE_URL', 'SUPABASE_SERVICE_KEY'); if (subscriberStore === 'google-sheets') workflowSecrets.push('GOOGLE_SHEETS_SPREADSHEET_ID', 'GOOGLE_SERVICE_ACCOUNT_EMAIL', 'GOOGLE_PRIVATE_KEY'); } @@ -743,10 +732,6 @@ export async function main(): Promise { console.log(` ${step}. Set GitHub secrets ${workflowSecrets.join(', ')}`); step++; } - if (emailProvider === 'gmail') { - console.log(` ${step}. Authorize Gmail npx @atriumn/cryyer auth gmail`); - step++; - } if (subscriberStore === 'json') { console.log(` ${step}. Add subscribers Edit subscribers.json`); } else if (subscriberStore === 'supabase') {