import type { EmailDraft, ExportResult, ExportedFile, FetchResult } from './types.js'; import { createEnvConfig as createBaseEnvConfig, getAccessToken } from '@clawd/auth-runtime'; import { fetchLeadDataset } from './lead-dataset.js'; import { draftToEml, emlFilename } from './eml.js'; import { loadR2Config, uploadToR2 } from '@clawd/r2-upload'; /** * Fetch lead-dataset from completed workflow. * Returns businesses with emails and reviews for LLM to compose emails. */ export async function fetchLeads( workflowId: string, dryRun: boolean = false, ): Promise { if (!workflowId) { return failedFetch('', 'missing workflow-id'); } if (dryRun) { return { status: 'success', error: null, workflowId, workflowStatus: 'dry_run', query: 'dry-run-query', country: 'us', productKeywords: null, completedAt: null, summary: { businesses_count: 3, contacts_count: 5, businesses_with_reviews: 2, businesses_with_email: 3 }, businesses: [], }; } const config = createBaseEnvConfig(); let accessToken: string; try { accessToken = await getAccessToken(false, config); } catch (err) { return failedFetch(workflowId, err instanceof Error ? err.message : 'failed to exchange token'); } const { data, error } = await fetchLeadDataset(config, accessToken, workflowId, false); if (!data) { return failedFetch(workflowId, error); } return { status: 'success', error: null, workflowId: data.workflow_id, workflowStatus: data.status, query: data.query, country: data.country, productKeywords: data.product_keywords, completedAt: data.completed_at, summary: data.summary, businesses: data.businesses, }; } /** * Export drafts: convert to EML, upload to R2, return URLs. */ export async function exportDrafts( drafts: EmailDraft[], workflowId: string, options: { from?: string; prefix?: string; dryRun?: boolean; } = {}, ): Promise { const from = options.from || process.env.SENDER_EMAIL || 'outreach@company.com'; const prefix = options.prefix || `outreach/eml/${workflowId}/${Date.now()}`; if (!drafts.length) { return { status: 'failed', error: 'no drafts to export', workflowId, totalDrafts: 0, exportedCount: 0, bundleUrl: null, files: [] }; } // Convert all drafts to EML const emlFiles: { filename: string; content: string; draft: EmailDraft }[] = []; for (let i = 0; i < drafts.length; i++) { const draft = drafts[i]; const filename = emlFilename(draft, i); const content = draftToEml(draft, from); emlFiles.push({ filename, content, draft }); } if (options.dryRun) { const files: ExportedFile[] = emlFiles.map(f => ({ filename: f.filename, url: `(dry-run) ${prefix}/${f.filename}`, recipient: f.draft.recipient_email, subject: f.draft.subject, })); return { status: 'success', error: null, workflowId, totalDrafts: drafts.length, exportedCount: files.length, bundleUrl: `(dry-run) ${prefix}/bundle.zip`, files }; } // Upload to R2 const r2Config = loadR2Config(); const files: ExportedFile[] = []; for (const eml of emlFiles) { const key = `${prefix}/${eml.filename}`; const url = await uploadToR2(r2Config, key, eml.content, 'message/rfc822'); files.push({ filename: eml.filename, url, recipient: eml.draft.recipient_email, subject: eml.draft.subject, }); } // Create and upload zip bundle let bundleUrl: string | null = null; try { const { createZipBundle } = await import('./zip.js'); const zipBuffer = await createZipBundle(emlFiles.map(f => ({ name: f.filename, content: f.content }))); bundleUrl = await uploadToR2(r2Config, `${prefix}/bundle.zip`, zipBuffer, 'application/zip'); } catch { // zip is optional } return { status: 'success', error: null, workflowId, totalDrafts: drafts.length, exportedCount: files.length, bundleUrl, files }; } /** * Read drafts from a JSON file. */ export async function readDraftsFile(path: string): Promise { const file = Bun.file(path); if (!await file.exists()) { throw new Error(`file not found: ${path}`); } const raw = await file.json(); return Array.isArray(raw) ? raw : (raw?.drafts ?? []); } function failedFetch(workflowId: string, error: string): FetchResult { return { status: 'failed', error, workflowId, workflowStatus: '', query: '', country: '', productKeywords: null, completedAt: null, summary: null, businesses: [], }; }