Skip to content

Inbox Import/Export

VaultSandbox allows you to export and import inboxes, including their encryption keys and metadata. This enables advanced workflows like test reproducibility, manual testing, cross-environment sharing, and debugging.

When you export an inbox, you get an ExportedInbox struct containing:

  • Export format version
  • Email address
  • Inbox identifier (hash)
  • Expiration time
  • Secret encryption key (sensitive!)
  • Server public signing key
  • Export timestamp

The public key is not included in the export as it can be derived from the secret key.

This exported data can be imported into another client instance, allowing you to access the same inbox from different environments or at different times.

Exported inbox data contains private encryption keys. Anyone with this data can:

  • Read all emails in the inbox
  • Impersonate the inbox to receive new emails
  • Decrypt all future emails sent to the inbox

Never:

  • Commit exported data to version control
  • Share exported data over insecure channels
  • Store exported data in plaintext in production

Always:

  • Treat exported data as sensitive credentials
  • Encrypt exported files at rest
  • Use secure channels for sharing
  • Rotate/delete inboxes after use

Export an inbox at the end of a test run to reproduce issues later:

package mytest
import (
"context"
"encoding/json"
"os"
"testing"
"time"
vaultsandbox "github.com/vaultsandbox/client-go"
)
func TestEmailFlow(t *testing.T) {
ctx := context.Background()
client, err := vaultsandbox.New(os.Getenv("VAULTSANDBOX_API_KEY"))
if err != nil {
t.Fatal(err)
}
defer client.Close()
inbox, err := client.CreateInbox(ctx)
if err != nil {
t.Fatal(err)
}
// Ensure cleanup, but export on failure
defer func() {
if t.Failed() {
// Export on test failure
exportData := inbox.Export()
jsonData, _ := json.MarshalIndent(exportData, "", " ")
filename := fmt.Sprintf("./debug/inbox-%d.json", time.Now().Unix())
os.WriteFile(filename, jsonData, 0600)
t.Logf("Inbox exported to %s", filename)
}
inbox.Delete(ctx)
}()
// Send welcome email to inbox
sendWelcomeEmail(inbox.EmailAddress())
email, err := inbox.WaitForEmail(ctx,
vaultsandbox.WithSubjectRegex(regexp.MustCompile(`Welcome`)),
vaultsandbox.WithWaitTimeout(10*time.Second),
)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(email.Subject, "Welcome") {
t.Errorf("expected subject to contain 'Welcome', got %s", email.Subject)
}
}

Export an inbox from automated tests for manual verification:

package main
import (
"context"
"fmt"
"os"
vaultsandbox "github.com/vaultsandbox/client-go"
)
func main() {
ctx := context.Background()
client, err := vaultsandbox.New(os.Getenv("VAULTSANDBOX_API_KEY"))
if err != nil {
panic(err)
}
defer client.Close()
inbox, err := client.CreateInbox(ctx)
if err != nil {
panic(err)
}
// Export for manual testing
if err := client.ExportInboxToFile(inbox, "./manual-test-inbox.json"); err != nil {
panic(err)
}
fmt.Printf("Manual test inbox: %s\n", inbox.EmailAddress())
fmt.Println("Exported to: ./manual-test-inbox.json")
// Continue with automated tests...
}

Then manually inspect:

Terminal window
# Use the exported inbox in a manual test script
go run scripts/check-inbox/main.go ./manual-test-inbox.json

Export an inbox from one environment and import it in another:

package main
import (
"context"
"encoding/json"
"fmt"
"os"
vaultsandbox "github.com/vaultsandbox/client-go"
)
func exportInDev() {
ctx := context.Background()
devClient, err := vaultsandbox.New(
os.Getenv("DEV_API_KEY"),
vaultsandbox.WithBaseURL("https://dev.vaultsandbox.com"),
)
if err != nil {
panic(err)
}
defer devClient.Close()
inbox, err := devClient.CreateInbox(ctx)
if err != nil {
panic(err)
}
exportData := inbox.Export()
// Save to shared location
jsonData, _ := json.MarshalIndent(exportData, "", " ")
os.WriteFile("./shared/staging-inbox.json", jsonData, 0600)
fmt.Printf("Exported: %s\n", inbox.EmailAddress())
}
func importInStaging() {
ctx := context.Background()
// Note: Must use same server URL!
stagingClient, err := vaultsandbox.New(
os.Getenv("STAGING_API_KEY"),
vaultsandbox.WithBaseURL("https://dev.vaultsandbox.com"),
)
if err != nil {
panic(err)
}
defer stagingClient.Close()
inbox, err := stagingClient.ImportInboxFromFile(ctx, "./shared/staging-inbox.json")
if err != nil {
panic(err)
}
fmt.Printf("Imported inbox: %s\n", inbox.EmailAddress())
}

Export a problematic inbox from production for local debugging:

package main
import (
"context"
"fmt"
"os"
vaultsandbox "github.com/vaultsandbox/client-go"
)
func debugProductionIssue() {
ctx := context.Background()
// Local development: Import and investigate
localClient, err := vaultsandbox.New(
os.Getenv("LOCAL_API_KEY"),
vaultsandbox.WithBaseURL("https://smtp.vaultsandbox.com"), // Same server as production
)
if err != nil {
panic(err)
}
defer localClient.Close()
inbox, err := localClient.ImportInboxFromFile(ctx, "./production-issue-123.json")
if err != nil {
panic(err)
}
// Check emails
emails, err := inbox.GetEmails(ctx)
if err != nil {
panic(err)
}
fmt.Printf("Found %d emails\n", len(emails))
for _, email := range emails {
fmt.Println("\n---")
fmt.Printf("Subject: %s\n", email.Subject)
fmt.Printf("From: %s\n", email.From)
fmt.Printf("Received: %s\n", email.ReceivedAt.Format(time.RFC3339))
fmt.Printf("Links: %d\n", len(email.Links))
fmt.Printf("Attachments: %d\n", len(email.Attachments))
}
}
func (i *Inbox) Export() *ExportedInbox

Returns an ExportedInbox struct with the inbox data:

inbox, _ := client.CreateInbox(ctx)
data := inbox.Export()
fmt.Printf("Email: %s\n", data.EmailAddress)
fmt.Printf("Expires: %s\n", data.ExpiresAt.Format(time.RFC3339))
fmt.Printf("Exported: %s\n", data.ExportedAt.Format(time.RFC3339))
// Save to file manually
jsonData, _ := json.MarshalIndent(data, "", " ")
os.WriteFile("inbox.json", jsonData, 0600)

The ExportedInbox struct contains:

type ExportedInbox struct {
Version int `json:"version"` // Export format version (must be 1)
EmailAddress string `json:"emailAddress"` // Inbox email address
ExpiresAt time.Time `json:"expiresAt"` // Inbox expiration timestamp
InboxHash string `json:"inboxHash"` // Unique inbox identifier
ServerSigPk string `json:"serverSigPk"` // Server's ML-DSA-65 public key (base64url)
SecretKey string `json:"secretKey"` // ML-KEM-768 secret key (base64url)
ExportedAt time.Time `json:"exportedAt"` // Export timestamp (informational)
}

The public key is derived from the secret key during import, so it is not included in the export format.

func (c *Client) ExportInboxToFile(inbox *Inbox, filePath string) error

Directly writes the inbox data to a JSON file with secure permissions (0600):

inbox, _ := client.CreateInbox(ctx)
// Export to file
err := client.ExportInboxToFile(inbox, "./backups/inbox.json")
if err != nil {
log.Fatal(err)
}
func (c *Client) ImportInbox(ctx context.Context, data *ExportedInbox) (*Inbox, error)

Imports inbox data from an ExportedInbox struct:

// Read and parse JSON file
jsonData, _ := os.ReadFile("./backup.json")
var exportedData vaultsandbox.ExportedInbox
json.Unmarshal(jsonData, &exportedData)
inbox, err := client.ImportInbox(ctx, &exportedData)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Imported: %s\n", inbox.EmailAddress())
// Use inbox normally
emails, _ := inbox.GetEmails(ctx)
func (c *Client) ImportInboxFromFile(ctx context.Context, filePath string) (*Inbox, error)

Directly imports an inbox from a JSON file:

inbox, err := client.ImportInboxFromFile(ctx, "./backups/inbox.json")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Imported: %s\n", inbox.EmailAddress())
// Monitor for new emails
subscription := inbox.OnNewEmail(func(email *vaultsandbox.Email) {
fmt.Printf("New email: %s\n", email.Subject)
})
defer subscription.Unsubscribe()

The SDK validates imported data and returns errors for invalid imports:

import (
"errors"
vaultsandbox "github.com/vaultsandbox/client-go"
)
inbox, err := client.ImportInbox(ctx, data)
if err != nil {
if errors.Is(err, vaultsandbox.ErrInvalidImportData) {
fmt.Println("Invalid import data:", err)
// Possible causes:
// - Unsupported export format version
// - Missing or invalid required fields
// - Invalid encryption key size or encoding
// - Corrupted JSON
} else if errors.Is(err, vaultsandbox.ErrInboxAlreadyExists) {
fmt.Println("Inbox already imported in this client")
// The inbox is already available in this client instance
} else if errors.Is(err, vaultsandbox.ErrInboxNotFound) {
fmt.Println("Inbox no longer exists on server")
// The inbox may have expired or been deleted
} else {
fmt.Println("Import failed:", err)
}
}
cmd/export-test-inbox/main.go
package main
import (
"context"
"fmt"
"os"
vaultsandbox "github.com/vaultsandbox/client-go"
)
func main() {
ctx := context.Background()
client, err := vaultsandbox.New(os.Getenv("VAULTSANDBOX_API_KEY"))
if err != nil {
panic(err)
}
defer client.Close()
inbox, err := client.CreateInbox(ctx)
if err != nil {
panic(err)
}
fmt.Printf("Created test inbox: %s\n", inbox.EmailAddress())
fmt.Printf("Expires at: %s\n", inbox.ExpiresAt().Format(time.RFC3339))
// Export for manual use
if err := client.ExportInboxToFile(inbox, "./tmp/test-inbox.json"); err != nil {
panic(err)
}
fmt.Println("Exported to: ./tmp/test-inbox.json")
fmt.Println("\nSend test emails to this address, then run:")
fmt.Println(" go run cmd/check-test-inbox/main.go")
}
cmd/check-test-inbox/main.go
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
vaultsandbox "github.com/vaultsandbox/client-go"
)
func main() {
ctx := context.Background()
client, err := vaultsandbox.New(os.Getenv("VAULTSANDBOX_API_KEY"))
if err != nil {
panic(err)
}
defer client.Close()
// Import the test inbox
inbox, err := client.ImportInboxFromFile(ctx, "./tmp/test-inbox.json")
if err != nil {
panic(err)
}
fmt.Printf("Monitoring: %s\n\n", inbox.EmailAddress())
// Show existing emails
emails, err := inbox.GetEmails(ctx)
if err != nil {
panic(err)
}
fmt.Printf("Found %d existing emails:\n\n", len(emails))
for i, email := range emails {
fmt.Printf("%d. \"%s\" from %s\n", i+1, email.Subject, email.From)
fmt.Printf(" Received: %s\n", email.ReceivedAt.Format(time.RFC1123))
fmt.Printf(" Links: %d\n\n", len(email.Links))
}
// Monitor for new emails
fmt.Println("Waiting for new emails (Ctrl+C to exit)...\n")
subscription := inbox.OnNewEmail(func(email *vaultsandbox.Email) {
fmt.Println("New email received!")
fmt.Printf(" Subject: %s\n", email.Subject)
fmt.Printf(" From: %s\n", email.From)
fmt.Printf(" Received: %s\n\n", email.ReceivedAt.Format(time.RFC1123))
})
defer subscription.Unsubscribe()
// Wait for interrupt signal
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
}
testutil/inbox_helper.go
package testutil
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"testing"
"time"
vaultsandbox "github.com/vaultsandbox/client-go"
)
// TestInbox wraps an inbox with automatic export on test failure.
type TestInbox struct {
*vaultsandbox.Inbox
t *testing.T
client *vaultsandbox.Client
exportDir string
}
// NewTestInbox creates a new inbox that exports on test failure.
func NewTestInbox(t *testing.T, client *vaultsandbox.Client, exportDir string) *TestInbox {
ctx := context.Background()
inbox, err := client.CreateInbox(ctx)
if err != nil {
t.Fatal(err)
}
ti := &TestInbox{
Inbox: inbox,
t: t,
client: client,
exportDir: exportDir,
}
t.Cleanup(func() {
if t.Failed() {
ti.exportForDebugging()
}
inbox.Delete(context.Background())
})
return ti
}
func (ti *TestInbox) exportForDebugging() {
os.MkdirAll(ti.exportDir, 0755)
filename := fmt.Sprintf("inbox-%s-%d.json",
ti.t.Name(),
time.Now().Unix(),
)
filepath := filepath.Join(ti.exportDir, filename)
if err := ti.client.ExportInboxToFile(ti.Inbox, filepath); err != nil {
ti.t.Logf("Failed to export inbox: %v", err)
return
}
ti.t.Logf("Exported failed test inbox to: %s", filepath)
}

Usage in tests:

func TestEmailWorkflow(t *testing.T) {
client, _ := vaultsandbox.New(os.Getenv("VAULTSANDBOX_API_KEY"))
defer client.Close()
inbox := testutil.NewTestInbox(t, client, "./debug")
// Test code here...
// If test fails, inbox is automatically exported to ./debug/
}
cmd/sync-inbox-to-staging/main.go
package main
import (
"context"
"encoding/json"
"fmt"
"os"
vaultsandbox "github.com/vaultsandbox/client-go"
)
func main() {
ctx := context.Background()
// Export from development
devClient, err := vaultsandbox.New(
os.Getenv("DEV_API_KEY"),
vaultsandbox.WithBaseURL("https://dev.vaultsandbox.com"),
)
if err != nil {
panic(err)
}
defer devClient.Close()
devInbox, err := devClient.CreateInbox(ctx)
if err != nil {
panic(err)
}
fmt.Printf("Created dev inbox: %s\n", devInbox.EmailAddress())
// Export
exportData := devInbox.Export()
exportPath := "./tmp/staging-sync.json"
jsonData, _ := json.MarshalIndent(exportData, "", " ")
os.WriteFile(exportPath, jsonData, 0600)
fmt.Printf("Exported to: %s\n", exportPath)
fmt.Println("\nRun in staging environment:")
fmt.Println(" go run cmd/import-from-dev/main.go")
// Keep inbox alive
fmt.Println("\nInbox will remain active for manual testing...")
fmt.Println("Press Ctrl+C to exit")
select {}
}
cmd/import-from-dev/main.go
package main
import (
"context"
"fmt"
"os"
vaultsandbox "github.com/vaultsandbox/client-go"
)
func main() {
ctx := context.Background()
stagingClient, err := vaultsandbox.New(
os.Getenv("STAGING_API_KEY"),
vaultsandbox.WithBaseURL("https://dev.vaultsandbox.com"), // Same server!
)
if err != nil {
panic(err)
}
defer stagingClient.Close()
inbox, err := stagingClient.ImportInboxFromFile(ctx, "./tmp/staging-sync.json")
if err != nil {
panic(err)
}
fmt.Printf("Imported inbox: %s\n", inbox.EmailAddress())
fmt.Println("Checking for emails...\n")
emails, _ := inbox.GetEmails(ctx)
for _, email := range emails {
fmt.Printf("- %s (%s)\n", email.Subject, email.From)
}
}

Never store exported data in plaintext. Use encryption:

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/json"
"io"
)
func exportInboxSecurely(inbox *vaultsandbox.Inbox, password string) ([]byte, error) {
data := inbox.Export()
jsonData, err := json.Marshal(data)
if err != nil {
return nil, err
}
// Derive key from password
key := sha256.Sum256([]byte(password))
block, err := aes.NewCipher(key[:])
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
return gcm.Seal(nonce, nonce, jsonData, nil), nil
}
func importInboxSecurely(encryptedData []byte, password string, client *vaultsandbox.Client, ctx context.Context) (*vaultsandbox.Inbox, error) {
// Derive key from password
key := sha256.Sum256([]byte(password))
block, err := aes.NewCipher(key[:])
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
nonce, ciphertext := encryptedData[:nonceSize], encryptedData[nonceSize:]
jsonData, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, err
}
var data vaultsandbox.ExportedInbox
if err := json.Unmarshal(jsonData, &data); err != nil {
return nil, err
}
return client.ImportInbox(ctx, &data)
}

Imported inboxes must be used with the same server:

// Export from server A
clientA, _ := vaultsandbox.New(
"key-a",
vaultsandbox.WithBaseURL("https://server-a.vaultsandbox.com"),
)
inbox, _ := clientA.CreateInbox(ctx)
data := inbox.Export()
// Import must use same server
clientB, _ := vaultsandbox.New(
"key-b", // Different API key is OK
vaultsandbox.WithBaseURL("https://server-a.vaultsandbox.com"), // Same server
)
inbox, err := clientB.ImportInbox(ctx, data) // Works
// Wrong server will fail
clientC, _ := vaultsandbox.New(
"key-c",
vaultsandbox.WithBaseURL("https://server-c.vaultsandbox.com"), // Different server
)
inbox, err = clientC.ImportInbox(ctx, data) // Returns error - inbox not found

Delete inboxes when done to avoid quota issues:

func debugWithImportedInbox(ctx context.Context, client *vaultsandbox.Client, filepath string) error {
inbox, err := client.ImportInboxFromFile(ctx, filepath)
if err != nil {
return err
}
defer inbox.Delete(ctx) // Clean up when done
// Debug...
emails, err := inbox.GetEmails(ctx)
if err != nil {
return err
}
fmt.Printf("Found %d emails\n", len(emails))
return nil
}

The ExportedInbox struct includes a built-in Version field and ExportedAt timestamp. For additional tracking, wrap exports with custom metadata:

type ExportWithMetadata struct {
ExportedBy string `json:"exportedBy"`
Environment string `json:"environment"`
Notes string `json:"notes,omitempty"`
Inbox *vaultsandbox.ExportedInbox `json:"inbox"`
}
func exportWithMetadata(inbox *vaultsandbox.Inbox, notes string) *ExportWithMetadata {
return &ExportWithMetadata{
ExportedBy: os.Getenv("USER"),
Environment: os.Getenv("GO_ENV"),
Notes: notes,
Inbox: inbox.Export(),
}
}
func importWithMetadata(ctx context.Context, client *vaultsandbox.Client, data *ExportWithMetadata) (*vaultsandbox.Inbox, error) {
fmt.Printf("Import from: %s\n", data.ExportedBy)
fmt.Printf("Environment: %s\n", data.Environment)
fmt.Printf("Exported at: %s\n", data.Inbox.ExportedAt.Format(time.RFC3339))
return client.ImportInbox(ctx, data.Inbox)
}