Java SDK
Complete guide to the OneShotMail Java SDK -- Maven/Gradle installation, API reference, JUnit 5 and Cucumber-JVM integration.
Installation
Maven
<dependency>
<groupId>com.oneshotmail</groupId>
<artifactId>oneshot-mail</artifactId>
<version>0.1.0</version>
</dependency>
Gradle
implementation 'com.oneshotmail:oneshot-mail:0.1.0'
Requires Java 17+ (uses java.net.http.HttpClient). Depends on Gson for JSON handling.
Configuration
From environment variable
import com.oneshotmail.OneShotClient;
// Reads ONESHOT_API_KEY from environment if constructor arg is null/empty
var client = new OneShotClient(null);
Explicit API key
var client = new OneShotClient("osm_live_your_key");
Custom base URL
var client = new OneShotClient("osm_live_your_key", "http://localhost:4566/v1");
API Reference
create(ttlSeconds, label, mode)
Create a new one-shot email address.
| Parameter | Type | Description |
|---|---|---|
ttlSeconds | int | TTL in seconds (0 defaults to 3600). |
label | String | Optional label (null for none). |
mode | String | "receive" or "send" (null defaults to receive). |
Returns: Address
Throws: OneShotException
Address addr = client.create(300, "signup-test", "receive");
System.out.println(addr.id); // "abc123xyz789def456"
System.out.println(addr.address); // "abc123xyz789def456@in.oneshotemail.com"
System.out.println(addr.status); // "waiting"
get(addressId)
Retrieve an address by ID.
Throws: OneShotException (404 if not found, 410 if expired).
Address addr = client.get("abc123xyz789def456");
if ("received".equals(addr.status)) {
System.out.println("Got email!");
}
getEmail(addressId)
Retrieve the full email content.
Returns: Email with fields: from, to, subject, textBody, htmlBody, headers, receivedAt, sizeBytes, attachments.
Email email = client.getEmail("abc123xyz789def456");
System.out.println("From: " + email.from);
System.out.println("Subject: " + email.subject);
System.out.println("Body: " + email.textBody);
System.out.println("Attachments: " + email.attachments.size());
getEmailRaw(addressId)
Retrieve the raw RFC 822 email source.
String raw = client.getEmailRaw("abc123xyz789def456");
downloadAttachment(addressId, index)
Download an attachment by zero-based index.
Returns: byte[]
byte[] data = client.downloadAttachment("abc123xyz789def456", 0);
Files.write(Path.of("invoice.pdf"), data);
waitForEmail(addressId, timeout)
Poll until an email arrives or timeout. Uses exponential backoff (2s initial, 1.4x multiplier, 10s cap).
| Parameter | Type | Description |
|---|---|---|
addressId | String | The address ID. |
timeout | Duration | Max wait time. |
Throws: OneShotException on timeout (status code 408) or if address expires (410). Also throws InterruptedException if the thread is interrupted.
Email email = client.waitForEmail(addr.id, Duration.ofSeconds(30));
System.out.println("Subject: " + email.subject);
send(to, subject, textBody, htmlBody, ttlSeconds)
Send a one-shot email.
Address result = client.send(
"intake@myapp.com",
"Test invoice",
"Please process this invoice.",
null, // htmlBody
300 // ttlSeconds
);
System.out.println("Sent from: " + result.address);
list(status, label, mode, limit)
List addresses with optional filtering.
Returns: List<Address>
List<Address> addresses = client.list("waiting", null, "receive", 10);
for (Address addr : addresses) {
System.out.printf("%s: %s%n", addr.id, addr.status);
}
delete(addressId)
Delete an address immediately.
client.delete("abc123xyz789def456");
deleteByLabel(label)
Bulk-delete all addresses with the given label.
client.deleteByLabel("ci-run-abc123");
account()
Get account details.
Account acct = client.account();
System.out.printf("Plan: %s, Credits: %d%n", acct.plan, acct.creditsRemaining);
health()
Check API health.
HealthStatus h = client.health();
System.out.printf("Status: %s, Region: %s%n", h.status, h.region);
Error handling
All API errors throw OneShotException:
public class OneShotException extends Exception {
public String getCode(); // e.g., "QUOTA_EXCEEDED"
public String getMessage(); // Human-readable message
public int getStatusCode(); // HTTP status code (0 for non-API errors)
}
try {
Email email = client.waitForEmail(addr.id, Duration.ofSeconds(30));
} catch (OneShotException e) {
switch (e.getStatusCode()) {
case 402 -> System.err.println("Quota exceeded: " + e.getMessage());
case 408 -> System.err.println("Timeout: email did not arrive");
case 410 -> System.err.println("Address expired");
case 429 -> System.err.println("Rate limited, retry later");
default -> throw e;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("Interrupted while waiting for email");
}
JUnit 5 integration
Extension for shared client
// OneShotExtension.java
import org.junit.jupiter.api.extension.*;
public class OneShotExtension implements BeforeAllCallback, AfterAllCallback,
BeforeEachCallback, AfterEachCallback, ParameterResolver {
private static final String CLIENT_KEY = "oneshotClient";
private static final String RUN_LABEL_KEY = "runLabel";
private static final String ADDR_KEY = "currentAddress";
@Override
public void beforeAll(ExtensionContext ctx) {
var store = ctx.getStore(ExtensionContext.Namespace.GLOBAL);
var client = new OneShotClient(System.getenv("ONESHOT_API_KEY"));
var runLabel = "junit-" + java.util.UUID.randomUUID().toString().substring(0, 8);
store.put(CLIENT_KEY, client);
store.put(RUN_LABEL_KEY, runLabel);
}
@Override
public void afterAll(ExtensionContext ctx) throws Exception {
var store = ctx.getStore(ExtensionContext.Namespace.GLOBAL);
var client = (OneShotClient) store.get(CLIENT_KEY);
var label = (String) store.get(RUN_LABEL_KEY);
if (client != null && label != null) {
client.deleteByLabel(label);
}
}
@Override
public void beforeEach(ExtensionContext ctx) throws Exception {
var store = ctx.getStore(ExtensionContext.Namespace.GLOBAL);
var client = (OneShotClient) store.get(CLIENT_KEY);
var label = (String) store.get(RUN_LABEL_KEY);
var addr = client.create(300, label, "receive");
ctx.getStore(ExtensionContext.Namespace.create(ctx.getUniqueId())).put(ADDR_KEY, addr);
}
@Override
public void afterEach(ExtensionContext ctx) throws Exception {
var store = ctx.getStore(ExtensionContext.Namespace.create(ctx.getUniqueId()));
var globalStore = ctx.getStore(ExtensionContext.Namespace.GLOBAL);
var client = (OneShotClient) globalStore.get(CLIENT_KEY);
var addr = (Address) store.get(ADDR_KEY);
if (client != null && addr != null) {
try { client.delete(addr.id); } catch (OneShotException ignored) {}
}
}
@Override
public boolean supportsParameter(ParameterContext paramCtx, ExtensionContext extCtx) {
return paramCtx.getParameter().getType() == Address.class
|| paramCtx.getParameter().getType() == OneShotClient.class;
}
@Override
public Object resolveParameter(ParameterContext paramCtx, ExtensionContext extCtx) {
var globalStore = extCtx.getStore(ExtensionContext.Namespace.GLOBAL);
if (paramCtx.getParameter().getType() == OneShotClient.class) {
return globalStore.get(CLIENT_KEY);
}
return extCtx.getStore(ExtensionContext.Namespace.create(extCtx.getUniqueId())).get(ADDR_KEY);
}
}
Test class
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(OneShotExtension.class)
class SignupTest {
@Test
void sendVerificationEmail(OneShotClient client, Address addr) throws Exception {
// Trigger signup with the temporary address
MyApp.signup(addr.address, "Test User");
// Wait for the verification email
Email email = client.waitForEmail(addr.id, Duration.ofSeconds(30));
assertTrue(email.subject.contains("Verify your account"));
assertTrue(email.textBody.contains("Test User"));
}
@Test
void sendPasswordResetEmail(OneShotClient client, Address addr) throws Exception {
MyApp.signup(addr.address, "Test User");
MyApp.requestPasswordReset(addr.address);
// Note: this test needs a second address since the first one
// already received the signup email (one-shot guarantee).
// The extension creates a fresh address per test.
Email email = client.waitForEmail(addr.id, Duration.ofSeconds(30));
assertTrue(email.subject.contains("Reset") || email.subject.contains("Verify"));
}
}
Simpler approach with @BeforeEach / @AfterEach
If you prefer not to use a custom extension:
import org.junit.jupiter.api.*;
class EmailTest {
private static OneShotClient client;
private static String runLabel;
private Address address;
@BeforeAll
static void setupClient() {
client = new OneShotClient(System.getenv("ONESHOT_API_KEY"));
runLabel = "junit-" + java.util.UUID.randomUUID().toString().substring(0, 8);
}
@BeforeEach
void createAddress() throws Exception {
address = client.create(300, runLabel, "receive");
}
@AfterEach
void cleanup() {
try { client.delete(address.id); } catch (OneShotException ignored) {}
}
@AfterAll
static void cleanupAll() {
try { client.deleteByLabel(runLabel); } catch (OneShotException ignored) {}
}
@Test
void testSignupEmail() throws Exception {
MyApp.signup(address.address, "Test User");
Email email = client.waitForEmail(address.id, Duration.ofSeconds(30));
assertTrue(email.subject.contains("Verify your account"));
}
}
Cucumber-JVM integration
pom.xml dependencies
<dependencies>
<dependency>
<groupId>com.oneshotmail</groupId>
<artifactId>oneshot-mail</artifactId>
<version>0.1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>7.15.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit-platform-engine</artifactId>
<version>7.15.0</version>
<scope>test</scope>
</dependency>
</dependencies>
Feature file: src/test/resources/features/signup.feature
Feature: User signup
Scenario: Successful signup sends verification email
Given I have a temporary email address
When I sign up with that email address
Then I should receive a verification email within 30 seconds
And the subject should contain "Verify your account"
Step definitions: src/test/java/steps/SignupSteps.java
package steps;
import com.oneshotmail.*;
import io.cucumber.java.After;
import io.cucumber.java.Before;
import io.cucumber.java.en.*;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.*;
public class SignupSteps {
private OneShotClient client;
private Address address;
private Email email;
private String runLabel;
@Before
public void setup() {
client = new OneShotClient(System.getenv("ONESHOT_API_KEY"));
runLabel = "cucumber-" + java.util.UUID.randomUUID().toString().substring(0, 8);
}
@After
public void cleanup() {
try {
if (address != null) client.delete(address.id);
} catch (OneShotException ignored) {}
}
@Given("I have a temporary email address")
public void createAddress() throws OneShotException {
address = client.create(300, runLabel, "receive");
}
@When("I sign up with that email address")
public void signup() {
// Replace with your app's signup call
MyApp.signup(address.address, "Test User");
}
@Then("I should receive a verification email within {int} seconds")
public void waitForEmail(int timeout) throws Exception {
email = client.waitForEmail(address.id, Duration.ofSeconds(timeout));
assertNotNull(email);
}
@Then("the subject should contain {string}")
public void checkSubject(String expectedText) {
assertTrue(email.subject.contains(expectedText),
"Expected subject to contain '" + expectedText + "', got: " + email.subject);
}
}
Running
mvn test
Or with the ONESHOT_API_KEY:
ONESHOT_API_KEY=osm_live_your_key mvn test
Thread safety
The OneShotClient uses java.net.http.HttpClient, which is thread-safe and designed for concurrent use. You can safely share a single client instance across tests running in parallel.