Migrate from Mailinator
Side-by-side comparison and step-by-step migration guide from Mailinator to OneShotMail.
Why switch?
| Concern | Mailinator | OneShotMail |
|---|---|---|
| Public inboxes | Free tier uses shared, public inboxes. | Every address is private and cryptographically random. |
| Pricing | $79-$495+/month. | Free tier + $10-$79/month paid plans. |
| Test isolation | Shared inboxes. Risk of cross-contamination. | One-shot guarantee: each address gets one email. |
| Sending | No outbound send API. | Built-in one-shot sending. |
| SDK quality | REST API, no official SDKs. | Official SDKs for Python, Go, Ruby, JS, Java. |
| wait_for_email() | Not available. Roll your own polling. | Built into every SDK with exponential backoff. |
| Cleanup | Manual. Inboxes persist. | Automatic via TTL + bulk delete by label. |
| AI agent support | None. | MCP server for Claude, Cursor, Windsurf. |
Feature mapping
| Mailinator feature | OneShotMail equivalent |
|---|---|
| Public inbox | oneshot.create() — private, isolated address |
| Private inbox (paid) | oneshot.create() — all addresses are private |
| Inbox fetch via API | oneshot.get_email(address_id) |
| Team domains | Labels for grouping. Custom domains in future. |
| Message list | oneshot.list(label="...") |
| Delete message | oneshot.delete(address_id) |
| Rules engine | Not needed — one-shot guarantee handles routing. |
| Webhooks (paid) | Coming soon. Use wait_for_email() for now. |
| SMS testing | Not supported (different product focus). |
Code comparison
Before: Mailinator (Python, manual HTTP)
import requests
import time
MAILINATOR_API_KEY = "your-mailinator-key"
DOMAIN = "your-domain.testinator.com"
def test_signup():
# Create a random inbox name (no real isolation)
inbox = f"test-{int(time.time())}"
email = f"{inbox}@{DOMAIN}"
# Trigger signup
app.signup(email=email)
# Poll for email (no SDK, manual implementation)
for _ in range(30):
resp = requests.get(
f"https://mailinator.com/api/v2/domains/{DOMAIN}/inboxes/{inbox}",
headers={"Authorization": MAILINATOR_API_KEY},
)
messages = resp.json().get("msgs", [])
if messages:
# Fetch the first message
msg_id = messages[0]["id"]
msg_resp = requests.get(
f"https://mailinator.com/api/v2/domains/{DOMAIN}/inboxes/{inbox}/messages/{msg_id}",
headers={"Authorization": MAILINATOR_API_KEY},
)
msg = msg_resp.json()
assert "Verify" in msg["subject"]
return
time.sleep(2)
raise TimeoutError("No email received")
After: OneShotMail (Python)
import oneshot
def test_signup():
# Create an isolated, private address
addr = oneshot.create(ttl_seconds=300, label="test-signup")
# Trigger signup
app.signup(email=addr.address)
# Wait for email (built-in polling with backoff)
email = oneshot.wait_for_email(addr.id, timeout=30)
assert "Verify" in email.subject
Lines of code: 25 —> 7. Dependencies: requests + manual polling —> oneshot-mail with built-in everything.
Before: Mailinator (Ruby, manual HTTP)
require "faraday"
require "json"
def test_signup
inbox = "test-#{Time.now.to_i}"
email = "#{inbox}@your-domain.testinator.com"
MyApp.signup(email: email)
# Poll for email
30.times do
resp = Faraday.get(
"https://mailinator.com/api/v2/domains/your-domain.testinator.com/inboxes/#{inbox}",
nil,
{ "Authorization" => ENV["MAILINATOR_API_KEY"] }
)
messages = JSON.parse(resp.body)["msgs"]
if messages&.any?
msg_id = messages.first["id"]
# ... fetch full message ...
return
end
sleep 2
end
raise "Timeout"
end
After: OneShotMail (Ruby)
require "oneshot"
def test_signup
client = OneShot::Client.new
addr = client.create(ttl: 300, label: "test-signup")
MyApp.signup(email: addr["address"])
email = client.wait_for_email(addr["id"], timeout: 30)
expect(email["subject"]).to include("Verify")
end
Migration steps
1. Install the SDK
pip install oneshot-mail # Python
# or
gem install oneshot-mail # Ruby
# or
npm install oneshot-mail # JavaScript
# or
go get github.com/oneshotmail/oneshot-go # Go
2. Get an API key
Sign up at app.oneshotemail.com. Free tier for evaluation; Solo plan ($10/month) for production test suites.
3. Replace inbox creation
- Mailinator: Generate a random inbox name string.
- OneShotMail: Call
create()which returns a guaranteed-unique, cryptographically random address.
4. Replace polling logic
- Mailinator: Custom polling loop with
time.sleep(). - OneShotMail:
wait_for_email()handles polling, backoff, timeout, and error handling.
5. Replace email fetching
- Mailinator: Separate calls to list messages, then fetch individual message by ID.
- OneShotMail:
get_email()returns the full parsed email in one call.
6. Add labels for cleanup
Tag your addresses with the CI run ID:
addr = oneshot.create(label=f"ci-{os.environ['BUILD_ID']}")
7. Add cleanup
In your test teardown:
oneshot.delete_by_label(f"ci-{os.environ['BUILD_ID']}")
8. Update CI secrets
Replace MAILINATOR_API_KEY with ONESHOT_API_KEY in your CI/CD secrets.
Gotchas
-
One email per address. Mailinator inboxes can hold multiple messages. OneShotMail addresses accept exactly one email. If your test triggers multiple emails to the same address, create multiple addresses.
-
No persistent inboxes. Mailinator inboxes persist (on paid plans). OneShotMail addresses auto-delete. If you need to reference old emails, save the content before the TTL expires.
-
TTL is mandatory. Every OneShotMail address has a TTL. Mailinator inboxes persist until manually deleted. Set your TTL with enough margin for your test to complete.
-
Different email format. Mailinator returns email in its own JSON format. OneShotMail returns a different JSON structure. Update your assertions to use the OneShotMail field names (
text_body,html_body,from, etc.). -
No shared/public inboxes. If you were using Mailinator’s public inboxes (free tier), those are intentionally not replicated. Every OneShotMail address is private.