Mailfornet

Email testing with Selenium and Python

A pytest fixture that gives each test a fresh inbox, and one call that returns the verification code. It works with Selenium, Playwright for Python, or plain requests.

1. Install

pip install pytest requests selenium
export MAILFORNET_API_KEY="mf_live_your_key"

2. The fixture

In conftest.py:

import os, time
import pytest, requests

API = "https://api.mailfornet.com/v1"


class Inbox:
    def __init__(self, session, address):
        self.session, self.address, self.since = session, address, int(time.time() * 1000)

    def wait_for_code(self, subject=None, sender=None, timeout=60):
        """Blocks until a verification code or confirm link arrives. Returns the JSON body."""
        params = {"wait": min(timeout, 60), "since": self.since}
        if subject:
            params["subject"] = subject
        if sender:
            params["from"] = sender
        r = self.session.get(f"{API}/inboxes/{self.address}/code", params=params, timeout=timeout + 10)
        if r.status_code == 404:
            raise AssertionError(f"No verification email reached {self.address} in {timeout}s")
        r.raise_for_status()
        return r.json()  # {"code": "...", "link": "...", "subject": "...", ...}


@pytest.fixture(scope="session")
def mailfornet():
    s = requests.Session()
    s.headers["Authorization"] = f"Bearer {os.environ['MAILFORNET_API_KEY']}"
    return s


@pytest.fixture
def inbox(mailfornet):
    r = mailfornet.post(f"{API}/inboxes", json={"ttlMinutes": 30})
    r.raise_for_status()
    box = Inbox(mailfornet, r.json()["address"])
    yield box
    mailfornet.delete(f"{API}/inboxes/{box.address}")

3. A Selenium test

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import pytest


@pytest.fixture
def driver():
    d = webdriver.Chrome()
    yield d
    d.quit()


def test_signup_verification(driver, inbox):
    driver.get("https://staging.yourapp.com/signup")
    driver.find_element(By.NAME, "email").send_keys(inbox.address)
    driver.find_element(By.NAME, "password").send_keys("Correct-Horse-9")
    driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()

    mail = inbox.wait_for_code(subject="Verify")
    assert mail["code"] and len(mail["code"]) == 6

    driver.find_element(By.NAME, "code").send_keys(mail["code"])
    driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
    WebDriverWait(driver, 10).until(EC.text_to_be_present_in_element((By.TAG_NAME, "h1"), "Welcome"))


def test_confirm_link(driver, inbox):
    driver.get("https://staging.yourapp.com/signup")
    driver.find_element(By.NAME, "email").send_keys(inbox.address)
    driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()

    link = inbox.wait_for_code(subject="Confirm")["link"]
    driver.get(link)
    assert "confirmed" in driver.page_source.lower()

4. API-only tests (no browser)

Testing a backend that sends mail? The same fixture works without Selenium:

def test_signup_api_sends_code(inbox):
    requests.post("https://staging.yourapp.com/api/signup", json={"email": inbox.address}).raise_for_status()
    mail = inbox.wait_for_code()
    assert mail["from"].endswith("@yourapp.com")
    assert mail["code"]

Parallel runs

With pytest-xdist (pytest -n 8) each worker creates its own inboxes, so tests never read each other's mail. Nothing needs to change in the fixture.

Reading the full email

full = mailfornet.get(f"{API}/messages/{mail['messageId']}", params={"address": inbox.address}).json()
assert "Welcome to YourApp" in full["text"]
print(full["links"])            # every link, with its anchor text
print(full["verificationLink"]) # the one that confirms something

Related

Playwright guide · Cypress guide · Code examples · API reference

Get a free API key