Skill: merjua14/cold-email-engine (version 1.0.0)

--- name: cold-email-engine description: Automated cold email outreach system with lead enrichment, personalized templates, drip sequences, and CAN-SPAM compliance. Use when building outbound sales pipelines, prospecting businesses, automating email campaigns, enriching leads with contact info, setting up drip follow-ups, or managing cold outreach at scale. Supports Resend, SendGrid, or any SMTP provider. --- # Cold Email Engine Automated outbound email pipeline: find leads → enrich contacts → send personalized emails → drip follow-ups. ## Pipeline 1. **Source leads** — from CSV, Google Sheets, API scraping, or manual input 2. **Enrich** — find emails via website scraping, Hunter.io, or Apollo 3. **Personalize** — variable substitution in templates ({name}, {company}, {pain_point}) 4. **Send** — via Resend, SendGrid, or raw SMTP with rate limiting 5. **Drip** — automated follow-ups at day 3 and day 7 6. **Track** — log all sends, bounces, replies to CSV/Sheets ## Requirements - **Email provider API key** — Resend (`RESEND_API_KEY`), SendGrid, or SMTP credentials - **Verified sending domain** — with SPF, DKIM, DMARC configured - **Lead source** — CSV file, Google Sheet ID, or API endpoint ## Quick Start ```bash # Set environment export RESEND_API_KEY=your_key # Send from CSV node scripts/cold-email-engine.js --source leads.csv --template templates/default.txt --from "Name <[REDACTED_SECRET]>" # Dry run (no emails sent) node scripts/cold-email-engine.js --source leads.csv --template templates/default.txt --dry-run # Run drip follow-ups node scripts/cold-email-engine.js --drip --days 3 ``` ## Configuration Edit `scripts/config.json`: - `maxPerDay`: Daily send limit per domain (default: 25) - `delayBetweenMs`: Delay between emails in ms (default: 3000) - `dripDays`: Follow-up schedule [3, 7] days after initial send - `suppressionFile`: Path to suppression/unsubscribe list - `trackingFile`: Path to send log ## Templates Templates use `{variable}` syntax. Available variables: - `{first_name}`, `{last_name}`, `{email}` - `{company}`, `{website}`, `{city}`, `{state}` - `{pain_point}` — auto-generated from website analysis - `{sender_name}`, `{sender_title}` ### Template Example ``` Subject: {company} — quick question Hi {first_name}, I noticed {company} {pain_point}. We help businesses like yours fix that in under a week. Would it make sense to chat for 10 minutes this week? {sender_name} {sender_title} ``` ## Compliance - CAN-SPAM: physical address in footer, unsubscribe mechanism - GDPR: only email businesses (B2B exemption), honor removal requests - Rate limits: 25/day per domain, 3-second delay between sends - Suppression list checked before every send ## Scripts - `scripts/cold-email-engine.js` — Main send engine - `scripts/enrich-leads.js` — Email finder from websites/domains - `scripts/config.json` — Configuration ## References - See `references/deliverability.md` for domain warmup and inbox placement - See `references/templates.md` for proven email templates by industry

path: references/deliverability.md
content: # Email Deliverability Guide

## Domain Setup (Required)

### SPF Record
Add to DNS TXT: `v=spf1 include:_spf.resend.com ~all`

### DKIM
Follow your provider's DKIM setup (Resend auto-generates DKIM records).

### DMARC
Add DNS TXT: `v=DMARC1; p=none; rua=mailto:[REDACTED_SECRET]`

## Warmup Schedule

New domains MUST be warmed up:
- **Days 1-3**: 5 emails/day
- **Days 4-7**: 10 emails/day
- **Days 8-14**: 15 emails/day
- **Days 15-21**: 20 emails/day
- **Day 22+**: 25 emails/day (cruising speed)

## Inbox Placement Tips

- Keep emails under 150 words
- No images in cold emails (triggers spam filters)
- No links in first email (or max 1 link)
- Personalize subject and first line
- Avoid spam trigger words: "free", "guarantee", "act now", "limited time"
- Plain text outperforms HTML for cold outreach
- Reply-to should match from address
- Include 1-2 sentence footer with physical address

## Bounce Management

- **Hard bounce**: Remove immediately, never email again
- **Soft bounce**: Retry once after 48 hours, then remove
- **Bounce rate >2%**: Pause and clean your list

## Monitoring

- Track open rates (target: 40-60%)
- Track reply rates (target: 5-15%)
- Track bounce rates (target: <2%)
- If open rate drops below 20%, check deliverability


path: references/templates.md
content: # Proven Cold Email Templates

## Template 1: The Website Critique (Web Design / Marketing)
```
Subject: Quick question about {company}'s website

Hi {first_name},

I was looking at {company}'s website and noticed {pain_point}.

I help businesses in {city} fix exactly this. Usually takes about a week and the results speak for themselves.

Worth a quick 10-min chat?

{sender_name}
```

## Template 2: The Competitor Angle (Any B2B)
```
Subject: How {competitor} is getting ahead

Hi {first_name},

I noticed {competitor} recently {specific_action}. Companies in {industry} that don't adapt tend to fall behind fast.

We help businesses like {company} stay competitive with {service}. Happy to share what we're seeing work.

Open to a quick call this week?

{sender_name}
```

## Template 3: The Data Hook (Analytics / SaaS)
```
Subject: {company} is leaving money on the table

Hi {first_name},

I ran a quick analysis on {company}'s {website/social/presence} and found 3 things costing you customers:

1. {issue_1}
2. {issue_2}
3. {issue_3}

I put together a free report with fixes. Want me to send it over?

{sender_name}
```

## Template 4: The Referral Ask (Warm-ish)
```
Subject: {mutual_connection} suggested I reach out

Hi {first_name},

{mutual_connection} mentioned you might be looking for help with {pain_point}. We just helped {similar_company} solve the same problem and they saw {result}.

Would love to share how we did it. Free to chat Thursday or Friday?

{sender_name}
```

## Template 5: The Break-Up (Last Follow-Up)
```
Subject: Closing the loop

Hi {first_name},

I've reached out a couple times about helping {company} with {service}. I know you're busy, so I won't keep following up.

If it ever becomes a priority, I'm here. Just reply to this email.

Best,
{sender_name}
```

## Subject Line Formulas (Ranked by Open Rate)
1. "{company} — quick question" (45-55%)
2. "Noticed something about {company}" (40-50%)
3. "{first_name}, quick thought" (35-45%)
4. "Idea for {company}" (30-40%)
5. "{mutual_connection} suggested I reach out" (50-60% when genuine)


path: scripts/cold-email-engine.js
content: #!/usr/bin/env node
/**
 * Cold Email Engine — Automated outbound email pipeline
 * 
 * Usage:
 *   node cold-email-engine.js --source leads.csv --template template.txt --from "Name <email>"
 *   node cold-email-engine.js --source leads.csv --dry-run
 *   node cold-email-engine.js --drip --days 3
 *
 * Environment:
 *   RESEND_API_KEY — Resend API key
 *   SENDGRID_API_KEY — SendGrid API key (alternative)
 */

const fs = require('fs');
const path = require('path');
const https = require('https');

const configPath = path.join(__dirname, 'config.json');
const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, 'utf8')) : {};

const RESEND_KEY = process.env.RESEND_API_KEY;
const SENDGRID_KEY = process.env.SENDGRID_API_KEY;
const DRY_RUN = process.argv.includes('--dry-run');
const IS_DRIP = process.argv.includes('--drip');

function parseArgs() {
  const args = {};
  for (let i = 2; i < process.argv.length; i++) {
    if (process.argv[i] === '--source') args.source = process.argv[++i];
    if (process.argv[i] === '--template') args.template = process.argv[++i];
    if (process.argv[i] === '--from') args.from = process.argv[++i];
    if (process.argv[i] === '--days') args.days = parseInt(process.argv[++i]);
  }
  return args;
}

function httpPost(url, body, headers = {}) {
  return new Promise((resolve, reject) => {
    const u = new URL(url);
    const data = JSON.stringify(body);
    const req = https.request({
      hostname: u.hostname, path: u.pathname, method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data), ...headers }
    }, res => {
      let d = ''; res.on('data', c => d += c);
      res.on('end', () => resolve({ status: res.statusCode, data: d }));
    });
    req.on('error', reject);
    req.write(data); req.end();
  });
}

// Parse CSV (simple — handles quoted fields)
function parseCSV(filepath) {
  const content = fs.readFileSync(filepath, 'utf8');
  const lines = content.trim().split('\n');
  const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, ''));
  return lines.slice(1).map(line => {
    const values = line.match(/(".*?"|[^,]+)/g) || [];
    const obj = {};
    headers.forEach((h, i) => { obj[h] = (values[i] || '').replace(/"/g, '').trim(); });
    return obj;
  });
}

// Load template
function loadTemplate(filepath) {
  return fs.readFileSync(filepath, 'utf8');
}

// Fill template variables
function fillTemplate(template, lead, sender) {
  let filled = template;
  const vars = { ...lead, ...sender };
  for (const [key, val] of Object.entries(vars)) {
    filled = filled.replace(new RegExp(`\\{${key}\\}`, 'g'), val || '');
  }
  return filled;
}

// Extract subject from template (first line: "Subject: ...")
function extractSubject(filled) {
  const match = filled.match(/^Subject:\s*(.+)/i);
  if (match) {
    return { subject: match[1].trim(), body: filled.replace(/^Subject:.+\n+/i, '').trim() };
  }
  return { subject: 'Quick question', body: filled.trim() };
}

// Load suppression list
function loadSuppression() {
  const file = config.suppressionFile || path.join(__dirname, 'suppression.txt');
  if (!fs.existsSync(file)) return new Set();
  return new Set(fs.readFileSync(file, 'utf8').split('\n').map(e => e.trim().toLowerCase()).filter(Boolean));
}

// Log send
function logSend(lead, status) {
  const file = config.trackingFile || path.join(__dirname, 'send-log.csv');
  const exists = fs.existsSync(file);
  if (!exists) fs.writeFileSync(file, 'timestamp,email,company,status,template\n');
  fs.appendFileSync(file, `${new Date().toISOString()},${lead.email},${lead.company || ''},${status},initial\n`);
}

// Send via Resend
async function sendResend(from, to, subject, body) {
  if (DRY_RUN) { console.log(`  [DRY] Would send to ${to}: ${subject}`); return true; }
  const res = await httpPost('https://api.resend.com/emails', {
    from, to: [to], subject, text: body
  }, { 'Authorization': 'Bearer ' + RESEND_KEY });
  const json = JSON.parse(res.data);
  return !!json.id;
}

// Send via SendGrid
async function sendSendGrid(from, to, subject, body) {
  if (DRY_RUN) { console.log(`  [DRY] Would send to ${to}: ${subject}`); return true; }
  const res = await httpPost('https://api.sendgrid.com/v3/mail/send', {
    personalizations: [{ to: [{ email: to }] }],
    from: { email: from },
    subject, content: [{ type: 'text/plain', value: body }]
  }, { 'Authorization': 'Bearer ' + SENDGRID_KEY });
  return res.status >= 200 && res.status < 300;
}

function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }

// Drip follow-up logic
async function runDrip(args) {
  const file = config.trackingFile || path.join(__dirname, 'send-log.csv');
  if (!fs.existsSync(file)) { console.log('No send log found. Run initial campaign first.'); return; }
  
  const days = args.days || 3;
  const cutoff = new Date(Date.now() - days * 86400000);
  const log = parseCSV(file);
  const candidates = log.filter(l => {
    const sent = new Date(l.timestamp);
    return l.status === 'sent' && sent < cutoff;
  });
  
  console.log(`Found ${candidates.length} leads eligible for ${days}-day follow-up`);
  // Would send follow-up template here
}

// Main
async function main() {
  const args = parseArgs();
  
  if (IS_DRIP) { await runDrip(args); return; }
  
  if (!args.source) { console.log('Usage: node cold-email-engine.js --source leads.csv --template template.txt [--from "Name <email>"] [--dry-run]'); return; }
  if (!RESEND_KEY && !SENDGRID_KEY && !DRY_RUN) { console.error('Set RESEND_API_KEY or SENDGRID_API_KEY'); return; }

  const leads = parseCSV(args.source);
  const template = args.template ? loadTemplate(args.template) : config.defaultTemplate || 'Hi {first_name}, I wanted to reach out about {company}.';
  const from = args.from || config.from || '[REDACTED_SECRET]';
  const suppression = loadSuppression();
  const maxPerDay = config.maxPerDay || 25;
  const delay = config.delayBetweenMs || 3000;
  const send = RESEND_KEY ? sendResend : sendSendGrid;

  console.log(`\n📧 Cold Email Engine`);
  console.log(`Leads: ${leads.length} | Max/day: ${maxPerDay} | Delay: ${delay}ms | ${DRY_RUN ? 'DRY RUN' : 'LIVE'}\n`);

  let sent = 0, skipped = 0, failed = 0;

  for (const lead of leads) {
    if (sent >= maxPerDay) { console.log(`\n⚠️  Daily limit (${maxPerDay}) reached. Resume tomorrow.`); break; }
    if (!lead.email) { skipped++; continue; }
    if (suppression.has(lead.email.toLowerCase())) { console.log(`  ⏭️  Suppressed: ${lead.email}`); skipped++; continue; }

    const filled = fillTemplate(template, lead, { sender_name: from.split('<')[0].trim(), sender_title: config.senderTitle || '' });
    const { subject, body } = extractSubject(filled);

    try {
      const ok = await send(from, lead.email, subject, body);
      if (ok) {
        sent++;
        logSend(lead, 'sent');
        console.log(`✅ ${sent}. ${lead.email} — "${subject}"`);
      } else {
        failed++;
        logSend(lead, 'failed');
        console.log(`❌ Failed: ${lead.email}`);
      }
    } catch (e) {
      failed++;
      logSend(lead, 'error');
      console.log(`❌ Error: ${lead.email} — ${e.message}`);
    }

    if (sent < leads.length) await sleep(delay);
  }

  console.log(`\n📊 Done: ${sent} sent, ${skipped} skipped, ${failed} failed`);
}

main().catch(e => { console.error('Error:', e.message); process.exit(1); });


path: scripts/config.json
content: {
  "maxPerDay": 25,
  "delayBetweenMs": 3000,
  "dripDays": [3, 7],
  "suppressionFile": "suppression.txt",
  "trackingFile": "send-log.csv",
  "from": "Your Name <[REDACTED_SECRET]>",
  "senderTitle": "Founder",
  "defaultTemplate": "Subject: {company} — quick question\n\nHi {first_name},\n\nI noticed {company} {pain_point}. We help businesses like yours fix that in under a week.\n\nWould it make sense to chat for 10 minutes this week?\n\n{sender_name}\n{sender_title}"
}


path: scripts/enrich-leads.js
content: #!/usr/bin/env node
/**
 * Lead Enrichment — Find emails from business websites/domains
 * 
 * Usage:
 *   node enrich-leads.js --source leads.csv --output enriched.csv
 *   node enrich-leads.js --domain example.com
 */

const https = require('https');
const fs = require('fs');

function httpGet(url, headers = {}) {
  return new Promise((resolve, reject) => {
    const u = new URL(url);
    https.get({ hostname: u.hostname, path: u.pathname + u.search, headers: { 'User-Agent': 'Mozilla/5.0', ...headers } }, res => {
      let d = ''; res.on('data', c => d += c);
      res.on('end', () => resolve({ status: res.statusCode, data: d }));
    }).on('error', reject);
  });
}

// Extract emails from HTML
function extractEmails(html) {
  const matches = html.match(/[\w.+-]+@[\w-]+\.[\w.]+/g) || [];
  return [...new Set(matches)].filter(e =>
    !e.includes('example.com') && !e.includes('wixpress') &&
    !e.includes('sentry.io') && !e.includes('schema.org') &&
    !e.endsWith('.png') && !e.endsWith('.jpg') && !e.endsWith('.gif')
  );
}

// Try multiple pages to find email
async function findEmail(domain) {
  const pages = [
    `https://${domain}`,
    `https://${domain}/contact`,
    `https://${domain}/about`,
    `https://${domain}/contact-us`,
    `https://www.${domain}`,
    `https://www.${domain}/contact`,
  ];

  for (const url of pages) {
    try {
      const res = await httpGet(url);
      if (res.status === 200) {
        const emails = extractEmails(res.data);
        if (emails.length > 0) return emails[0];
      }
    } catch (e) { /* skip */ }
  }
  return null;
}

// Parse CSV
function parseCSV(filepath) {
  const content = fs.readFileSync(filepath, 'utf8');
  const lines = content.trim().split('\n');
  const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, ''));
  return { headers, rows: lines.slice(1).map(line => {
    const values = line.match(/(".*?"|[^,]+)/g) || [];
    const obj = {};
    headers.forEach((h, i) => { obj[h] = (values[i] || '').replace(/"/g, '').trim(); });
    return obj;
  })};
}

async function main() {
  const args = {};
  for (let i = 2; i < process.argv.length; i++) {
    if (process.argv[i] === '--source') args.source = process.argv[++i];
    if (process.argv[i] === '--output') args.output = process.argv[++i];
    if (process.argv[i] === '--domain') args.domain = process.argv[++i];
  }

  if (args.domain) {
    const email = await findEmail(args.domain);
    console.log(email ? `Found: ${email}` : 'No email found');
    return;
  }

  if (!args.source) {
    console.log('Usage: node enrich-leads.js --source leads.csv --output enriched.csv');
    console.log('       node enrich-leads.js --domain example.com');
    return;
  }

  const { headers, rows } = parseCSV(args.source);
  const output = args.output || args.source.replace('.csv', '-enriched.csv');
  let enriched = 0;

  console.log(`\n🔍 Enriching ${rows.length} leads...\n`);

  const outHeaders = headers.includes('email') ? headers : [...headers, 'email'];
  const lines = [outHeaders.join(',')];

  for (const row of rows) {
    if (row.email) {
      lines.push(outHeaders.map(h => row[h] || '').join(','));
      continue;
    }

    const domain = row.website || row.domain || row.url || '';
    if (!domain) {
      lines.push(outHeaders.map(h => row[h] || '').join(','));
      continue;
    }

    const cleanDomain = domain.replace(/^https?:\/\//, '').replace(/^www\./, '').replace(/\/.*/, '');
    const email = await findEmail(cleanDomain);
    if (email) {
      row.email = email;
      enriched++;
      console.log(`✅ ${cleanDomain} → ${email}`);
    } else {
      console.log(`❌ ${cleanDomain} — no email found`);
    }
    lines.push(outHeaders.map(h => row[h] || '').join(','));

    await new Promise(r => setTimeout(r, 1000)); // rate limit
  }

  fs.writeFileSync(output, lines.join('\n'));
  console.log(`\n📊 Enriched ${enriched}/${rows.length} leads → ${output}`);
}

main().catch(e => { console.error('Error:', e.message); process.exit(1); });


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)