MailFlatDocs
Documentation/Test frameworks & CI/Robot Framework

Using MailFlat with Robot Framework

Wrap the Python client in a small keyword library and your suites get New Inbox and Wait For Otp as ordinary Robot keywords.

How it fits together

Robot Framework talks to Python, and our Python client is two calls, so the library file is short. Once it exists, the suite reads like every other Robot suite. The part that catches people is not the client, it is library scope. A library class with no scope declared is instantiated once for the entire run, so the inbox held on self is shared by every suite in that run. Since the waiting keyword returns the newest message in the box, a test can read a code that a completely different suite triggered, and it passes.

The keyword library

One Python file, three keywords, and one line that decides the whole isolation story. ROBOT_LIBRARY_SCOPE defaults to GLOBAL, which means one instance for the run; TEST gives each test case its own instance and therefore its own inbox.
Python
# MailFlatLibrary.py
import os, uuid
from mailflat import MailFlat
 
class MailFlatLibrary:
ROBOT_LIBRARY_SCOPE = "TEST" # one instance per test case, so one inbox per test case
 
def __init__(self):
self._mf = MailFlat(api_key=os.environ["MAILFLAT_API_KEY"])
self._inbox = None
 
def new_inbox(self):
self._inbox = self._mf.create(
prefix=f"robot-{uuid.uuid4().hex[:8]}", retention_hours=2
)
return self._inbox.address
 
def wait_for_otp(self, timeout=60):
return self._inbox.wait_for_otp(timeout=int(timeout))
 
def delete_inbox(self):
if self._inbox:
self._inbox.delete()
self._inbox = None

The suite

Robot converts method names to keywords: new_inbox becomes New Inbox. Arguments arrive as strings, which is why the library casts the timeout to an int before handing it over; the Python client takes it as a number of seconds and would reject the string.
Robot Framework
*** Settings ***
Library SeleniumLibrary
Library MailFlatLibrary
Test Teardown Delete Inbox
 
*** Test Cases ***
User Can Sign Up With A Real Code
${address}= New Inbox
Go To https://staging.example.com/signup
Input Text id:email ${address}
Click Button id:submit
 
${otp}= Wait For Otp 60
Input Text id:code ${otp}
Location Should Contain /dashboard

Worth knowing

Library scope defaults to GLOBAL, meaning one instance for the whole run. Declare ROBOT_LIBRARY_SCOPE = 'TEST' unless you have a reason to share the inbox, because the shared version fails silently rather than loudly.
The Python client keeps its wait in seconds while the JavaScript one uses milliseconds. Wait For Otp 60 waits a minute; the same number in a JavaScript suite would be sixty milliseconds.
wait_for_otp takes keyword arguments only, so the library has to call it as wait_for_otp(timeout=...). Passing the value positionally raises a TypeError that has nothing to do with mail.
Give the waiting test a [Timeout] longer than the wait itself. Robot's timeout stops the keyword mid poll and reports its own message, which hides the client's, and the client's is the one that says whether anything arrived at all.
The address is permanent until you delete it. For a smoke suite that runs on a schedule, put one address in a variable file and give the run a key with inbox:read only, so no keyword in the suite is able to create or delete anything.

When it goes wrong

SymptomWhyWhat to do
A test passes with a code that belongs to another suite entirelyThe library is running at GLOBAL scope, so every suite shares one instance and one inbox. The waiting keyword returns the newest message in that box regardless of which suite caused it.Set ROBOT_LIBRARY_SCOPE = 'TEST' as shown. This is the failure mode worth checking first, because a suite hitting it stays green.
New Inbox fails immediately with a permission errorCreating and deleting boxes sit behind the inbox:manage scope, and a newly issued key carries inbox:read only.Reissue the key with inbox:manage, or drop New Inbox and read a fixed address from a variable file. The Python client raises this as MailFlatPermissionError, named that way so it does not shadow Python's own built-in.
Wait For Otp raises OTPTimeoutError saying mail did arriveA message landed but no code could be read out of it. That is a parsing miss rather than a delivery failure, and the two need different fixes.The error quotes the newest message. Add a keyword that returns inbox.latest() and read the body in the report, then send us the format so it stops needing a workaround.
The suite works alone and fails when run with the othersFree allows three agent inboxes at once. Running suites together multiplies the boxes opened in the same window, and the fourth is refused with a 400 that names the plan and the number.Keep Delete Inbox in Test Teardown so each box is returned as soon as its test ends. Paid plans do not cap agent inboxes.
A scheduled suite reports an empty inbox in the morningThe inbox is still there; the messages are not. Retention is two hours on Free, so anything sent by an earlier run has already expired.Trigger the mail inside the test that reads it. If a test genuinely needs older mail, paid plans hold messages for up to thirty days.