Official .NET client for VaultSandbox email testing API with quantum-safe encryption
$ dotnet add package VaultSandbox.ClientVaultSandbox is in Public Beta. Join the journey to 1.0. Share feedback on GitHub.
Production-like email testing. Self-hosted and secure.
The official .NET SDK for VaultSandbox Gateway — a self-hosted SMTP testing platform that replicates real-world email delivery with TLS, authentication, spam analysis, chaos engineering, and zero-knowledge encryption.
Stop mocking. Test email like production.
.NET 9+ required. Not intended for Blazor WebAssembly or browser runtimes.
dotnet add package VaultSandbox.Client
Or via the NuGet Package Manager:
Install-Package VaultSandbox.Client
using VaultSandbox.Client;
// Initialize client with your API key
var client = VaultSandboxClientBuilder.Create()
.WithBaseUrl("https://smtp.vaultsandbox.com")
.WithApiKey("your-api-key")
.Build();
await using (client)
{
// Create inbox (keypair generated automatically)
var inbox = await client.CreateInboxAsync();
Console.WriteLine($"Send email to: {inbox.EmailAddress}");
// Wait for email with timeout
var email = await inbox.WaitForEmailAsync(new WaitForEmailOptions
{
Timeout = TimeSpan.FromSeconds(30),
Subject = "Test", // Optional filter
});
// Email is already decrypted - just use it!
Console.WriteLine($"From: {email.From}");
Console.WriteLine($"Subject: {email.Subject}");
Console.WriteLine($"Text: {email.Text}");
Console.WriteLine($"HTML: {email.Html}");
}
using VaultSandbox.Client;
var client = VaultSandboxClientBuilder.Create()
.WithBaseUrl(url)
.WithApiKey(apiKey)
.Build();
await using (client)
{
var inbox = await client.CreateInboxAsync();
// Trigger password reset in your app (replace with your own implementation)
await yourApp.RequestPasswordResetAsync(inbox.EmailAddress);
// Wait for and validate the reset email
var email = await inbox.WaitForEmailAsync(new WaitForEmailOptions
{
Timeout = TimeSpan.FromSeconds(10),
Subject = "Reset your password",
UseRegex = true,
});
// Extract reset link
var resetLink = email.Links?.FirstOrDefault(url => url.Contains("/reset-password"));
Console.WriteLine($"Reset link: {resetLink}");
// Validate email authentication
var authValidation = email.AuthResults?.Validate();
// In a real test, this may not pass if the sender isn't fully configured.
// A robust check verifies the validation was performed and has the correct shape.
Assert.IsNotNull(authValidation);
Assert.IsInstanceOfType<bool>(authValidation.Passed);
Assert.IsNotNull(authValidation.Failures);
}
var email = await inbox.WaitForEmailAsync(new WaitForEmailOptions
{
Timeout = TimeSpan.FromSeconds(5)
});
var validation = email.AuthResults?.Validate();
if (validation is not null && !validation.Passed)
{
Console.WriteLine("Email authentication failed:");
foreach (var reason in validation.Failures)
{
Console.WriteLine($" - {reason}");
}
}
// Or check individual results. Results can vary based on the sending source.
if (email.AuthResults?.Spf is not null)
{
Assert.IsTrue(Enum.IsDefined(email.AuthResults.Spf.Result));
}
if (email.AuthResults?.Dkim is not null)
{
Assert.IsTrue(email.AuthResults.Dkim.Count > 0);
}
if (email.AuthResults?.Dmarc is not null)
{
Assert.IsTrue(Enum.IsDefined(email.AuthResults.Dmarc.Result));
}
var email = await inbox.WaitForEmailAsync(new WaitForEmailOptions
{
Subject = "Verify your email",
UseRegex = true,
});
// All links are automatically extracted
var verifyLink = email.Links?.FirstOrDefault(url => url.Contains("/verify"));
Assert.IsNotNull(verifyLink);
Assert.IsTrue(verifyLink.Contains("https://"));
// Test the verification flow
using var httpClient = new HttpClient();
var response = await httpClient.GetAsync(verifyLink);
Assert.IsTrue(response.IsSuccessStatusCode);
Email attachments are automatically decrypted and available as byte[] arrays, ready to be processed or saved.
var email = await inbox.WaitForEmailAsync(new WaitForEmailOptions
{
Subject = "Documents Attached",
UseRegex = true,
});
// Access attachments array
Console.WriteLine($"Found {email.Attachments?.Count ?? 0} attachments");
// Iterate through attachments
if (email.Attachments is not null)
{
foreach (var attachment in email.Attachments)
{
Console.WriteLine($"Filename: {attachment.Filename}");
Console.WriteLine($"Content-Type: {attachment.ContentType}");
Console.WriteLine($"Size: {attachment.Size} bytes");
if (attachment.Content is null) continue;
// Decode text-based attachments
if (attachment.ContentType.Contains("text"))
{
var textContent = System.Text.Encoding.UTF8.GetString(attachment.Content);
Console.WriteLine($"Content: {textContent}");
}
// Parse JSON attachments
if (attachment.ContentType.Contains("json"))
{
var jsonContent = System.Text.Encoding.UTF8.GetString(attachment.Content);
var data = System.Text.Json.JsonSerializer.Deserialize<object>(jsonContent);
Console.WriteLine($"Parsed data: {data}");
}
// Save binary files to disk
if (attachment.ContentType.Contains("pdf") || attachment.ContentType.Contains("image"))
{
await File.WriteAllBytesAsync($"./downloads/{attachment.Filename}", attachment.Content);
Console.WriteLine($"Saved {attachment.Filename}");
}
}
}
// Find and verify specific attachment in tests
var pdfAttachment = email.Attachments?.FirstOrDefault(att => att.Filename == "invoice.pdf");
Assert.IsNotNull(pdfAttachment);
Assert.AreEqual("application/pdf", pdfAttachment.ContentType);
Assert.IsTrue(pdfAttachment.Size > 0);
// Verify attachment content exists and has expected size
if (pdfAttachment.Content is not null)
{
Assert.AreEqual(pdfAttachment.Size, pdfAttachment.Content.Length);
}
public class EmailFlowTests : IAsyncLifetime
{
private IVaultSandboxClient _client = null!;
private IInbox _inbox = null!;
public async Task InitializeAsync()
{
_client = VaultSandboxClientBuilder.Create()
.WithBaseUrl(url)
.WithApiKey(apiKey)
.Build();
_inbox = await _client.CreateInboxAsync();
}
public async Task DisposeAsync()
{
if (_inbox is not null)
await _inbox.DisposeAsync();
if (_client is not null)
await _client.DisposeAsync();
}
[Fact]
public async Task Should_receive_welcome_email()
{
await SendWelcomeEmailAsync(_inbox.EmailAddress);
var email = await _inbox.WaitForEmailAsync(new WaitForEmailOptions
{
Timeout = TimeSpan.FromSeconds(5),
Subject = "Welcome",
UseRegex = true,
});
Assert.Equal("noreply@example.com", email.From);
Assert.Contains("Thank you for signing up", email.Text);
}
}
[TestFixture]
public class EmailFlowTests
{
private IVaultSandboxClient _client = null!;
private IInbox _inbox = null!;
[SetUp]
public async Task SetUp()
{
_client = VaultSandboxClientBuilder.Create()
.WithBaseUrl(url)
.WithApiKey(apiKey)
.Build();
_inbox = await _client.CreateInboxAsync();
}
[TearDown]
public async Task TearDown()
{
if (_inbox is not null)
await _inbox.DisposeAsync();
if (_client is not null)
await _client.DisposeAsync();
}
[Test]
public async Task Should_receive_welcome_email()
{
await SendWelcomeEmailAsync(_inbox.EmailAddress);
var email = await _inbox.WaitForEmailAsync(new WaitForEmailOptions
{
Timeout = TimeSpan.FromSeconds(5),
Subject = "Welcome",
UseRegex = true,
});
Assert.That(email.From, Is.EqualTo("noreply@example.com"));
Assert.That(email.Text, Does.Contain("Thank you for signing up"));
}
}
When testing scenarios that send multiple emails, use WaitForEmailCountAsync() instead of arbitrary delays for faster and more reliable tests:
[Fact]
public async Task Should_receive_multiple_notification_emails()
{
// Send multiple emails
await SendNotificationsAsync(_inbox.EmailAddress, 3);
// Wait for all 3 emails to arrive
await _inbox.WaitForEmailCountAsync(3, new WaitForEmailCountOptions
{
Timeout = TimeSpan.FromSeconds(30)
});
// Now list and verify all emails
var emails = await _inbox.GetEmailsAsync();
Assert.Equal(3, emails.Count);
Assert.Contains("Notification", emails[0].Subject);
}
For scenarios where you need to process emails as they arrive without blocking, you can use the WatchAsync method which returns an IAsyncEnumerable<Email>.
using VaultSandbox.Client;
var client = VaultSandboxClientBuilder.Create()
.WithBaseUrl(url)
.WithApiKey(apiKey)
.Build();
await using (client)
{
var inbox = await client.CreateInboxAsync();
Console.WriteLine($"Watching for emails at: {inbox.EmailAddress}");
using var cts = new CancellationTokenSource();
// Process emails as they arrive
await foreach (var email in inbox.WatchAsync(cts.Token))
{
Console.WriteLine($"New email received: \"{email.Subject}\"");
// Process the email here...
}
}
var inbox1 = await client.CreateInboxAsync();
var inbox2 = await client.CreateInboxAsync();
var monitor = client.MonitorInboxes(inbox1, inbox2);
Console.WriteLine($"Monitoring inboxes: {inbox1.EmailAddress}, {inbox2.EmailAddress}");
await foreach (var evt in monitor.WatchAsync())
{
Console.WriteLine($"New email in {evt.InboxAddress}: {evt.Email.Subject}");
// Further processing...
}
Register a webhook to receive notifications when emails arrive:
var inbox = await client.CreateInboxAsync();
// Create a webhook for email.received events
var webhook = await inbox.CreateWebhookAsync(new CreateWebhookOptions
{
Url = "https://your-server.com/webhook",
Events = [WebhookEventType.EmailReceived],
Description = "Notify on new emails"
});
Console.WriteLine($"Webhook ID: {webhook.Id}");
Console.WriteLine($"Signing secret: {webhook.Secret}");
// Test the webhook
var testResult = await webhook.TestAsync();
Console.WriteLine($"Test successful: {testResult.Success}");
// List all webhooks
var webhooks = await inbox.ListWebhooksAsync();
Console.WriteLine($"Total webhooks: {webhooks.Count}");
// Clean up
await webhook.DeleteAsync();
Test how your application handles email delivery failures:
var inbox = await client.CreateInboxAsync();
// Enable latency injection (adds 1-5 second delays)
await inbox.SetChaosConfigAsync(new SetChaosConfigOptions
{
Enabled = true,
Latency = new LatencyOptions
{
Enabled = true,
MinDelayMs = 1000,
MaxDelayMs = 5000,
Probability = 1.0 // Apply to all emails
}
});
// Verify chaos is configured
var config = await inbox.GetChaosConfigAsync();
Console.WriteLine($"Chaos enabled: {config.Enabled}");
Console.WriteLine($"Latency enabled: {config.Latency?.Enabled}");
// Disable chaos when done
await inbox.DisableChaosAsync();
The SDK integrates seamlessly with the .NET dependency injection container.
Using IConfiguration:
// appsettings.json
{
"VaultSandbox": {
"BaseUrl": "https://smtp.vaultsandbox.com",
"ApiKey": "your-api-key"
}
}
// Program.cs or Startup.cs
services.AddVaultSandboxClient(configuration);
Using a configuration action:
services.AddVaultSandboxClient(options =>
{
options.BaseUrl = "https://smtp.vaultsandbox.com";
options.ApiKey = Environment.GetEnvironmentVariable("VAULTSANDBOX_API_KEY")!;
options.DefaultDeliveryStrategy = DeliveryStrategy.Sse;
});
Using the builder with service provider:
services.AddVaultSandboxClient((builder, sp) =>
{
var config = sp.GetRequiredService<IConfiguration>();
builder
.WithBaseUrl(config["VaultSandbox:BaseUrl"]!)
.WithApiKey(config["VaultSandbox:ApiKey"]!)
.WithLogging(sp.GetRequiredService<ILoggerFactory>());
});
Then inject IVaultSandboxClient where needed:
public class EmailTestService
{
private readonly IVaultSandboxClient _client;
public EmailTestService(IVaultSandboxClient client)
{
_client = client;
}
public async Task TestPasswordResetAsync()
{
var inbox = await _client.CreateInboxAsync();
// ...
}
}
The fluent builder for creating IVaultSandboxClient instances.
VaultSandboxClientBuilder.Create()
WithBaseUrl(string baseUrl) - Gateway URL (required)WithApiKey(string apiKey) - Your API key (required)WithHttpTimeout(TimeSpan timeout) - HTTP request timeout (default: 30s)WithWaitTimeout(TimeSpan timeout) - Default WaitForEmail timeout (default: 30s)WithPollInterval(TimeSpan interval) - Polling interval (default: 2s)WithMaxRetries(int maxRetries) - Max retry attempts (default: 3)WithRetryDelay(TimeSpan delay) - Initial retry delay (default: 1s)WithSseReconnectInterval(TimeSpan interval) - SSE reconnection interval (default: 5s)WithSseMaxReconnectAttempts(int maxAttempts) - Max SSE reconnection attempts (default: 10)WithDeliveryStrategy(DeliveryStrategy strategy) - Delivery strategyUseSseDelivery() - Use SSE delivery strategy (default)UsePollingDelivery() - Use polling delivery strategyWithDefaultInboxTtl(TimeSpan ttl) - Default inbox TTL (default: 1 hour)WithLogging(ILoggerFactory loggerFactory) - Configure loggingWithHttpClient(HttpClient httpClient, bool disposeClient = false) - Use custom HttpClientBuild() - Creates the clientBuildAndValidateAsync(CancellationToken ct = default) - Creates and validates the clientThe main client interface for interacting with the VaultSandbox Gateway.
CreateInboxAsync(CreateInboxOptions? options = null, CancellationToken ct = default) - Creates a new inboxDeleteInboxAsync(string emailAddress, CancellationToken ct = default) - Deletes a single inboxDeleteAllInboxesAsync(CancellationToken ct = default) - Deletes all inboxes for this API keyValidateApiKeyAsync(CancellationToken ct = default) - Validates API keyGetServerInfoAsync(CancellationToken ct = default) - Gets server informationMonitorInboxes(params IInbox[] inboxes) - Monitors multiple inboxesExportInboxToFileAsync(IInbox inbox, string filePath, CancellationToken ct = default) - Exports inbox to JSON fileImportInboxFromFileAsync(string filePath, CancellationToken ct = default) - Imports inbox from JSON fileImportInboxAsync(InboxExport export, CancellationToken ct = default) - Imports inbox from export dataRepresents a single email inbox.
EmailAddress - The inbox email addressInboxHash - Unique inbox identifierExpiresAt - When the inbox expiresIsDisposed - Whether disposedGetEmailsAsync(CancellationToken ct = default) - Lists all emails (decrypted)GetEmailAsync(string emailId, CancellationToken ct = default) - Gets a specific emailGetEmailRawAsync(string emailId, CancellationToken ct = default) - Gets raw email sourceWaitForEmailAsync(WaitForEmailOptions? options = null, CancellationToken ct = default) - Waits for an email matching criteriaWaitForEmailCountAsync(int count, WaitForEmailCountOptions? options = null, CancellationToken ct = default) - Waits until inbox has N emailsWatchAsync(CancellationToken ct = default) - Watches for new emails as IAsyncEnumerable<Email>GetEmailCountAsync(CancellationToken ct = default) - Gets current email countGetSyncStatusAsync(CancellationToken ct = default) - Gets inbox sync statusMarkAsReadAsync(string emailId, CancellationToken ct = default) - Marks email as readDeleteEmailAsync(string emailId, CancellationToken ct = default) - Deletes an emailExportAsync() - Exports inbox dataRepresents a decrypted email.
Id - Email IDInboxId - The inbox hash this email belongs toFrom - Sender addressTo - Recipient addressesSubject - Email subjectText - Plain text contentHtml - HTML contentReceivedAt - When the email was receivedIsRead - Read statusLinks - Extracted URLs from emailHeaders - All email headersAttachments - Email attachmentsAuthResults - Email authentication resultsMetadata - Other metadataMarkAsReadAsync(CancellationToken ct = default) - Marks this email as readDeleteAsync(CancellationToken ct = default) - Deletes this emailGetRawAsync(CancellationToken ct = default) - Gets raw email sourceRepresents an email attachment.
Filename - Attachment filenameContentType - MIME type (e.g., "application/pdf")Size - Size in bytesContent - Decoded binary content as byte[]ContentId - Content ID for inline attachmentsContentDisposition - "attachment" or "inline"Checksum - Optional SHA-256 checksumEmail authentication results (SPF, DKIM, DMARC).
Spf - SPF resultDkim - All DKIM resultsDmarc - DMARC resultReverseDns - Reverse DNS resultValidate() - Returns AuthValidation with Passed, per-check booleans, and Failures listOptions for creating an inbox.
EmailAddress - Optional specific email address to requestTtl - Time-to-live for the inbox (default: 1 hour)Options for waiting for emails.
Timeout - Maximum time to wait (default: 30s)PollInterval - Polling interval (default: 2s)Subject - Filter emails by subject (exact or regex)From - Filter emails by sender address (exact or regex)Predicate - Custom filter function Func<Email, bool>UseRegex - Whether to use regex matching (default: false)Options for waiting for a specific number of emails.
Timeout - Maximum time to wait (default: 30s)Monitors multiple inboxes for new emails.
Inboxes - The monitored inboxesInboxCount - Number of monitored inboxesWatchAsync(CancellationToken ct = default) - Returns IAsyncEnumerable<InboxEmailEvent>Start() - Explicitly start monitoringEvent emitted when an email arrives in a monitored inbox.
Inbox - The inbox that received the emailEmail - The received emailInboxAddress - Shortcut to inbox email addressThe SDK is designed to be resilient and provide clear feedback when issues occur. It includes automatic retries for transient network and server errors, and throws specific, catchable exceptions for different failure scenarios.
All custom exceptions thrown by the SDK inherit from the base VaultSandboxException class, so you can catch all SDK-specific errors with a single catch block if needed.
By default, the client automatically retries failed HTTP requests that result in one of the following status codes: 408, 429, 500, 502, 503, 504. This helps mitigate transient network or server-side issues.
The retry behavior can be configured via the VaultSandboxClientBuilder:
WithMaxRetries(int) - The maximum number of retry attempts (default: 3)WithRetryDelay(TimeSpan) - The base delay between retries (default: 1s). Uses exponential backoff.| Exception | Key Properties | Purpose |
|---|---|---|
VaultSandboxException | Message, InnerException | Base class for all SDK errors |
ApiException | StatusCode, ResponseBody | HTTP API errors |
VaultSandboxTimeoutException | Timeout | Operation timeouts |
DecryptionException | Message | Decryption failures (CRITICAL) |
SignatureVerificationException | Message | Signature verification failures (CRITICAL) |
EmailNotFoundException | EmailId | Email not found (404) |
InboxNotFoundException | EmailAddress | Inbox not found (404) |
InboxAlreadyExistsException | EmailAddress | Inbox already exists during import |
InvalidImportDataException | Message | Import data validation failures |
NetworkException | Message | Network connectivity issues |
SseException | Message | SSE connection issues |
using VaultSandbox.Client;
using VaultSandbox.Client.Exceptions;
var client = VaultSandboxClientBuilder.Create()
.WithBaseUrl(url)
.WithApiKey(apiKey)
.Build();
try
{
await using (client)
{
var inbox = await client.CreateInboxAsync();
Console.WriteLine($"Send email to: {inbox.EmailAddress}");
// This might throw a VaultSandboxTimeoutException
var email = await inbox.WaitForEmailAsync(new WaitForEmailOptions
{
Timeout = TimeSpan.FromSeconds(5)
});
Console.WriteLine($"Email received: {email.Subject}");
}
}
catch (VaultSandboxTimeoutException ex)
{
Console.WriteLine($"Timed out waiting for email after {ex.Timeout}: {ex.Message}");
}
catch (ApiException ex)
{
Console.WriteLine($"API Error ({ex.StatusCode}): {ex.Message}");
}
catch (VaultSandboxException ex)
{
// Catch any other SDK-specific error
Console.WriteLine($"An unexpected SDK error occurred: {ex.Message}");
}
All configuration options available when using dependency injection:
public sealed class VaultSandboxClientOptions
{
public required string BaseUrl { get; set; }
public required string ApiKey { get; set; }
public int HttpTimeoutMs { get; set; } = 30_000;
public int WaitTimeoutMs { get; set; } = 30_000;
public int PollIntervalMs { get; set; } = 2_000;
public int MaxRetries { get; set; } = 3;
public int RetryDelayMs { get; set; } = 1_000;
public int SseReconnectIntervalMs { get; set; } = 5_000;
public int SseMaxReconnectAttempts { get; set; } = 10;
public DeliveryStrategy DefaultDeliveryStrategy { get; set; } = DeliveryStrategy.Sse;
public int DefaultInboxTtlSeconds { get; set; } = 3600;
}
public enum DeliveryStrategy
{
Sse, // Server-Sent Events (default)
Polling // Polling
}
dotnet build
# Run all tests
dotnet test
# With coverage
dotnet test --collect:"XPlat Code Coverage" --results-directory ./coverage
# Generate HTML coverage report
dotnet tool install -g dotnet-reportgenerator-globaltool
~/.dotnet/tools/reportgenerator \
-reports:"./coverage/**/coverage.cobertura.xml" \
-targetdir:"./coverage/report" \
-reporttypes:"Html;TextSummary"
# View summary
cat ./coverage/report/Summary.txt
The SDK is built on several layers:
All cryptographic operations are performed transparently - developers never need to handle keys, encryption, or signatures directly.
vaultsandbox:email:v1 today).SignatureVerificationException; decryption issues throw DecryptionException. Always surface these in logs/alerts for investigation.Contributions are welcome! Please read our contributing guidelines before submitting PRs.
Apache 2.0 — see LICENSE for details.