Scraper Workers

Email finder

The Ultron Email Finder takes a name plus a company (or a LinkedIn URL) and returns the most likely email address with a verification confidence. It resolves the company domain, generates pattern candidates, then asks a sibling Cloudflare worker to verify deliverability over SMTP. A separate track resolves personal addresses (Gmail, Outlook) when the corporate domain is unknown or yields nothing.

Updated today

Overview

At a glance
Worker
ultron-enrich-email
Runtime
Python 3 with Apify SDK, httpx, beautifulsoup4, browserforge
Source
apify-actors/email-finder/
SMTP verifier
Cloudflare worker ultron-smtp-verify (CF allows outbound 25)
Tracks
company domain + pattern, personal address search
Output verdict
valid, accept_all, role, invalid, unknown

The Email Finder is the scraper that turns prospects into deliverable contacts. It runs after the Maps or LinkedIn scrapers produce names but no emails. The confidence scoring is conservative: a result with status valid and confidence above 0.85 is treated as deliverable by the outreach skills; anything below skips, falls back to a different pattern, or routes the prospect to the LinkedIn DM track.

Input

apify-actors/email-finder/.actor/actor.jsonjson
1{
2 "leads": [
3 {
4 "first_name": "Maria",
5 "last_name": "Schultz",
6 "company_name": "Acme",
7 "company_url": "https://acme.com", // optional, helps domain resolution
8 "linkedin_url": "https://linkedin.com/in/...", // optional, enables personal track
9 "title": "Head of Sales" // optional, biases role-account scoring
10 }
11 ],
12 "smtp_verify_url": "https://ultron-smtp-verify.<acct>.workers.dev",
13 "smtp_verify_secret": "<shared secret>",
14 "patterns": ["first.last", "f.last", "firstlast", "first"],
15 "track_personal": true
16}

Two tracks

Corporate pattern path and personal address path. Most leads go through both.

TrackInputsOutput
Corporate patternname + company + (url | resolved domain)Highest-confidence company email per pattern
Personal addressname + linkedin handle + plausible personal providersGmail / Outlook address when the lead leaves one in their public LinkedIn profile or commits to one in OSS

Domain resolution

The same company can have several public domains. The resolver cascades.

apify-actors/email-finder/src/domain_resolver.pytext
1# Cascade order:
2# 1. company_url field if present and resolvable
3# 2. LinkedIn company page parsed for the canonical website
4# 3. Brave search "<company_name> official site" first organic
5# 4. Whois lookup against the apparent root domain
6# 5. MX record check on the resolved domain - drops any candidate
7# that does not have a working MX (parked domains, expired sites)

The resolver does not trust any one source. A LinkedIn page can point at a stale site; an old Brave index can return the new owner's site for an acquired brand; whois can be hidden. The cascade picks the candidate that survives an MX check, which is the cheapest evidence that mail can actually be delivered.

SMTP verification

Apify blocks outbound port 25, so verification runs on a sibling Cloudflare worker.

apify-actors/email-finder/src/email_finder.pypython
1async def smtp_verify_batch(
2 emails: list[str],
3 cf_url: str,
4 cf_secret: str,
5 client: httpx.AsyncClient,
6) -> dict[str, dict]:
7 """Batch verify emails via CF ultron-smtp-verify worker."""
8 resp = await client.post(
9 f"{cf_url}/verify",
10 json={"emails": emails},
11 headers={"x-shim-secret": cf_secret},
12 timeout=30.0,
13 )
14 return {row["email"]: row for row in resp.json()["results"]}
Note
Cloudflare allows outbound SMTP from workers, which Apify does not. The verifier worker takes a batch of addresses, runs the RCPT TO probe against each domain's MX, and returns a status per address (valid, accept_all, role, invalid, unknown). The shared secret is the gate.

Personal email track

When the corporate domain yields nothing.

personal_track.py tries a small set of public sources for a personal address: the lead's GitHub commit history (parses author email from the most recent public commits), their LinkedIn contact info section if visible, and a handful of personal-domain heuristics. Results are verified the same way as corporate addresses. Confidence on personal addresses caps at 0.75; corporate matches always outrank them when both exist.

Output shape

One row per input lead with the best match and the runner-up.

dataset-row.jsonjson
1{
2 "lead_id": "<uuid from input>",
3 "best_email": "maria.schultz@acme.com",
4 "best_status": "valid",
5 "best_confidence": 0.91,
6 "best_method": "pattern:first.last + smtp",
7 "alternates": [
8 { "email": "m.schultz@acme.com", "status": "accept_all", "confidence": 0.55 },
9 { "email": "maria@acme.com", "status": "role", "confidence": 0.35 }
10 ],
11 "company_domain": "acme.com",
12 "personal_email": null,
13 "scraped_at": "2026-05-11T18:42:00Z"
14}

How to trigger it

The skill emits a capability call. The platform translates it into an Apify run.

invocationtext
1POST https://api.apify.com/v2/acts/ultron-enrich-email/runs?token=<APIFY_TOKEN>
2Content-Type: application/json
3
4{
5 "leads": [...],
6 "smtp_verify_url": "https://ultron-smtp-verify.<acct>.workers.dev",
7 "smtp_verify_secret": "<shared>"
8}

File map

apify-actors/email-finder
Dockerfile
requirements.txt
src
__main__.pyApify entry
email_finder.pytrack orchestration
company_filters.pyconfident-company heuristics
domain_resolver.pythe 5-step cascade
personal_track.pyGmail / Outlook resolution
gmail_pool.pypool of free Gmail addresses for outbound spoof checks
vm_client.pyclient for the SMTP verify worker
cf-workers
ultron-smtp-verify/...Cloudflare side of SMTP verification