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):

FieldTypeDefaultDescription
TTLint3600TTL in seconds.
Labelstring""Optional label for filtering.
Modestring"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):

FieldTypeDefaultDescription
Timeouttime.Duration60sMax wait time.
PollIntervaltime.Duration2sInitial 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):

FieldTypeDefaultDescription
TostringDestination address.
SubjectstringEmail subject.
TextBodystring""Plain text body.
HTMLBodystring""HTML body.
Attachments[]SendAttachmentnilAttachments.
TTLint300TTL in seconds.
Labelstring""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):

FieldTypeDefaultDescription
Statusstring""Filter by status.
Labelstring""Filter by label.
Modestring""Filter by mode.
Limitint0Max results (0 = default).
Cursorstring""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 WaitForEmail from 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.