DEV Community

ZickMail
ZickMail

Posted on

Fetching email verification codes over IMAP with Python (no third-party libraries)

Signup flows, password resets, one-time login codes: almost every app sends an email you have to open by hand. When you test these flows dozens of times a day, or run several projects that each need their own address, copying six digits out of an inbox gets old fast.

This post shows a small, dependency-free Python helper that waits for a message and pulls the code out of it, plus a few things to look for in a mailbox provider when you automate this.

Why IMAP

IMAP is the standard protocol every mail client speaks. Python ships imaplib in the standard library, so there is nothing to install. Any provider that gives you IMAP access lets you read mail from code the same way Thunderbird does.

The helper

import imaplib, email, re, time

def get_code(host, login, password, sender=None, timeout=60):
    """Wait for an unread message and return the first 4-8 digit code."""
    deadline = time.time() + timeout
    while time.time() < deadline:
        with imaplib.IMAP4_SSL(host, 993) as box:
            box.login(login, password)
            box.select("INBOX")
            query = f'(UNSEEN FROM "{sender}")' if sender else "(UNSEEN)"
            _, data = box.search(None, query)
            ids = data[0].split()
            if ids:
                _, msg_data = box.fetch(ids[-1], "(RFC822)")
                msg = email.message_from_bytes(msg_data[0][1])
                text = ""
                for part in msg.walk():
                    if part.get_content_type() in ("text/plain", "text/html"):
                        text += part.get_payload(decode=True).decode(errors="ignore")
                m = re.search(r"\b(\d{4,8})\b", text)
                if m:
                    return m.group(1)
        time.sleep(3)
    return None

code = get_code("imap.zick-mail.casa", "box@zickmail.com", "password",
                sender="noreply@github.com")
print(code)
Enter fullscreen mode Exit fullscreen mode

It polls every three seconds, takes the newest unread message from the given sender and extracts the first 4 to 8 digit number. Swap the regex for https://\S+ if the service sends an activation link instead of a code.

A few practical notes:

  • Mark as seen. Fetching with RFC822 marks the message as read, so the next call will not return the same code twice.
  • HTML only letters. Many senders have no plain-text part. Searching both parts, as above, covers that.
  • Timeouts. Codes usually expire in 5 to 10 minutes. Keep the timeout well under that.

Working with many mailboxes

Keep credentials in a plain email:password file, one per line, and loop over it:

with open("boxes.txt") as f:
    for line in f:
        login, password = line.strip().split(":", 1)
        print(login, get_code("imap.zick-mail.casa", login, password, timeout=20))
Enter fullscreen mode Exit fullscreen mode

What to look for in a provider

  • Delivery speed. If a message takes a minute to arrive, your test is flaky.
  • Mailbox lifetime. Ten-minute inboxes break the moment you need a password reset a month later.
  • Public uptime page. You should be able to check whether a failure is yours or theirs.
  • Several domains. Some services treat popular domains differently, so having a choice helps.

Disclosure

I work on ZickMail, which is the host used in the example. It gives permanent mailboxes on several domains with IMAP on imap.zick-mail.casa:993, messages arrive in under a second, and pricing starts at 0.06 RUB per mailbox. The helper above works with any IMAP provider though.

Top comments (0)