146 lines
4.5 KiB
TypeScript
146 lines
4.5 KiB
TypeScript
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<FetchResult> {
|
|
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<ExportResult> {
|
|
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<EmailDraft[]> {
|
|
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: [],
|
|
};
|
|
}
|