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.

ParameterTypeDescription
ttlSecondsintTTL in seconds (0 defaults to 3600).
labelStringOptional label (null for none).
modeString"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).

ParameterTypeDescription
addressIdStringThe address ID.
timeoutDurationMax 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.