Skip to content

SmtpPlugin Guide

SmtpPlugin replaces smtplib.SMTP with a fake class that routes all SMTP operations through a session script. It is included in core bigfoot -- no extra required.

Setup

In pytest, access SmtpPlugin through the bigfoot.smtp_mock proxy. It auto-creates the plugin for the current test on first use:

import bigfoot

def test_send_email():
    (bigfoot.smtp_mock
        .new_session()
        .expect("connect",  returns=None)
        .expect("ehlo",     returns=(250, b"OK"))
        .expect("sendmail", returns={})
        .expect("quit",     returns=(221, b"Bye")))

    with bigfoot:
        import smtplib
        smtp = smtplib.SMTP("mail.example.com", 25)
        smtp.ehlo()
        smtp.sendmail("from@example.com", ["to@example.com"], "Subject: hi\r\n\r\nhello")
        smtp.quit()

    bigfoot.smtp_mock.assert_connect(host="mail.example.com", port=25)
    bigfoot.smtp_mock.assert_ehlo(name="")
    bigfoot.smtp_mock.assert_sendmail(
        from_addr="from@example.com",
        to_addrs=["to@example.com"],
        msg="Subject: hi\r\n\r\nhello",
    )
    bigfoot.smtp_mock.assert_quit()

For manual use outside pytest, construct SmtpPlugin explicitly:

from bigfoot import StrictVerifier
from bigfoot.plugins.smtp_plugin import SmtpPlugin

verifier = StrictVerifier()
smtp = SmtpPlugin(verifier)

Each verifier may have at most one SmtpPlugin. A second SmtpPlugin(verifier) raises ValueError.

State machine

disconnected --connect--> connected --ehlo/helo--> greeted
greeted --starttls--> greeted          (optional, self-loop)
greeted --login--> authenticated       (optional)
greeted/authenticated/sending --sendmail/send_message--> sending
sending/greeted/authenticated --quit--> closed

The connect step fires automatically during smtplib.SMTP(host, port) construction. After that, the SMTP protocol requires a greeting (ehlo or helo) before any mail operations. starttls and login are optional intermediate steps.

Scripting a session

Use new_session() to create a SessionHandle and chain .expect() calls:

(bigfoot.smtp_mock
    .new_session()
    .expect("connect",  returns=None)
    .expect("ehlo",     returns=(250, b"OK"))
    .expect("starttls", returns=(220, b"Ready"))
    .expect("login",    returns=(235, b"Auth OK"))
    .expect("sendmail", returns={})
    .expect("quit",     returns=(221, b"Bye")))

expect() parameters

Parameter Type Default Description
method str required Step name (see below)
returns Any required Value returned by the step (see below)
raises BaseException \| None None Exception to raise instead of returning
required bool True Whether an unused step causes UnusedMocksError at teardown

Return values by step

Step returns type Description
connect None Connection is established implicitly
ehlo tuple[int, bytes] SMTP response code and message
helo tuple[int, bytes] SMTP response code and message
starttls tuple[int, bytes] SMTP response code and message
login tuple[int, bytes] SMTP response code and message
sendmail dict[str, tuple[int, bytes]] Empty dict for success; maps refused recipients to error codes
send_message dict[str, tuple[int, bytes]] Same as sendmail
quit tuple[int, bytes] SMTP response code and message

Asserting interactions

Each step records an interaction on the timeline. Use the typed assertion helpers on bigfoot.smtp_mock:

assert_connect(*, host, port)

bigfoot.smtp_mock.assert_connect(host="mail.example.com", port=587)

assert_ehlo(*, name)

bigfoot.smtp_mock.assert_ehlo(name="")

assert_helo(*, name)

bigfoot.smtp_mock.assert_helo(name="")

assert_starttls()

No fields are required.

bigfoot.smtp_mock.assert_starttls()

assert_login(*, user, password)

bigfoot.smtp_mock.assert_login(user="user@example.com", password="s3cret")

assert_sendmail(*, from_addr, to_addrs, msg)

bigfoot.smtp_mock.assert_sendmail(
    from_addr="from@example.com",
    to_addrs=["to@example.com"],
    msg="Subject: hello\r\n\r\nhello",
)

assert_send_message(*, msg)

bigfoot.smtp_mock.assert_send_message(msg=email_message_object)

assert_quit()

No fields are required.

bigfoot.smtp_mock.assert_quit()

Full authenticated flow

The full flow with TLS and authentication:

import smtplib
import bigfoot

def send_secure_email(host, port, user, password, from_addr, to_addrs, body):
    smtp = smtplib.SMTP(host, port)
    smtp.ehlo()
    smtp.starttls()
    smtp.login(user, password)
    smtp.sendmail(from_addr, to_addrs, body)
    smtp.quit()

def test_send_secure_email():
    (bigfoot.smtp_mock
        .new_session()
        .expect("connect",  returns=None)
        .expect("ehlo",     returns=(250, b"OK"))
        .expect("starttls", returns=(220, b"Ready"))
        .expect("login",    returns=(235, b"Auth OK"))
        .expect("sendmail", returns={})
        .expect("quit",     returns=(221, b"Bye")))

    with bigfoot:
        send_secure_email(
            "smtp.example.com", 587,
            "user@example.com", "s3cret",
            "user@example.com", ["recipient@example.com"],
            "Subject: Report\r\n\r\nSee attached.",
        )

    bigfoot.smtp_mock.assert_connect(host="smtp.example.com", port=587)
    bigfoot.smtp_mock.assert_ehlo(name="")
    bigfoot.smtp_mock.assert_starttls()
    bigfoot.smtp_mock.assert_login(user="user@example.com", password="s3cret")
    bigfoot.smtp_mock.assert_sendmail(
        from_addr="user@example.com",
        to_addrs=["recipient@example.com"],
        msg="Subject: Report\r\n\r\nSee attached.",
    )
    bigfoot.smtp_mock.assert_quit()

Unauthenticated flow

Skip starttls and login for servers that do not require authentication:

def test_send_unauthenticated_email():
    (bigfoot.smtp_mock
        .new_session()
        .expect("connect",  returns=None)
        .expect("ehlo",     returns=(250, b"OK"))
        .expect("sendmail", returns={})
        .expect("quit",     returns=(221, b"Bye")))

    with bigfoot:
        smtp = smtplib.SMTP("mail.example.com", 25)
        smtp.ehlo()
        smtp.sendmail("from@example.com", ["to@example.com"], "Subject: test\r\n\r\ntest")
        smtp.quit()

    bigfoot.smtp_mock.assert_connect(host="mail.example.com", port=25)
    bigfoot.smtp_mock.assert_ehlo(name="")
    bigfoot.smtp_mock.assert_sendmail(
        from_addr="from@example.com",
        to_addrs=["to@example.com"],
        msg="Subject: test\r\n\r\ntest",
    )
    bigfoot.smtp_mock.assert_quit()

The state machine validates that sendmail is called from greeted (after ehlo without login) or from authenticated (after login). Calling sendmail from connected (skipping ehlo) raises InvalidStateError.

Using helo instead of ehlo

Some legacy servers use HELO instead of EHLO. The state machine treats both identically:

def test_helo_flow():
    (bigfoot.smtp_mock
        .new_session()
        .expect("connect",  returns=None)
        .expect("helo",     returns=(250, b"OK"))
        .expect("sendmail", returns={})
        .expect("quit",     returns=(221, b"Bye")))

    with bigfoot:
        smtp = smtplib.SMTP("mail.example.com", 25)
        smtp.helo()
        smtp.sendmail("from@example.com", ["to@example.com"], "Subject: test\r\n\r\ntest")
        smtp.quit()

    bigfoot.smtp_mock.assert_connect(host="mail.example.com", port=25)
    bigfoot.smtp_mock.assert_helo(name="")
    bigfoot.smtp_mock.assert_sendmail(
        from_addr="from@example.com",
        to_addrs=["to@example.com"],
        msg="Subject: test\r\n\r\ntest",
    )
    bigfoot.smtp_mock.assert_quit()

Using send_message

send_message accepts an email.message.EmailMessage object and works the same as sendmail from a state machine perspective:

from email.message import EmailMessage

def test_send_message():
    msg = EmailMessage()
    msg["Subject"] = "Report"
    msg["From"] = "from@example.com"
    msg["To"] = "to@example.com"
    msg.set_content("See attached.")

    (bigfoot.smtp_mock
        .new_session()
        .expect("connect",      returns=None)
        .expect("ehlo",         returns=(250, b"OK"))
        .expect("send_message", returns={})
        .expect("quit",         returns=(221, b"Bye")))

    with bigfoot:
        smtp = smtplib.SMTP("mail.example.com", 25)
        smtp.ehlo()
        smtp.send_message(msg)
        smtp.quit()

    bigfoot.smtp_mock.assert_connect(host="mail.example.com", port=25)
    bigfoot.smtp_mock.assert_ehlo(name="")
    bigfoot.smtp_mock.assert_send_message(msg=msg)
    bigfoot.smtp_mock.assert_quit()