Skip to main content

Extract any regex-matchable value (2FA/OTP codes, links, tokens, text) from an IMAP mailbox without deleting messages.

Project description

email-code-finder

PyPI Python License: MIT

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_pattern you 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. When None, it is loaded from config_path.
  • config_path — path to a JSON config file (used only when config is None).
  • notify_callback — optional Callable(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.

  1. 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.
  2. Polling. Every check_interval_seconds it searches the inbox for unread messages whose subject matches subject_to_find (a Fwd: variant is matched too).
  3. 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.
  4. 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.
  5. Timeout. If nothing matches within max_wait_time_seconds, the call returns None.

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_SSL on port 993) with certificate verification via ssl.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/INFO level for troubleshooting. Keep your log level and log storage appropriately restricted, and avoid DEBUG in production if logs are shared.

  • Regex from untrusted input. If regex_pattern ever 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 INBOX only.
  • 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

email_code_finder-0.1.0.tar.gz (15.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

email_code_finder-0.1.0-py3-none-any.whl (11.4 kB view details)

Uploaded Python 3

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

Hashes for email_code_finder-0.1.0.tar.gz
Algorithm Hash digest
SHA256 7f7c0a874e2ab0aee400791e12cc688557ebfe978ad9408b14aa918e3f44b296
MD5 e9da91b5af3d6f0d7e6df71c9a03dc47
BLAKE2b-256 59dfb70563b6a30d7de9252c5f13a43fab6b02d223553dc0ebb32114cb71c329

See more details on using hashes here.

File details

Details for the file email_code_finder-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for email_code_finder-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f4ad6180f67764ed034f3ad22d348617b8e75a2448b040228883b37beeac1293
MD5 5a2915a0801ba59ad8a7c97a18b843cf
BLAKE2b-256 0b3757796f2bad3feb3b77a66f23b99fa8d2ac499f373da872c2daa1232940dc

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page