Go SDK
Complete guide to the OneShotMail Go SDK -- installation, API reference, testing package and testify integration.
Installation
go get github.com/oneshotmail/oneshot-go
Requires Go 1.21+.
Configuration
From environment variable
import "github.com/oneshotmail/oneshot-go"
// Reads ONESHOT_API_KEY from environment
client := oneshot.NewClient("")
Explicit API key
client := oneshot.NewClient("osm_live_your_key")
Custom configuration
client := oneshot.NewClient("osm_live_your_key").
WithBaseURL("http://localhost:4566/v1").
WithHTTPClient(&http.Client{Timeout: 10 * time.Second})
API Reference
Create(ctx, opts) (*Address, error)
Create a new one-shot email address.
Options (*CreateOptions):
| Field | Type | Default | Description |
|---|---|---|---|
TTL | int | 3600 | TTL in seconds. |
Label | string | "" | Optional label for filtering. |
Mode | string | "receive" | "receive" or "send". |
addr, err := client.Create(ctx, &oneshot.CreateOptions{
TTL: 300,
Label: "signup-test",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(addr.Address) // "abc123@in.oneshotemail.com"
fmt.Println(addr.Status) // "waiting"
Pass nil for defaults (receive mode, 1-hour TTL, no label):
addr, err := client.Create(ctx, nil)
Get(ctx, addressID) (*Address, error)
Retrieve an address and its current status.
addr, err := client.Get(ctx, "abc123xyz789def456")
if err != nil {
log.Fatal(err)
}
if addr.Email != nil {
fmt.Println("Subject:", addr.Email.Subject)
}
GetEmail(ctx, addressID) (*Email, error)
Retrieve the full email content.
email, err := client.GetEmail(ctx, "abc123xyz789def456")
if err != nil {
log.Fatal(err)
}
fmt.Println("From:", email.From)
fmt.Println("Subject:", email.Subject)
fmt.Println("Text:", email.TextBody)
fmt.Println("Attachments:", len(email.Attachments))
GetEmailRaw(ctx, addressID) (string, error)
Retrieve the raw RFC 822 email source.
raw, err := client.GetEmailRaw(ctx, "abc123xyz789def456")
// raw contains the complete email including all headers
DownloadAttachment(ctx, addressID, index) ([]byte, error)
Download a single attachment by zero-based index.
data, err := client.DownloadAttachment(ctx, "abc123xyz789def456", 0)
if err != nil {
log.Fatal(err)
}
os.WriteFile("invoice.pdf", data, 0644)
WaitForEmail(ctx, addressID, opts) (*Email, error)
Poll until an email arrives or the timeout is exceeded. Uses exponential backoff starting at PollInterval, multiplied by 1.4 per attempt, capped at 10 seconds.
Options (*WaitOptions):
| Field | Type | Default | Description |
|---|---|---|---|
Timeout | time.Duration | 60s | Max wait time. |
PollInterval | time.Duration | 2s | Initial polling interval. |
Respects context cancellation. If the context is cancelled, WaitForEmail returns immediately with the context error.
email, err := client.WaitForEmail(ctx, addr.ID, &oneshot.WaitOptions{
Timeout: 30 * time.Second,
})
if err != nil {
log.Fatal(err)
}
fmt.Println("Subject:", email.Subject)
Send(ctx, opts) (*Address, error)
Send a one-shot email from a temporary address.
Options (*SendOptions):
| Field | Type | Default | Description |
|---|---|---|---|
To | string | Destination address. | |
Subject | string | Email subject. | |
TextBody | string | "" | Plain text body. |
HTMLBody | string | "" | HTML body. |
Attachments | []SendAttachment | nil | Attachments. |
TTL | int | 300 | TTL in seconds. |
Label | string | "" | Optional label. |
addr, err := client.Send(ctx, &oneshot.SendOptions{
To: "intake@myapp.com",
Subject: "Test invoice",
TextBody: "Please process this invoice.",
Attachments: []oneshot.SendAttachment{
{
Filename: "invoice.pdf",
ContentType: "application/pdf",
ContentBase64: base64.StdEncoding.EncodeToString(pdfBytes),
},
},
})
List(ctx, opts) ([]Address, error)
List addresses with optional filtering.
Options (*ListOptions):
| Field | Type | Default | Description |
|---|---|---|---|
Status | string | "" | Filter by status. |
Label | string | "" | Filter by label. |
Mode | string | "" | Filter by mode. |
Limit | int | 0 | Max results (0 = default). |
Cursor | string | "" | Pagination cursor. |
addresses, err := client.List(ctx, &oneshot.ListOptions{
Status: "waiting",
Label: "ci-run-abc",
Limit: 10,
})
for _, addr := range addresses {
fmt.Printf("%s: %s\n", addr.ID, addr.Status)
}
Delete(ctx, addressID) error
Delete an address immediately.
err := client.Delete(ctx, "abc123xyz789def456")
DeleteByLabel(ctx, label) error
Bulk-delete all addresses with the given label.
err := client.DeleteByLabel(ctx, "ci-run-abc123")
Account(ctx) (*Account, error)
Get account details and usage.
acct, err := client.Account(ctx)
fmt.Printf("Plan: %s, Receive: %d/%d\n",
acct.Plan, acct.Usage.Receive.Used, acct.Usage.Receive.Limit)
Health(ctx) (*HealthStatus, error)
Check API health.
h, err := client.Health(ctx)
fmt.Printf("Status: %s, Region: %s\n", h.Status, h.Region)
Error handling
All API errors are returned as *APIError:
type APIError struct {
StatusCode int
Code string
Message string
UpgradeURL string // only on 402 errors
}
Use errors.As to check for API errors:
import "errors"
email, err := client.WaitForEmail(ctx, addr.ID, nil)
if err != nil {
var apiErr *oneshot.APIError
if errors.As(err, &apiErr) {
switch apiErr.StatusCode {
case 402:
log.Fatalf("Quota exceeded. Upgrade at: %s", apiErr.UpgradeURL)
case 410:
log.Fatal("Address expired before receiving email")
case 429:
log.Printf("Rate limited, retry...")
default:
log.Fatalf("API error: %s", apiErr.Message)
}
}
// Non-API errors (network, context cancellation, timeout)
log.Fatal(err)
}
Timeout errors from WaitForEmail are regular error values (not *APIError). Check the error message or use string matching:
if strings.Contains(err.Error(), "timeout") {
// Email did not arrive in time
}
testing package integration
TestMain setup
package myapp_test
import (
"context"
"os"
"testing"
"github.com/oneshotmail/oneshot-go"
)
var oneshotClient *oneshot.Client
func TestMain(m *testing.M) {
oneshotClient = oneshot.NewClient(os.Getenv("ONESHOT_API_KEY"))
code := m.Run()
os.Exit(code)
}
Subtests with t.Cleanup
func TestSignupFlow(t *testing.T) {
ctx := context.Background()
t.Run("sends verification email", func(t *testing.T) {
addr, err := oneshotClient.Create(ctx, &oneshot.CreateOptions{
TTL: 300,
Label: "test-signup-" + t.Name(),
})
if err != nil {
t.Fatal(err)
}
// Cleanup: delete the address after the test
t.Cleanup(func() {
oneshotClient.Delete(context.Background(), addr.ID)
})
// Trigger your app's signup
triggerSignup(addr.Address)
email, err := oneshotClient.WaitForEmail(ctx, addr.ID, &oneshot.WaitOptions{
Timeout: 30 * time.Second,
})
if err != nil {
t.Fatalf("Did not receive verification email: %v", err)
}
if !strings.Contains(email.Subject, "Verify your account") {
t.Errorf("Expected subject to contain 'Verify your account', got: %s", email.Subject)
}
})
}
Parallel tests
OneShotMail addresses are isolated by design, making parallel tests safe:
func TestParallelEmailFlows(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
action func(email string)
subject string
}{
{"signup", triggerSignup, "Verify your account"},
{"password_reset", triggerPasswordReset, "Reset your password"},
{"invite", triggerInvite, "You've been invited"},
}
for _, tc := range tests {
tc := tc // capture range variable
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
addr, err := oneshotClient.Create(ctx, &oneshot.CreateOptions{
TTL: 300,
Label: "parallel-" + tc.name,
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
oneshotClient.Delete(context.Background(), addr.ID)
})
tc.action(addr.Address)
email, err := oneshotClient.WaitForEmail(ctx, addr.ID, &oneshot.WaitOptions{
Timeout: 30 * time.Second,
})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(email.Subject, tc.subject) {
t.Errorf("Expected subject '%s', got '%s'", tc.subject, email.Subject)
}
})
}
}
Helper for test suites
Create a shared test helper:
// testhelper/oneshot.go
package testhelper
import (
"context"
"testing"
"time"
"github.com/oneshotmail/oneshot-go"
)
// CreateAddress creates a temporary address and registers cleanup.
func CreateAddress(t *testing.T, client *oneshot.Client, label string) *oneshot.Address {
t.Helper()
ctx := context.Background()
addr, err := client.Create(ctx, &oneshot.CreateOptions{
TTL: 300,
Label: label,
})
if err != nil {
t.Fatalf("Failed to create oneshot address: %v", err)
}
t.Cleanup(func() {
client.Delete(context.Background(), addr.ID)
})
return addr
}
// WaitForEmail waits for an email with a default timeout and fails the test on error.
func WaitForEmail(t *testing.T, client *oneshot.Client, addressID string) *oneshot.Email {
t.Helper()
ctx := context.Background()
email, err := client.WaitForEmail(ctx, addressID, &oneshot.WaitOptions{
Timeout: 30 * time.Second,
})
if err != nil {
t.Fatalf("Failed to receive email at %s: %v", addressID, err)
}
return email
}
testify integration
package myapp_test
import (
"context"
"testing"
"time"
"github.com/oneshotmail/oneshot-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type SignupSuite struct {
suite.Suite
client *oneshot.Client
ctx context.Context
}
func (s *SignupSuite) SetupSuite() {
s.client = oneshot.NewClient("") // reads from ONESHOT_API_KEY
s.ctx = context.Background()
}
func (s *SignupSuite) TestVerificationEmail() {
addr, err := s.client.Create(s.ctx, &oneshot.CreateOptions{
TTL: 300,
Label: "suite-signup",
})
require.NoError(s.T(), err)
s.T().Cleanup(func() {
s.client.Delete(context.Background(), addr.ID)
})
triggerSignup(addr.Address)
email, err := s.client.WaitForEmail(s.ctx, addr.ID, &oneshot.WaitOptions{
Timeout: 30 * time.Second,
})
require.NoError(s.T(), err)
assert.Contains(s.T(), email.Subject, "Verify your account")
assert.NotEmpty(s.T(), email.TextBody)
}
func TestSignupSuite(t *testing.T) {
suite.Run(t, new(SignupSuite))
}
Context cancellation
All Go SDK methods accept a context.Context. Use it for:
- Timeouts:
context.WithTimeout(ctx, 10*time.Second) - Cancellation: Cancel long-running
WaitForEmailfrom a signal handler. - Tracing: Propagate trace IDs through the context.
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
email, err := client.WaitForEmail(ctx, addr.ID, nil)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
log.Println("Timed out waiting for email")
}
}
Thread safety
The *Client is safe for concurrent use from multiple goroutines. The underlying *http.Client is shared and designed for concurrent access.