Lead Generation Suite NEW in v1.2

Collect, verify, score, and activate outbound prospects — all inside KodMail. Prospects live in a customer-scoped leads table with dedup on (customer_id, email), so every collector is idempotent and every enrichment run is quota-metered.

Where it lives

Customer sidebar → Growth group. Three entries:

Plan gating

Every collector + enrichment path is gated by a per-plan quota key. Ships enabled on Starter+; Free tier gets a taste (100 stored leads, 20 verifications/month) so buyers can trial before upgrading.

Quota keyFreeStarterProEnterprise
max_leads_stored1002,50025,000
max_lead_enrichments_per_month205005,000
max_lead_scoring_calls_per_month205005,000
max_lead_sources_per_month525100
max_hosted_forms0325
allow_url_scrape
allow_hosted_form

Collectors — how prospects get in

1. CSV bulk import

Leads index → Import CSV. Auto-detects header aliases — you don't have to name columns a specific way:

Upserts on (customer_id, email). Missing company_domain is auto-filled from the email suffix. Sync cap 5,000 rows per upload — split larger files into batches.

2. Hosted lead form

Lead sources → Add source → type = Hosted form. On creation, a 24-character token is generated and a public URL becomes available: https://your-site.com/p/lead-form/<token>. Copy it from the source row and embed anywhere — landing page, link in bio, email signature.

Anti-spam: hidden honeypot field + per-IP rate limit (10 submissions/hour per source). Bots that populate the honeypot get a fake success screen and are silently dropped.

Default fields: email, first name, last name, company, title, phone. Edit the config later to add or remove fields (buyer-facing editor lands in a follow-up patch; for now the form JSON is admin-editable on the source row).

3. URL scrape

Lead sources → Add source → type = URL scrape → paste the page URL. Auto-queues; scraper fetches the page (10-sec timeout, custom UA), extracts emails from mailto: anchors + body text, dedupes, and filters obvious noise (example.com, yourdomain.com, image filenames masquerading as emails).

Company domain per lead is derived from the email suffix; falls back to the page host if the email is on a generic provider.

Verification pipeline

Every lead can be verified before you spend send credits on it. Layered checks, cheapest first:

  1. Syntax — regex + filter_var. Bad syntax → invalid.
  2. Disposable-domain blocklist — mailinator, tempmail, guerrillamail, etc. → invalid.
  3. Role account detectioninfo@, sales@, noreply@ → flag is_role_account.
  4. MX lookup — no MX + no A record → invalid (no_mx).
  5. SMTP handshake — connect to lowest-priority MX, HELO/MAIL FROM/RCPT TO probe (no DATA). Also probes a random address on the same domain to detect catch-all.

Verdict: valid (accepted, not catch-all) · risky (catch-all or role account on unreachable host) · invalid (rejected or bounced pre-DATA) · unknown (network blocked port 25 — most shared hosts).

Trigger: Leads index → Verify unknowns (batch, queues one VerifyLeadJob per lead, respects remaining monthly quota). Or open a lead → Re-verify button on the verification card.

ICP scoring

Describe your ideal customer in plain English on an ICP profile. Copilot's AI provider scores every lead 0–100 against it, with a written reasoning paragraph.

Example profile:

B2B SaaS founder or head of growth at a US company with 10–50 employees,
currently spending on paid acquisition. Bonus points for HubSpot,
Segment, or Mixpanel in their stack.

Score buckets (dropdown filter on Leads index):

Trigger: Leads index → Score unscored (batch). Uses the default ICP profile unless you pass rule_id via the Copilot tool.

Cost is logged per call in lead_enrichments.cost_estimate so admins can bill accurately.

Activation — push to a list

Bulk-select prospects on the Leads index (checkboxes), then click Push to list. Modal:

Selected leads become ListSubscriber rows on the chosen list. Idempotent — already-subscribed emails are skipped. Consent metadata (consent_method = lead_push) and source (import) are recorded on every subscriber. Lead status flips to opted_in.

Double-opt-in lists insert subscribers as unconfirmed; single-opt-in lists insert as confirmed with confirmed_at = now().

Copilot tools

Four new tools auto-register in Copilot. Ask in plain English:

Data model — for developers

lead_sources        → origin metadata (customer_id, name, type, config, status, leads_added)
leads               → prospect rows (customer_id, email, firmographics, verification flags, icp_score, custom_fields)
lead_enrichments    → per-call audit trail for quota + billing (provider, status, result, duration_ms, cost_estimate)
lead_score_rules    → ICP profiles (customer_id, name, icp_description, weights, is_default)

Tenant boundary is enforced by Lead::scopeVisibleTo($customer) on the model + controller-level authorization guards. Every controller uses the scope, never a raw WHERE customer_id.

Common questions

My verification returns "unknown" for everything

Your host blocks outbound port 25 (very common on shared hosting, DigitalOcean droplets, and most PaaS providers). This is treated as risky not invalid so you don't false-negative real leads. For deep verification, wire an external provider (Snov, Hunter) in v1.2.1.

Can I export leads?

Yes — REST API endpoint GET /api/v1/leads ships in v1.2.1. In v1.2, click any lead → detail page, or use the Copilot find_leads tool which returns up to 20 rows as JSON.

Do scraped leads count against my max_leads_stored quota?

Yes — every collector honors it. Over-quota new inserts are silently skipped; already-known emails are still updated. The response summary shows skipped_quota so you know why.