Email testing with Java
Built on java.net.http, no HTTP dependency of its own. Made for Selenium and JUnit suites that need a real address per test.
Before you begin
- A MailFlat accountFree to start, no card. Every plan can open inboxes from the API.
- An account API keyAgents → API keys in the dashboard. It looks like mf_live_… and goes in the X-API-Key header. Keep it in an environment variable, never in the repo. See API keys and authentication.
- Java 17+That is the whole toolchain requirement.
Install
<!-- pom.xml: add the JitPack repository first, or resolution fails -->
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependency>
<groupId>com.github.onderyentar21</groupId>
<artifactId>mailflat-sdks</artifactId>
<version>v0.1.1</version>
</dependency>
<!-- Gradle -->
<!-- repositories { maven { url "https://jitpack.io" } } -->
<!-- implementation "com.github.onderyentar21:mailflat-sdks:v0.1.1" -->
Package: mailflat-sdks
Your first inbox and one-time code
The whole loop: open an address, let your app mail it, read the code back, clean up.
import net.mailflat.MailFlat;
import net.mailflat.Inbox;
MailFlat mf = new MailFlat(System.getenv("MAILFLAT_API_KEY"));
// 1. a real, deliverable address
Inbox inbox = mf.create("signup");
System.out.println(inbox.address()); // signup@a7f2c.mailflat.net
// 2. your app sends the code to that address
myApp.register(inbox.address());
// 3. read it back, no mocking anywhere
String otp = inbox.waitForOtp(30); // seconds
System.out.println(otp); // "482913"
// 4. done with it
inbox.delete();
Line by linewhat each step of this Java example does
- create(...)
- Returns immediately with a real, deliverable address. Nothing is queued or simulated.
- retention_hours
- Optional. Messages purge themselves after it, so a skipped teardown never leaks.
- wait_for_otp
- Polls for you and fails loudly on timeout, instead of returning nothing three lines before the real error.
- delete()
- Optional but tidy. Retention would clean up anyway.
The address is real
Mail actually travels: SMTP, DKIM, the lot. Nothing is stubbed, so a broken template or a misconfigured sender fails here exactly like it would in production.
Waiting for the code
We extract the one-time code server-side and hand it to you as a field, so you never write a regex against an email body.
// seconds, not milliseconds
String otp = inbox.waitForOtp(30);
// need the whole message?
Message msg = inbox.waitForMessage(30);
System.out.println(msg.subject() + " from " + msg.sender());
System.out.println(msg.text());
// throws on timeout, so a missing email fails the test loudly
try {
inbox.waitForOtp(5);
} catch (MailFlatException e) {
System.out.println("no code arrived: " + e.getMessage());
}
Always set a timeout
A poll loop without a deadline turns a missing email into a hung job. Fail loudly instead: the error message should name the address you were waiting on.
Reading every message
for (Message msg : inbox.messages()) { // newest first
System.out.println(msg.subject() + " " + msg.receivedAt());
System.out.println(msg.text()); // plain text body
System.out.println(msg.html()); // HTML body
System.out.println(msg.otp()); // extracted code, or null
}
Optional<Message> latest = inbox.latest(); // empty until the first message lands
latest.filter(m -> m.subject().contains("Reset"))
.ifPresent(m -> inbox.deleteMessage(m.id()));
| Field | Type | What it is |
|---|---|---|
| subject | string | Subject line |
| sender | string | From address |
| body_text | string | Plain text body |
| body_html | string | HTML body |
| otp_code | string | null | One-time code, extracted by us |
| to_address | string | The exact address it was sent to, tag included |
| tag | string | null | Plus-addressing tag, if the sender used one |
| received_at | ISO 8601 | When it landed |
| is_encrypted | boolean | True on end-to-end encrypted inboxes, where body and code are unavailable |
Sending mail from the inbox
Useful in reverse: point your own inbound pipeline at a MailFlat address and check that it handles what arrives.
// mail leaves from the inbox address, DKIM-signed by our own MTA
inbox.send("someone@example.com", "Welcome", "Plain text body");
// with an HTML body
inbox.send("someone@example.com", "Welcome", "Plain text", "<p>HTML body</p>");
Cleaning up
inbox.delete(); // inbox and every message, immediately
// or leave it: messages expire on their own at the retention you asked for
Inbox inbox = mf.create(CreateInboxOptions.builder()
.label("ci")
.retentionHours(2)
.build());
Two safety nets, use both
Delete in teardown so the list stays readable, and set retention_hours so a crashed run still cleans itself up.
In a test suite
Works with JUnit 5, Selenium, TestNG, Cucumber and anything else that gives you a setup and teardown hook.
// SignupTest.java — a fresh inbox per test, alongside Selenium
import net.mailflat.MailFlat;
import net.mailflat.Inbox;
import org.junit.jupiter.api.*;
import org.openqa.selenium.By;
class SignupTest {
static MailFlat mf = new MailFlat(System.getenv("MAILFLAT_API_KEY"));
Inbox inbox;
@BeforeEach
void openInbox() {
inbox = mf.create("ci-" + java.util.UUID.randomUUID().toString().substring(0, 8));
}
@AfterEach
void closeInbox() {
inbox.delete();
}
@Test
void userCanSignUpWithARealCode() {
driver.get("https://staging.example.com/signup");
driver.findElement(By.id("email")).sendKeys(inbox.address());
driver.findElement(By.id("submit")).click();
driver.findElement(By.id("code")).sendKeys(inbox.waitForOtp(60));
Assertions.assertTrue(driver.getCurrentUrl().endsWith("/dashboard"));
}
}
Line by linewhat each step of this Java example does
- unique prefix
- One inbox per test, so parallel workers never read each other's mail.
- teardown hook
- Runs even when the test fails — that is exactly when inboxes get left behind.
- real code, real email
- No test-mode backdoor in your app: the path under test is the one your users take.
Java reference
| Method | What it does |
|---|---|
| mf.create() / mf.create(label) / mf.create(CreateInboxOptions) | Open an inbox, returns Inbox |
| mf.list() | Every inbox this key opened |
| mf.inbox(address) | Attach to an existing address without an API call |
| inbox.waitForOtp(seconds) | Poll until a code arrives, throws on timeout |
| inbox.waitForMessage(seconds) | Same, but returns the whole Message |
| inbox.messages() / inbox.latest() | List<Message> (newest first) / Optional<Message> |
| inbox.send(to, subject, body[, html]) | Send from this address, DKIM-signed |
| inbox.delete() / inbox.deleteMessage(id) | Drop the inbox / one message |
Full endpoint reference, including error shapes and rate limits: Agent API and MCP.