Updated Jul 16, 2026
TL;DR: To scrape emails from a business website, fetch the contact and about pages, pull mailto: links first, then regex the visible text, respect robots.txt and rate limits, and verify every address with a syntax, MX, and SMTP check before sending. Collecting a business's published contact email is legal in most cases. Sending is where the compliance rules apply.
A surprising number of business websites just hand you the email. It's in the footer, on the contact page, behind a "Get in touch" button, sitting in a mailto: link that any browser can read. The hard part of scraping emails from a business website isn't finding them. It's doing it cleanly, at scale, without tripping anti-bot defenses or mailing a list that's half-dead on arrival.
This is the technical spoke. If you want the wider survey of where local-business emails live and the free manual methods, start with our guide on how to find local business email addresses. This post stays narrow: the actual scraping method, the selectors and regex, how to handle obfuscation and blocking, the legality, and the verification step that decides whether your scrape was worth running.
Key Takeaways
- The cleanest emails come from
mailto:links, not from regexing raw HTML. Pull those first, then fall back to a text-level regex for what's left. - Most addresses live on four pages: contact, about, team, and the footer of the homepage. Crawl those four before you crawl the whole site.
- Scraping publicly published business contact data is generally legal in the US after the hiQ v. LinkedIn line of cases, but how you send is governed separately by CAN-SPAM and GDPR.
- Never trust a scraped address. Run it through a syntax, MX, SMTP, and catch-all check before it enters a campaign, or your bounce rate does the damage for you.
Is it legal to scrape emails from a business website?
Short answer: collecting publicly published business contact data is generally lawful in the United States, but the rules that bite live on the sending side, not the collecting side. Let's separate the two, because conflating them is where people get scared off or get themselves in trouble.
On the collection question, US courts have landed on a fairly sender-friendly position. In the long-running hiQ Labs v. LinkedIn dispute, the Ninth Circuit held that scraping data from a publicly accessible website likely does not violate the Computer Fraud and Abuse Act, because there are no access gates to break when the page is open to anyone. A company's public contact page is about as open as a page gets. That doesn't make scraping a free-for-all, and a site's terms of service can still create a contract claim, but the CFAA "hacking" theory does not generally reach public data.
The one statutory landmine worth knowing is the CAN-SPAM Act's treatment of address harvesting. Federal law makes "harvesting" an aggravated violation, but the definition is narrow: it covers collecting addresses with an automated process from a website or online service that has a policy of not sharing its users' email addresses. Pulling a business's own published info@ address off its contact page isn't that. Scraping a directory or social platform that explicitly forbids harvesting, against its policy, is. And the harvesting penalty only enhances damages when you're also violating another CAN-SPAM provision, like sending with a forged header or no opt-out.
If any of your targets are in the EU or UK, GDPR changes the math. An address that identifies a person, like [email protected], is personal data even in a B2B context. A role address like [email protected] usually isn't tied to one identifiable individual, which is why those carry less risk. When you collect personal data from a third-party source rather than from the person, GDPR Article 14 requires you to tell them where you got it, at the latest at your first contact, and to have a lawful basis such as legitimate interest backed by a documented assessment. The collecting is rarely the problem. The cold email you send next is what the regulator reads.
We won't re-litigate sending compliance here. The full breakdown of what makes outreach legal versus spam lives in our guide on cold email vs. spam. Scrape clean, send compliant, and treat those as two separate disciplines.
One more norm worth respecting, even though it isn't a law: robots.txt. The Robots Exclusion Protocol was formalized as an IETF standard in RFC 9309, and it lets a site declare which paths automated clients should leave alone via Allow and Disallow rules. It's advisory, not enforced by the server, but a crawler that blows past an explicit Disallow is exactly the behavior that gets your IP blocked. Read it. Honor it.
Where business emails actually live on a website
Before you write a line of code, know what you're hunting. Email addresses cluster in predictable places, and crawling the whole site to find them is wasted effort. Hit the high-probability pages first.
Location | What you'll find | Reliability |
|---|---|---|
| A clean, machine-readable address with no parsing guesswork | Highest |
Contact / "Get in touch" page | Department and general inboxes, sometimes a form only | High |
About / Team / Staff pages | Named individual addresses (the higher-value targets) | Medium-high |
Homepage footer | A general | Medium |
Privacy policy / Terms page | A compliance or legal contact, often | Medium |
Image or JavaScript-rendered text | Address shown as an image or assembled in script | Low |
The pattern is consistent across small and mid-size business sites: the contact page and the footer carry the general inbox, and the team page carries the named people. If you only have time to crawl a handful of URLs per domain, crawl /contact, /about, /team, and the homepage. That's where you'll find email on a company website four times out of five.
How to scrape emails from a business website: the step-by-step method
Here's the core website email scraping method, broken into the steps a working scraper actually runs. The principle underneath all of it: extract structured sources first, fall back to unstructured ones, and dedupe hard.
Step 1: Fetch the page HTML
Request the page like a normal browser would. Set a real User-Agent header, accept a reasonable timeout, and don't hammer the server. A single GET on the contact page is usually enough for static sites.
1import re, requests2from bs4 import BeautifulSoup34HEADERS = {"User-Agent": "Mozilla/5.0 (compatible; research-bot/1.0)"}56def fetch(url):7 resp = requests.get(url, headers=HEADERS, timeout=10)8 resp.raise_for_status()9 return resp.text
If the page returns a 200 with real markup, you're set. If it returns a near-empty shell, the content is rendered by JavaScript and you'll need the browser approach covered further down.
Step 2: Extract mailto: links first
This is the step most tutorials skip, and it's the most valuable one. A mailto: anchor gives you the address pre-parsed, with zero ambiguity. Select every a tag whose href starts with mailto:, strip the prefix, and drop any ?subject= query string.
1def emails_from_mailto(soup):2 found = set()3 for a in soup.select('a[href^="mailto:"]'):4 addr = a["href"][len("mailto:"):].split("?")[0]5 if addr:6 found.add(addr.strip().lower())7 return found
When a site uses mailto: links, this alone often gets you everything you need, and it never produces the false positives a raw regex will.
Step 3: Regex the visible text as a fallback
For addresses printed as plain text with no link, fall back to a regular expression. Run it against the rendered text, not the raw HTML, so you're not matching addresses buried in tracking scripts or analytics tags. A practical pattern:
1EMAIL_RE = re.compile(r"[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}")23def emails_from_text(soup):4 text = soup.get_text(" ")5 return {m.strip().lower() for m in EMAIL_RE.findall(text)}
A note on regex perfectionism: the fully RFC 5322 compliant email pattern is famously enormous and almost no one uses it in production. The standard allows a local-part up to 64 octets and a domain up to 255 octets, and the syntax rules in RFC 5322 permit edge cases you'll basically never see on a business contact page. The pragmatic pattern above catches the real-world 99% and leaves the malformed matches for the verification step to reject.
Step 4: Crawl the right pages, not the whole site
A single homepage GET misses the team page. Build a tiny two-level crawler: fetch the homepage, find internal links whose path looks like contact, about, team, staff, or "people," fetch those, and run steps 2 and 3 on each. Cap it. You want four or five pages per domain, not a recursive crawl of the entire site.
1TARGET_PATHS = ("contact", "about", "team", "staff", "people")23def candidate_links(soup, base):4 links = set()5 for a in soup.select("a[href]"):6 href = a["href"].lower()7 if any(p in href for p in TARGET_PATHS):8 links.add(href)9 return links # resolve to absolute URLs against base before fetching
Step 5: Dedupe and normalize
Lowercase everything, strip whitespace, and collapse the set. The same info@ address often appears in the footer of every page, so a set rather than a list saves you from a dozen duplicates per domain. Drop obvious junk: addresses ending in image extensions, @example.com, @sentry.io, @2x.png, and the placeholder [email protected] that contact forms leave in the markup.
Handling obfuscation and anti-bot defenses
Not every site makes it easy, and the ones that don't usually have a reason. Here's how to read what you're up against and when to back off.
Text obfuscation. Some sites write jane [at] acme [dot] com or insert zero-width characters to defeat naive scrapers. You can normalize the common patterns ([at] to @, [dot] to .) before running your regex, but treat each normalization as a deliberate signal that the owner doesn't want automated collection. Use judgment.
HTML entity encoding. Addresses written as ja... are just decimal-encoded characters. Parse them with an HTML parser, which decodes entities for you, instead of regexing the raw source. This is one more reason to run your regex on soup.get_text() rather than the raw response body.
JavaScript-rendered content. If the contact page loads its address through a script, a plain requests.get sees an empty container. You'll need a headless browser like Playwright or Puppeteer to render the page, then read the resulting DOM. It's slower and heavier, so reserve it for domains where the static fetch came back empty, not as your default.
Rate limiting and IP blocks. Send too many requests too fast and the server returns 429 (Too Many Requests) or quietly starts feeding you 403s. The fix is restraint: add a delay between requests, randomize it a little, and crawl one domain at a time rather than hammering one host with parallel workers. If a site is behind a bot-management service and actively challenging you, that's your cue to stop. Brute-forcing past an active block is the behavior CAN-SPAM's harvesting clause was written to discourage, and it's a fast way to get your sending infrastructure flagged.
The honest framing: extracting a published contact address from a cooperative site is fair game. Defeating active, deliberate defenses on a site that's telling you "no" is a different activity with different risk. Know which one you're doing.
Verify every scraped address before it touches a campaign
This is the step that separates a useful list from a domain-burning one. A scraped address is a candidate, not a confirmed contact. The page might be three years stale, the inbox might be abandoned, the regex might have grabbed a typo. Send to that list cold and your bounce rate climbs, which is one of the fastest ways to wreck sender reputation. Google's own guidance is to keep error and spam-complaint signals low, and a high bounce rate from unverified scrapes works directly against you.
Real verification runs four checks in sequence, and each one can reject an address before the next runs:
Check | What it confirms | Rejects |
|---|---|---|
Syntax (RFC 5322) | The address is well-formed | Typos, truncated matches, junk |
MX record (DNS) | The domain can receive mail at all | Dead or misconfigured domains |
SMTP handshake | The specific mailbox accepts mail | Nonexistent mailboxes |
Catch-all detection | Whether the domain accepts any address | Flags "risky / unknown" results |
The SMTP step is the clever part. A verifier opens a connection to the domain's mail server, runs the HELO, MAIL FROM, and RCPT TO exchange against the target address, and reads the reply code without ever sending a message. A 250 reply means the mailbox is willing to accept mail. A 550 means it doesn't exist.
Catch-all domains are the asterisk on all of this. A catch-all server accepts mail for every address at the domain, real or invented, so the SMTP check returns 250 for [email protected] and for [email protected] alike. That's why a good verifier returns four states, not two: valid, invalid, risky (catch-all, can't be confirmed), and unknown. Treat "risky" addresses as a separate, lower-priority segment, not as confirmed contacts.
One more filter that's less about deliverability and more about reply rate: flag the role accounts. Addresses like info@, sales@, support@, and admin@ are shared inboxes, not people, and they convert poorly on cold outreach. Keep them, but segment them apart from the named individuals you pulled off the team page. For the full pre-send checklist, our email deliverability audit walks the whole list-hygiene sequence.
When to skip the DIY scraper
Writing your own scraper is a great way to understand the mechanics, and for a handful of target domains it's genuinely the fastest path. At scale, the math changes. Maintaining selectors as sites redesign, rotating IPs to avoid blocks, rendering JavaScript pages, and running a real verification pipeline is a lot of plumbing for a list of leads.
That's the gap MailBeast's Lead Finder closes. It discovers businesses from public business directories and local business listings, crawls each company's own site for published contact addresses, and runs the verification flow above, so what lands in your list is already syntax-checked, MX-checked, and SMTP-confirmed. You skip the brittle scraper and go straight to a clean, segmentable list. If your goal is a campaign rather than a coding project, that's the shorter route. And whichever path you take, the principles for assembling the list itself are the same ones in our guide on building a high-quality B2B lead list.
Whether you scrape by hand or automate it, the discipline doesn't change: extract from the cleanest source first, respect the signals a site sends you, and verify before you send. A scraped list is only an asset after it survives that last step. Before that, it's a liability with your domain's name on it.
Common questions
Is scraping emails from a website the same as buying a list?
No, and the difference matters. Scraping a business's own published contact address means you collected a current, first-party data point that the company chose to make public. A purchased list is data of unknown age, origin, and consent, often resold many times and riddled with spam traps. Scraped-then-verified beats bought almost every time. For the wider comparison of sources, see building a high-quality B2B lead list.
What's the best way to extract an email from a website without code?
For a single site, you don't need a scraper at all. Open the contact and about pages, then use your browser's "view page source" or a find-in-page search for the @ symbol and the word mailto. The address is almost always right there. Code only pays off once you're doing this across dozens or hundreds of domains. The manual methods are covered in how to find local business email addresses.
Why did my scraper return zero emails on a site that clearly has one?
Two usual suspects. Either the address is rendered by JavaScript, so a plain HTTP fetch sees an empty container and you need a headless browser to read the live DOM, or the address is shown as an image rather than text, which a regex can't read. A contact form with no visible address is the third case, and there's simply nothing to extract there.
How many scraped emails will bounce if I don't verify?
It depends entirely on list age and source, but stale, unverified scrapes routinely bounce in the double digits, and that's exactly the outcome verification exists to prevent. Mailbox providers read bounces as a reputation signal, so an unverified scrape can hurt your sending more than it helps. Run the four-check flow first and you keep bounces in the low single digits.
Does scraping put my own domain at risk?
The scraping itself doesn't touch your sending reputation. What you do with the results does. Mail an unverified, scraped list cold and you risk bounces, spam traps, and complaints, all of which degrade the domain you send from. Verify, segment out role accounts, warm your sending domain, and send compliant outreach, and the scrape stays an asset.
Sources
- Wikipedia, hiQ Labs v. LinkedIn
- Cornell Legal Information Institute, CAN-SPAM Act: Problematic Spamming Techniques
- Federal Trade Commission, CAN-SPAM Act: A Compliance Guide for Business
- GDPR.eu, Article 14: Information to be provided where personal data have not been obtained from the data subject
- RFC Editor, RFC 9309: Robots Exclusion Protocol
- IETF, RFC 5321: Simple Mail Transfer Protocol
- IETF, RFC 5322: Internet Message Format
- Google, Email sender guidelines



