Extract any regex-matchable value (2FA/OTP codes, links, tokens, text) from an IMAP mailbox without deleting messages.
Project description
email-code-finder
Wait for an email to arrive in an IMAP mailbox and extract any value you can describe with a regular expression — a 2FA/OTP code, a confirmation link, a tracking number, an order ID, a token, or any piece of text. The library connects over IMAP, waits for the message whose subject you specify, runs your regex against the body, and returns the captured value — without ever deleting your messages.
Not just 2FA. One-time codes are the most common use case, but the engine is generic: if the value is somewhere in the email body and a regex can capture it, this library can fetch it. The
regex_patternyou provide is the only thing that decides what gets extracted.
Examples of what you can extract:
| Goal | Example subject_to_find |
Example regex_pattern |
|---|---|---|
| 2FA / OTP code | Your verification code |
\b(\d{6})\b |
| Confirmation link | Confirm your email |
href="(https://[^"]*/confirm[^"]*)" |
| Order / tracking ID | Your order shipped |
Tracking:\s*([A-Z0-9]{10,}) |
| Arbitrary text | Your invoice |
Invoice No\.\s*(\S+) |
- No third-party dependencies — standard library only.
- Non-destructive — emails are flagged as read, never deleted.
- Provider-aware — Gmail, Outlook/Office 365, Yahoo, iCloud, or any custom IMAP host.
- Pluggable notifications — pass a callback to surface progress in your UI.
Installation
pip install email-code-finder
Requires Python 3.8+.
Quick start
from email_code_finder import EmailCodeFinder
config = {
"provider": "gmail",
"user_email": "user@example.com",
"password": "your-app-specific-password",
"subject_to_find": "Your verification code",
"regex_pattern": r"(?s)token-2fa-text\"?>.*?<b>(.*?)</b>.*?</div>",
"max_wait_time_seconds": 180,
"check_interval_seconds": 6,
}
finder = EmailCodeFinder(config=config, notify_callback=print)
code = finder.wait_for_code()
if code:
print(f"Got the code: {code}")
else:
print("Timed out without receiving a code.")
Or load the configuration from a JSON file:
finder = EmailCodeFinder(config_path="config.json")
code = finder.wait_for_code()
A runnable example lives in examples/basic_usage.py.
Configuration
Configuration can be passed as a dict or stored in a JSON file (see
examples/config.example.json).
| Key | Type | Required | Description |
|---|---|---|---|
user_email |
str | ✅ | Full email address used to authenticate. |
password |
str | ✅ | Account or app-specific password (see Security). |
subject_to_find |
str | ✅ | Subject line of the email carrying the code. A Fwd: variant is matched too. |
regex_pattern |
str | ✅ | Regex whose first capture group is the code. |
provider |
str | ➖ | gmail, outlook, office365, yahoo, icloud, kinghost. If omitted, the host is derived from your email domain (imap.<domain>). |
imap_server |
str | ➖ | Explicit IMAP host; overrides provider detection. |
max_wait_time_seconds |
int | ➖ | How long to wait before giving up. Default 180. |
check_interval_seconds |
int | ➖ | Delay between inbox polls. Default 6. |
Writing the regex
regex_pattern is matched with re.DOTALL; the value returned is capture
group 1. For a code wrapped in <b>123456</b> you might use:
<b>(\d{6})</b>
Test your pattern against a real email body before relying on it.
API
EmailCodeFinder(config=None, config_path="config.json", notify_callback=None)
config— configuration dict. WhenNone, it is loaded fromconfig_path.config_path— path to a JSON config file (used only whenconfigisNone).notify_callback— optionalCallable(message: str)invoked for user-facing events (waiting, code found, timeout, error). Must be thread-safe.
Missing required keys raise ValueError.
wait_for_code() -> Optional[str]
Connects, polls the inbox until the code arrives or the timeout elapses, and
returns the code (or None). On success the matching email is flagged as read.
The connection is always closed when the call returns.
ImapEmailClient
Lower-level IMAP wrapper exposed for advanced use (connect, get_max_uid,
search_unread_by_subject, fetch_body, extract_code, mark_as_read,
logout). It performs no destructive operations.
How matching works: timing & the UID baseline
Understanding the sequence is important to use the library correctly.
- Baseline. The moment
wait_for_code()connects, it reads the highest existing message UID in the inbox and stores it as a baseline. IMAP UIDs only ever increase, so this is a precise "everything up to here is old" mark. - Polling. Every
check_interval_secondsit searches the inbox for unread messages whose subject matchessubject_to_find(aFwd:variant is matched too). - New-only filter. Any match with a UID less than or equal to the baseline is skipped — it was already there before you started waiting, so it is treated as stale. Only genuinely new messages are inspected.
- Extraction. The regex runs against the body of each new match. The first message that yields a capture group wins: that value is returned and the message is flagged as read.
- Timeout. If nothing matches within
max_wait_time_seconds, the call returnsNone.
This UID baseline replaces the old, dangerous behaviour of deleting the inbox to "clean up" previous codes. Your existing emails are never touched.
⏱️ Critical: start waiting before the email is sent
Because the baseline is taken at the start, an email that arrives before you
call wait_for_code() will be at or below the baseline and therefore ignored.
Trigger the action that generates the email after (or concurrently with)
starting the wait:
import threading
from email_code_finder import EmailCodeFinder
finder = EmailCodeFinder(config=config)
# Run wait_for_code() first (in a thread), THEN trigger the email.
result = {}
waiter = threading.Thread(target=lambda: result.update(code=finder.wait_for_code()))
waiter.start()
trigger_login() # the action that makes the provider send the email
waiter.join()
print(result["code"])
Tuning the delays
| Setting | What it controls | Guidance |
|---|---|---|
max_wait_time_seconds |
Total time to wait before giving up. | Set it above the worst-case email delivery time. Mail can take anywhere from a few seconds to a couple of minutes; 180 (3 min) is a safe default. |
check_interval_seconds |
Pause between inbox polls. | Lower = the code is picked up sooner, but more IMAP requests. 6 is a good balance. Avoid going below 2–3 so you don't hit provider rate limits or get your IP throttled. |
The call returns as soon as a matching code is found — the interval is only the upper bound on how long after arrival you notice it, not a fixed wait.
Security
⚠️ This library handles mailbox credentials. Read this section.
-
Use app-specific passwords, not your main account password. Gmail, Outlook, Yahoo and iCloud all support them and most require them when 2FA is enabled on the account.
-
Never commit
config.json. It is listed in.gitignore. Prefer loading secrets from environment variables or a secret manager in production, e.g.:import os config["password"] = os.environ["EMAIL_PASSWORD"]
-
Connections use TLS (
IMAP4_SSLon port 993) with certificate verification viassl.create_default_context(). Do not disable verification. -
Least privilege. If your provider supports it, use a dedicated mailbox or an account scoped only to receiving these codes.
-
Logging. Extracted codes are written to logs at
DEBUG/INFOlevel for troubleshooting. Keep your log level and log storage appropriately restricted, and avoidDEBUGin production if logs are shared. -
Regex from untrusted input. If
regex_patternever comes from an untrusted source, beware of catastrophic backtracking (ReDoS). Prefer simple, anchored patterns.
Limitations
- IMAP only; POP3 and provider-specific APIs are not supported.
- Reads from
INBOXonly. - The code must be extractable from the email body via a single regex group.
Development
git clone https://github.com/erikmelias/email-code-finder.git
cd email-code-finder
pip install -e ".[dev]"
pytest
Tests use a mocked IMAP client and make no network connections.
License
MIT © Erik Melias
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file email_code_finder-0.1.0.tar.gz.
File metadata
- Download URL: email_code_finder-0.1.0.tar.gz
- Upload date:
- Size: 15.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7f7c0a874e2ab0aee400791e12cc688557ebfe978ad9408b14aa918e3f44b296
|
|
| MD5 |
e9da91b5af3d6f0d7e6df71c9a03dc47
|
|
| BLAKE2b-256 |
59dfb70563b6a30d7de9252c5f13a43fab6b02d223553dc0ebb32114cb71c329
|
File details
Details for the file email_code_finder-0.1.0-py3-none-any.whl.
File metadata
- Download URL: email_code_finder-0.1.0-py3-none-any.whl
- Upload date:
- Size: 11.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f4ad6180f67764ed034f3ad22d348617b8e75a2448b040228883b37beeac1293
|
|
| MD5 |
5a2915a0801ba59ad8a7c97a18b843cf
|
|
| BLAKE2b-256 |
0b3757796f2bad3feb3b77a66f23b99fa8d2ac499f373da872c2daa1232940dc
|