Open source · Python
MailGrab: an open-source email extractor in Python
A crawler for checking which email addresses your own websites publish, and on which pages. It checks robots.txt by default and also runs as an MCP server.
- Built by
- Nakshatra Ranjan Saha
- Language
- Python
- Licence
- MIT
- Version
- v2.0.0, released on 3 September 2026
- Runs on
- Windows, Linux and macOS
- Use it from
- The command line or an MCP client
- Automated tests
- 43 in v2.0.0
- Started
- January 2022
Overview
MailGrab is a free, open-source Python tool that crawls websites and lists the email addresses on their pages, with the page where each one was found. Our founder, Nakshatra Ranjan Saha, CEO of N&D Co., wrote and maintains it on GitHub. Use it to check which addresses your own websites expose, or to research sites you have permission to crawl.
Version 2.0.0, released on 3 September 2026, is the first tagged release of a project that goes back to January 2022. It turned a single-threaded script into a concurrent crawler that checks robots.txt by default, and added CSV and JSON output, a Model Context Protocol (MCP) server that lets AI coding agents run a crawl as a tool call, and 43 automated tests.
MailGrab is released under the MIT licence, so anyone may use, change and share the code, commercially too, as long as the copyright and licence notice go with it. The licence credits OCEAN OF ANYTHING, the name our founder publishes under on GitHub from the account OCEANOFANYTHING, and the README credits "OCEAN OF ANYTHING (Nakshatra Ranjan Saha)". Other software shares the name; this page is about the crawler in the MailGrab repository.
Links, versions and releases
| Item | Details |
|---|---|
| Website | MailGrab's own website |
| Source code | The MailGrab repository on GitHub |
| Documentation | The MailGrab documentation |
| Current version | v2.0.0, released on 3 September 2026: release notes for v2.0.0 |
| All releases | Every MailGrab release on GitHub |
| Commit history | Every MailGrab commit on GitHub, from the first on 3 January 2022 |
| Licence | The MIT licence |
What does MailGrab do?
MailGrab starts from a URL or a list of them, follows the links it finds, collects the email addresses on each page and saves them with the page each one came from. Its GitHub description calls it an email harvester.
Finding pages
- Crawls from one start URL or from each line of a seed file, 10 pages at a time by default, with a 10-second timeout on each request.
- Follows links to other websites too, unless --same-domain keeps it on the start URL's host.
- Can stop a set number of links from the start and add the pages listed in a site's sitemaps.
- Skips links to images, PDFs, archives, scripts, stylesheets, audio, video, fonts and XML files, and tel:, javascript: and in-page # links.
Finding addresses
- Searches the source of each page for text shaped like an email address, and reads mailto: links.
- Decodes the data-cfemail form of Cloudflare's email address obfuscation and addresses written as name [at] domain [dot] com.
- Lists the LinkedIn, X, Twitter, Facebook, Instagram and contact page links it comes across, and crawls them like any other link.
Cleaning and saving
- Lowercases addresses so each is listed once, and drops 10 known placeholder addresses, such as email@example.com, along with every address at example.com, example.org, example.net and wixpress.com.
- On request, drops addresses whose domain has no mail server (MX) record.
- Saves text, CSV and JSON files, can build on an earlier run and keeps what it found if you stop it with Ctrl+C.
How does a MailGrab crawl work?
A MailGrab crawl fetches pages in rounds of up to 10 at a time (the --concurrency default) and stops once it has requested the number of pages set by --depth or runs out of links.
- MailGrab takes the start URL from --url, the seed file or a prompt, adding http:// when the URL has no scheme.
- It queues that URL and, with --use-sitemap, the pages listed in the site's sitemaps.
- Each round takes up to --concurrency URLs from the queue and drops the ones the site's robots.txt disallows.
- Before each request it waits until the longer of --delay and the site's Crawl-delay has passed since its last request to that host, then fetches the round in parallel threads.
- On each page it collects and lowercases the email addresses, drops placeholders and queues the new links that --max-hops and --same-domain allow.
- At the end it merges earlier results (with --append or --resume), checks MX records (with --verify-mx) and writes its files.
How does MailGrab read robots.txt?
MailGrab fetches each site's robots.txt once for its rules (with --use-sitemap it requests the file again for each start URL, to read its Sitemap: lines), reads it with Python's built-in parser and skips the pages that parser finds disallowed for all crawlers. On Python 3.12 that parser matches plain path prefixes only, with no * or $ wildcards, and applies the first matching rule rather than the longest, so MailGrab can still fetch a page that a wildcard rule disallows. The parser matches robots.txt groups against the User-Agent up to its first slash, which for MailGrab's default is Mozilla: a group written for MailGrab by name does not apply, nor does its Crawl-delay. A Crawl-delay counts only when it is a whole number of seconds; a value such as 0.3 is ignored.
A robots.txt that is missing, cannot be reached or answers with an error counts as allowing everything, as the wiki's crawl correctness page says. RFC 9309, the robots.txt standard, asks crawlers to assume everything is disallowed when a server or network error makes the file unreachable.
How to use MailGrab responsibly
An email address that relates to an identifiable person is personal data, and collecting and using it is regulated. Use MailGrab on sites you own or have permission to crawl, to find out what they expose, and not to build mailing lists.
Which laws apply?
That depends on where you are, whose addresses they are and what you do with them. Three examples:
- India's Digital Personal Data Protection Act, 2023 counts collecting digital personal data as processing it, and allows processing only for a lawful purpose, with the person's consent or for certain legitimate uses. Its section 3 sets out what the Act does not apply to. Most of the Digital Personal Data Protection Rules, 2025, published on 13 November 2025, come into force on 13 May 2027.
- The EU's General Data Protection Regulation requires a lawful basis for collecting personal data. Anyone who collects it from somewhere other than the person, public sources included, must normally tell them within a month, and people can object to direct marketing at any time. For marketing email, Article 13 of the EU's ePrivacy Directive generally requires the recipient's prior consent, and each member state applies it through its own law.
- The US CAN-SPAM Act sets rules for every commercial email, business-to-business email included, such as honouring opt-out requests promptly. The FTC's guide to the Act lists harvesting email addresses among the practices that carry criminal penalties.
What MailGrab does by default
MailGrab checks robots.txt before each page unless you turn that off on purpose with --ignore-robots or its matching setting. It sets no rate limit of its own: --delay defaults to 0 while 10 pages load at once. Add a delay for small sites. Its default User-Agent names MailGrab and links the repository, so site owners can see what visited.
On the command line, MailGrab follows links to other websites unless you pass --same-domain; add that flag when you audit your own site. The MCP tool stays on the starting site and stops at 30 pages by default, but the agent chooses each call's options: it can raise the depth, turn same_domain off or set ignore_robots, which the tool's description asks it to leave off unless it has a clear reason. The server's instructions also tell agents to use the tool whenever they need contact emails for a company or site. Check which sites your agent asks it to crawl.
RFC 9309 says its rules are not a form of access authorisation: robots.txt not blocking a page does not by itself give you permission to crawl it. An address published on a page also does not mean the person agreed to be contacted. For site owners, the standard adds that robots.txt is not a substitute for security measures. A crawler that ignores the file can still read the addresses on your public pages. Our technical SEO checklist explains what robots.txt does for search engines.
Uses that fit
- Checking your own websites, or a client's with permission, for addresses left open to scrapers, including ones written with [at] and [dot] and the data-cfemail form of Cloudflare's obfuscation, which MailGrab decodes. It runs no JavaScript: a run that finds nothing does not prove a site exposes no addresses.
- Research on sites you have permission to crawl, keeping only the personal data you have a lawful basis to keep.
MailGrab sends no email. From the command line, its results stay in files on your machine until you delete them; through MCP, they go to the agent that asked. Treat them as personal data wherever they end up.
This section describes how MailGrab works and names some of the laws involved. It is not legal advice; ask a lawyer about your own use.
How to install and run MailGrab
MailGrab runs from a copy of its GitHub repository on Windows, Linux or macOS; it is not a PyPI package. You need git, an internet connection, Python 3.12 or newer from python.org with pip and, on Linux and macOS, curl, which MailGrab uses to check the connection at startup.
The pinned requirements.txt sets that minimum. It holds lxml at 5.4.0, which has ready-built packages only up to Python 3.13; on a newer Python, pip has to build lxml from source. The file also installs more than the crawler uses, which makes a virtual environment worth having. The repository also has an installer script in its install folder; the commands below use pip directly.
git clone https://github.com/OCEANOFANYTHING/MailGrab
cd MailGrab
pip install -r requirements.txt
python MailGrab.pyStarted this way, MailGrab asks for the URL to scan and for the depth, which is the most pages it fetches in total (up to 500). On Windows, windows.bat does the same and keeps the window open at the end. Run MailGrab with --help to list every option.
Scripted and batch runs
Given --url and --depth, or --depth and a seed file, MailGrab never prompts, so it can run from a script, a cron job or CI. Check the spelling of each option: MailGrab ignores one it does not recognise, without a warning. The wiki's fuller example stays on one site, waits half a second between requests, names its owner in the User-Agent (put your own bot name and URL there) and checks MX records:
python MailGrab.py \
--url https://example.com \
--depth 100 \
--same-domain \
--delay 0.5 \
--user-agent "MyCompanyBot/1.0 (+https://mycompany.com/bot)" \
--verify-mxRun it again with --resume to skip the pages it visited before, apart from the start page. To crawl several sites, list one URL per line in _inputUrls.txt, or in a file you name with --input, and leave out --url: MailGrab crawls them in turn, up to 200 pages each, skipping links it already crawled for an earlier site.
At startup MailGrab checks that it can reach oceanofanything.github.io, our founder's GitHub Pages site, by ping on Windows or curl on Linux and macOS, then with two HTTPS requests. If the ping or curl check fails, it prints "Internet Not Connected" and exits with code 0 without writing any files. If an HTTPS request fails, it stops with a Python error. A script should check that _results.json is new rather than trust the exit code. No option skips these checks. They do not go through --proxy, and neither do the DNS lookups of --verify-mx. The wiki's FAQ and troubleshooting page covers this and other common problems.
MailGrab command-line options
MailGrab's command-line options set where a crawl starts, how many pages it fetches, how fast and how far it goes, whether it checks robots.txt and what it saves. None is required: with neither --url nor --depth, MailGrab asks for the start URL and the depth, or only for the depth when the seed file lists URLs. With either, it never asks: --url without --depth stops with an error, and so does --depth without --url unless the seed file lists URLs. The wiki's command-line reference explains each option in more detail.
| Option | Default | What it does |
|---|---|---|
| --url | Asks | The start URL; it wins over a seed file |
| --depth | Asks | Pages to fetch in total: 1 to 500, or 1 to 200 per start URL in batch mode |
| --input | _inputUrls.txt | The seed file for batch mode, one URL per line |
| --concurrency | 10 | Pages fetched at once in each round |
| --delay | 0 seconds | Minimum time between requests to one host; a longer Crawl-delay in whole seconds wins |
| --timeout | 10 seconds | Time limit for each request |
| --same-domain | Off | Follow links on the start URL's exact host only |
| --max-hops | No limit | Maximum number of links away from the start; 0 means the start page only |
| --use-sitemap | Off | Also queue the pages in the site's sitemaps |
| --ignore-robots | Off | Skip the robots.txt checks and its Crawl-delay |
| --verify-mx | Off | Drop addresses whose domain has no MX record |
| --append | Off | Merge the results with the previous run's _results.json |
| --resume | Off | As --append, and skip pages already visited, except the start page |
| --quiet | Off | One JSON summary line instead of the console output |
| --user-agent | Names MailGrab | The User-Agent header on every crawl request |
| --proxy | None | An http, https, socks4, socks4a, socks5 or socks5h proxy; a malformed one stops the run at startup |
| --config | None | A JSON file that fills in MAILGRAB_ variables you have not set |
Most options can also be set with an environment variable named after them, such as MAILGRAB_SAME_DOMAIN for --same-domain; --concurrency uses MAILGRAB_MAX_WORKERS, and --url, --depth, --input and --config have none. A switch is on only when its variable is exactly 1, and a flag beats its variable, which beats the config file. A flag can only turn a switch on: a switch set to 1 in the environment or a config file stays on until you remove it there.
A config file is a flat JSON object keyed by those variable names in lower case without MAILGRAB_, so --concurrency is max_workers there. Write a switch as "1", because a JSON true counts as off. Pass the file with --config myconfig.json; a missing or broken file stops MailGrab before it crawls anything.
{
"delay": "0.5",
"same_domain": "1",
"user_agent": "MyCompanyBot/1.0"
}What files does MailGrab save?
Each run writes five files into the current directory: _emails.txt, _scrappedUrls.txt, _emails.csv, _results.json and the debug log, _MailGrabLog.txt. The four result files replace the last run's unless you pass --append or --resume.
| File | What it holds |
|---|---|
| _emails.txt | A header with the date and time of the scan, then the addresses, sorted, one per line |
| _scrappedUrls.txt | The same header, then every URL the crawl took from its queue, sorted: pages fetched, pages that failed and pages robots.txt blocked (with --append or --resume, earlier runs' too) |
| _emails.csv | Columns email and found_on_url, one row for each address and page |
| _results.json | emails, scrappedUrls, sources (the pages each address was on) and socialLinks; --append and --resume read it back |
| _MailGrabLog.txt | The debug log, replaced on every run, which records why a page was skipped |
These samples come from the wiki. A real run would drop their example.com addresses as placeholders.
email,found_on_url
alice@example.com,https://example.com/
alice@example.com,https://example.com/team
bob@example.com,https://example.com/contact{
"emails": ["alice@example.com", "bob@example.com"],
"scrappedUrls": ["https://example.com/", "https://example.com/contact"],
"sources": {
"alice@example.com": ["https://example.com/", "https://example.com/team"],
"bob@example.com": ["https://example.com/contact"]
},
"socialLinks": ["https://www.linkedin.com/company/example", "https://example.com/contact-us"]
}With --quiet, MailGrab prints one JSON summary line at the end instead of its console output, for another program to read:
{"emailCount": 12, "urlCount": 48, "outputFiles": ["_emails.txt", "_scrappedUrls.txt", "_emails.csv", "_results.json"]}A malformed --proxy, an out-of-range --depth or a failed save still prints its error to standard error under --quiet and exits with code 1.
Using MailGrab as an MCP server for AI agents
The repository's MCP server, mailgrab_mcp_server.py, gives AI coding agents one tool, crawl_website, which runs a MailGrab crawl and returns the results as structured data. It talks to its client over standard input and output (stdio), so the client starts it as a local process.
Each call runs MailGrab.py with --quiet as a separate process in a new temporary folder, reads _results.json back and deletes the folder. The repository gives two reasons: stdio reserves standard output for protocol messages, which the crawler's console output would corrupt, and two calls sharing a folder would overwrite each other's files. Every call also runs the startup check, so the server needs internet access.
crawl_website(
url: str, # required
depth: int = 30,
same_domain: bool = True,
max_hops: int | None = None,
use_sitemap: bool = False,
ignore_robots: bool = False,
verify_mx: bool = False,
delay: float = 0.0,
timeout: float = 10.0,
concurrency: int = 10,
user_agent: str | None = None,
proxy: str | None = None,
) -> {
emails: [str],
email_count: int,
scrapped_urls: [str],
url_count: int,
sources: {email: [url]},
social_links: [str],
}Each parameter maps to the matching command-line option, and two defaults differ for agents: same_domain is on, unlike on the command line, and depth is 30. Finding no addresses is a normal result, and so is a site that cannot be reached: the tool returns no addresses, but scrapped_urls still lists the start URL, so an empty email list does not show whether the site answered. A malformed proxy, a depth outside 1 to 500, a failed save, a run that leaves no results file (as when the startup check fails) or a crawl that runs out of time returns a tool error with the reason. The time limit depends on depth, concurrency and timeout: 75 seconds at the defaults and 1,800 seconds at most. The tool leaves out --append, --resume and --config, because each call stands alone.
Connecting a client
To start the server by hand, run this in the repository folder:
python mailgrab_mcp_server.pyAn MCP client runs the same command itself: point it at the absolute path of mailgrab_mcp_server.py, with a python command that has the requirements installed. For Claude Code, a .mcp.json file at the project root looks like this, and Claude Desktop and Cursor use the same shape in their own files:
{
"mcpServers": {
"mailgrab": {
"command": "python",
"args": ["/absolute/path/to/mailgrab_mcp_server.py"]
}
}
}GitHub Copilot in VS Code uses a "servers" key instead, GitHub Copilot CLI has its own file with a tools list, and Codex CLI reads TOML. The wiki's MCP server setup page covers all six clients; the release notes say its steps were checked against each client's documentation. Once it is connected, ask your agent which email addresses your own site publishes.
How MailGrab is built and tested
MailGrab is two Python scripts: MailGrab.py, the crawler, and mailgrab_mcp_server.py, the MCP server that runs it. Interactive and batch runs share one crawl loop, one page fetcher and one save function, as the wiki's architecture page explains.
| Job | Library |
|---|---|
| Fetching pages | requests, with urllib3 retrying server errors up to three times |
| Reading HTML | Beautiful Soup with the lxml parser |
| robots.txt and sitemaps | Python's urllib.robotparser and xml.etree |
| Parallel fetching | A thread pool from concurrent.futures |
| MX lookups | dnspython |
| Console and run statistics | Rich, colorama, cursor and psutil |
| MCP server | FastMCP, from the official MCP Python SDK (1.x) |
Worker threads only fetch and parse pages, and the main thread merges every result, so the crawler needs no locks.
Tests, scanning and updates
Version 2.0.0 has 43 tests, written with Python's unittest and run by hand, not in CI. The 35 in test_mailgrab.py run the real script against a local mock web server and cover crawling, decoding, robots.txt, output files and options. Of the eight in test_mailgrab_mcp.py, three check the time limit formula and five drive the real MCP server through the official MCP Python SDK client: one lists its tool, three run crawls (one of them two at once) and one checks that a malformed proxy comes back as a tool error. The four that call the tool need internet access for the startup check.
CodeQL scans the code on every push and pull request to main and once a week. Renovate merges minor and patch dependency updates by itself and leaves major ones for review.
Testing during the work on version 2 found two regular expressions open to catastrophic backtracking (ReDoS): a page with a long unbroken run of characters, such as a base64 image, could hold them up for minutes to hours. Both now cap their repeats, and a test page with a 200,000-character script must finish in under 5 seconds.
Known limits
The repository lists these itself: the [at] and [dot] decoder can, rarely, turn ordinary prose into an address; sitemap indexes are followed one level down, with no size cap; and the MCP tool keeps nothing between calls and shows no progress.
We checked this page against the MailGrab repository, its wiki and the v2.0.0 release notes on 9 October 2026.
How to report a bug or contribute to MailGrab
Report bugs on the MailGrab issues page on GitHub and send changes as pull requests. TODO.md lists the open items and asks you to claim one with an issue first. Under the MIT licence you can also adapt the code in your own copy.
Frequently asked questions
What is MailGrab?
MailGrab is a free, open-source Python tool that crawls a website and lists the email addresses published on its pages, with the page where each one was found. It runs from the command line or as an MCP server for AI agents, and it checks robots.txt by default.
Who built and maintains MailGrab?
Our founder, Nakshatra Ranjan Saha, CEO of N&D Co., wrote MailGrab and maintains it on GitHub, publishing as OCEAN OF ANYTHING from the account OCEANOFANYTHING. The project goes back to January 2022, and v2.0.0, its first tagged release, came out on 3 September 2026.
Is MailGrab free, and what is its licence?
Yes. MailGrab is open source under the MIT licence, so you may use, change and share it, commercially too, as long as the copyright and licence notice stay with it; it comes without warranty.
What do I need to run MailGrab?
MailGrab runs on Windows, Linux or macOS with Python 3.12 or newer, pip, git and an internet connection, plus curl on Linux and macOS, which it uses to check the connection at startup. The pinned requirements.txt sets that minimum, and its lxml 5.4.0 has ready-built packages only up to Python 3.13; on a newer Python, pip has to build lxml from source.
Does MailGrab respect robots.txt and rate limits?
By default it checks each page against the robots.txt rules for all crawlers and waits out their Crawl-delay when it is a whole number of seconds, but sets no rate limit of its own: --delay defaults to 0 while 10 pages load at once. Rules or a Crawl-delay written for MailGrab by name do not apply with its default User-Agent, and Python 3.12's robots.txt parser does not understand * or $ wildcards. Only --ignore-robots, its matching setting or the MCP tool's ignore_robots parameter turns the checks off.
Can AI agents use MailGrab?
Yes. Its MCP server, mailgrab_mcp_server.py, offers one tool, crawl_website, and the wiki explains how to connect it to Claude Code, Claude Desktop, Cursor, GitHub Copilot and Codex CLI. Each call runs in its own process and temporary folder and, by default, stays on the starting site and fetches at most 30 pages; the agent can change both and can turn the robots.txt check off with ignore_robots.
Where does MailGrab save its results?
Each run saves _emails.txt, _scrappedUrls.txt, _emails.csv and _results.json, plus a debug log, _MailGrabLog.txt, in the current directory. The CSV and JSON files record the page each address was found on. A new run replaces the four result files unless you pass --append or --resume, and always replaces the log.
How does MailGrab avoid junk addresses?
It lowercases every address so case variants count once, drops 10 known placeholder addresses and every address at example.com, example.org, example.net and wixpress.com, and reports what it dropped. With --verify-mx it also removes addresses whose domain has no mail server (MX) record; that check covers the domain, not the mailbox.
Is it legal to collect email addresses with MailGrab?
That depends on where you are, whose addresses they are and what you do with them, so ask a lawyer about your own case; this answer is not legal advice. An email address that relates to an identifiable person is personal data under laws such as India's Digital Personal Data Protection Act, 2023 and the EU's GDPR; the FTC's guide to the US CAN-SPAM Act, which covers all commercial email, lists harvesting email addresses among the practices that carry criminal penalties. Use MailGrab to audit your own sites or for research on sites you have permission to crawl, not to build mailing lists.
Does MailGrab accept bug reports and pull requests?
Yes. Report bugs as issues in the MailGrab repository on GitHub, or fork it and send a pull request. TODO.md in the repository lists the open items and asks you to claim one with an issue before you start.
Need a tool built for your business?
Our software development service builds web applications and internal tools. Tell us what you need in a few lines and we will reply with questions or a quote.
Get a quote