Migrate from Mailsac
Side-by-side comparison and migration guide from Mailsac to OneShotMail.
Why switch?
| Concern | Mailsac | OneShotMail |
|---|---|---|
| Model | Catch-all domains with persistent inboxes. | One-shot addresses with auto-deletion. |
| Isolation | Shared domains risk inbox collision. | Cryptographically random, guaranteed unique. |
| Cleanup | Manual deletion or expiry rules. | Automatic TTL + bulk delete by label. |
| Sending | Limited outbound support. | Full one-shot sending with attachments. |
| SDK quality | Node.js SDK, REST API. | SDKs for Python, Go, Ruby, JS, Java. |
| wait_for_email() | Websocket/webhook available. | Built into every SDK with exponential backoff. |
| AI agent support | None. | MCP server for Claude, Cursor, Windsurf. |
| Pricing | Free tier + paid tiers. | Free tier + $10/$79 paid plans. |
Feature mapping
| Mailsac feature | OneShotMail equivalent |
|---|---|
| Catch-all domain inbox | oneshot.create() — dedicated, one-shot address |
| Custom domain | in.oneshotemail.com / out.oneshotemail.com (custom domains in future) |
GET /addresses/{email}/messages | oneshot.get_email(address_id) |
GET /addresses/{email}/messages/{id} | oneshot.get_email(address_id) |
| Websocket message stream | oneshot.wait_for_email(address_id) |
| Webhook forwarding | Coming soon. Use wait_for_email() for now. |
| Message list | oneshot.list(label="...") |
| Delete message | oneshot.delete(address_id) |
| Reserve address | oneshot.create(ttl_seconds=...) |
| Outbound SMTP | oneshot.send(to=..., subject=..., ...) |
Code comparison
Before: Mailsac (JavaScript)
const Mailsac = require("@mailsac/api");
const mailsac = new Mailsac({ headers: { "Mailsac-Key": process.env.MAILSAC_KEY } });
async function testSignup() {
const inbox = `test-${Date.now()}@my-domain.mailsac.com`;
// Trigger signup
await app.signup({ email: inbox });
// Poll for messages
let messages;
for (let i = 0; i < 30; i++) {
const { data } = await mailsac.messages.listMessages(inbox);
messages = data;
if (messages.length > 0) break;
await new Promise((r) => setTimeout(r, 2000));
}
if (!messages?.length) throw new Error("No email received");
// Fetch the message
const { data: msg } = await mailsac.messages.getFullMessage(inbox, messages[0]._id);
assert(msg.subject.includes("Verify"));
// Cleanup
await mailsac.messages.deleteAllMessages(inbox);
}
After: OneShotMail (JavaScript)
import { OneShotClient } from "oneshot-mail";
const oneshot = new OneShotClient(process.env.ONESHOT_API_KEY);
async function testSignup() {
const addr = await oneshot.create({ ttl: 300, label: "test-signup" });
await app.signup({ email: addr.address });
const email = await oneshot.waitForEmail(addr.id, { timeout: 30000 });
assert(email.subject.includes("Verify"));
// No manual cleanup needed -- TTL handles it
}
Before: Mailsac (Python via REST)
import requests
import time
def test_signup():
inbox = f"test-{int(time.time())}@my-domain.mailsac.com"
headers = {"Mailsac-Key": os.environ["MAILSAC_KEY"]}
app.signup(email=inbox)
for _ in range(30):
resp = requests.get(
f"https://mailsac.com/api/addresses/{inbox}/messages",
headers=headers,
)
messages = resp.json()
if messages:
msg_id = messages[0]["_id"]
msg_resp = requests.get(
f"https://mailsac.com/api/addresses/{inbox}/messages/{msg_id}",
headers=headers,
)
msg = msg_resp.json()
assert "Verify" in msg["subject"]
# Cleanup
requests.delete(
f"https://mailsac.com/api/addresses/{inbox}/messages",
headers=headers,
)
return
time.sleep(2)
raise TimeoutError("No email received")
After: OneShotMail (Python)
import oneshot
def test_signup():
addr = oneshot.create(ttl_seconds=300, label="test-signup")
app.signup(email=addr.address)
email = oneshot.wait_for_email(addr.id, timeout=30)
assert "Verify" in email.subject
Migration steps
1. Install the SDK
pip install oneshot-mail # Python
npm install oneshot-mail # JavaScript
gem install oneshot-mail # Ruby
go get github.com/oneshotmail/oneshot-go # Go
2. Get an API key
Register at app.oneshotemail.com.
3. Replace inbox creation
- Mailsac: Generate an email at your Mailsac domain, optionally reserve it.
- OneShotMail: Call
create()to get a unique address. No domain setup required.
4. Replace message polling
- Mailsac: List messages endpoint + polling loop (or websockets for real-time).
- OneShotMail:
wait_for_email()handles everything.
5. Replace message fetching
- Mailsac: Two-step: list messages, then fetch by message ID.
- OneShotMail: One-step:
get_email(address_id)returns the full email.
6. Replace cleanup
- Mailsac:
DELETE /addresses/{email}/messagesto clean up. - OneShotMail: Automatic via TTL. Or
delete(address_id)/delete_by_label(label)for immediate cleanup.
7. Update CI secrets
Replace MAILSAC_KEY with ONESHOT_API_KEY in your CI/CD configuration.
Gotchas
-
One email per address. Mailsac inboxes can receive multiple messages. OneShotMail addresses accept exactly one. Create a new address for each expected email.
-
No custom domains (yet). Mailsac lets you use custom catch-all domains. OneShotMail currently uses
in.oneshotemail.comfor receiving andout.oneshotemail.comfor sending. Custom domain support is on the roadmap. -
No websocket streaming. Mailsac offers websocket-based real-time notifications. OneShotMail uses polling with exponential backoff via
wait_for_email(). For most test suites, this is functionally equivalent and simpler. -
Different JSON structure. Mailsac uses
_id,subject,from, etc. OneShotMail usesid,subject,from_address(Python SDK) /from(API),text_body,html_body. Update your assertions accordingly. -
Address format. Mailsac addresses are
name@domain.mailsac.com. OneShotMail receive addresses arerandom_id@in.oneshotemail.comand send addresses arerandom_id@out.oneshotemail.com. The local part is the address ID used for all API operations.