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.
Overview
Section titled “Overview”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.
Security Warning
Section titled “Security Warning”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
Use Cases
Section titled “Use Cases”1. Test Reproducibility
Section titled “1. Test Reproducibility”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) }}2. Manual Testing
Section titled “2. Manual Testing”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:
# Use the exported inbox in a manual test scriptgo run scripts/check-inbox/main.go ./manual-test-inbox.json3. Cross-Environment Sharing
Section titled “3. Cross-Environment Sharing”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())}4. Debugging Production Issues
Section titled “4. Debugging Production Issues”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)) }}Export Methods
Section titled “Export Methods”Export to Struct
Section titled “Export to Struct”func (i *Inbox) Export() *ExportedInboxReturns 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 manuallyjsonData, _ := 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.
Export to File
Section titled “Export to File”func (c *Client) ExportInboxToFile(inbox *Inbox, filePath string) errorDirectly writes the inbox data to a JSON file with secure permissions (0600):
inbox, _ := client.CreateInbox(ctx)
// Export to fileerr := client.ExportInboxToFile(inbox, "./backups/inbox.json")if err != nil { log.Fatal(err)}Import Methods
Section titled “Import Methods”Import from Struct
Section titled “Import from Struct”func (c *Client) ImportInbox(ctx context.Context, data *ExportedInbox) (*Inbox, error)Imports inbox data from an ExportedInbox struct:
// Read and parse JSON filejsonData, _ := os.ReadFile("./backup.json")
var exportedData vaultsandbox.ExportedInboxjson.Unmarshal(jsonData, &exportedData)
inbox, err := client.ImportInbox(ctx, &exportedData)if err != nil { log.Fatal(err)}
fmt.Printf("Imported: %s\n", inbox.EmailAddress())
// Use inbox normallyemails, _ := inbox.GetEmails(ctx)Import from File
Section titled “Import from File”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 emailssubscription := inbox.OnNewEmail(func(email *vaultsandbox.Email) { fmt.Printf("New email: %s\n", email.Subject)})defer subscription.Unsubscribe()Import Validation
Section titled “Import Validation”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) }}Complete Examples
Section titled “Complete Examples”Manual Testing Workflow
Section titled “Manual Testing Workflow”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")}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}Test Debugging Workflow
Section titled “Test Debugging Workflow”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/}Cross-Environment Sync
Section titled “Cross-Environment Sync”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 {}}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) }}Best Practices
Section titled “Best Practices”1. Secure Storage
Section titled “1. Secure Storage”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)}2. Server URL Matching
Section titled “2. Server URL Matching”Imported inboxes must be used with the same server:
// Export from server AclientA, _ := vaultsandbox.New( "key-a", vaultsandbox.WithBaseURL("https://server-a.vaultsandbox.com"),)inbox, _ := clientA.CreateInbox(ctx)data := inbox.Export()
// Import must use same serverclientB, _ := 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 failclientC, _ := vaultsandbox.New( "key-c", vaultsandbox.WithBaseURL("https://server-c.vaultsandbox.com"), // Different server)
inbox, err = clientC.ImportInbox(ctx, data) // Returns error - inbox not found3. Clean Up Exported Inboxes
Section titled “3. Clean Up Exported Inboxes”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}4. Add Export Metadata
Section titled “4. Add Export Metadata”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)}Next Steps
Section titled “Next Steps”- Delivery Strategies - SSE vs Polling
- Error Handling - Handle import errors
- Client API - Client import/export methods