OpenEmail 0.0.1
dotnet add package OpenEmail --version 0.0.1
NuGet\Install-Package OpenEmail -Version 0.0.1
<PackageReference Include="OpenEmail" Version="0.0.1" />
<PackageVersion Include="OpenEmail" Version="0.0.1" />
<PackageReference Include="OpenEmail" />
paket add OpenEmail --version 0.0.1
#r "nuget: OpenEmail, 0.0.1"
#:package OpenEmail@0.0.1
#addin nuget:?package=OpenEmail&version=0.0.1
#tool nuget:?package=OpenEmail&version=0.0.1
OpenEmail for .NET
Email you can build on. Send mail, read the mailbox and automate a workspace from C#.
Website • Documentation • Every method • API Reference
The .NET SDK
The official .NET client for the OpenEmail API. It has one method for every method of the TypeScript SDK, under the same name in PascalCase with an Async suffix, and a parity check proves that each one sends the same request. It targets net8.0 and netstandard2.0, is safe to trim and to compile ahead of time, and on .NET 8 and later it has no dependencies at all.
It carries a workspace API key or an OAuth access token, so it belongs on a server, in a job or in a tool that runs on your own machine.
Installing
dotnet add package OpenEmail
Using
using OpenEmail;
var client = new OpenEmailClient();
await using var invoice = File.OpenRead("invoice.pdf");
var sent = await client.Emails.SendAsync(new Body
{
["from"] = "Acme Billing <billing@acme.com>",
["to"] = "ada@example.com",
["subject"] = "Your September invoice",
["html"] = "<p>Your invoice is attached.</p>",
["attachments"] = new[]
{
new Body { ["filename"] = "invoice.pdf", ["content"] = invoice, ["contentType"] = "application/pdf" },
},
});
Console.WriteLine($"{sent["id"]} {sent["status"]}");
new OpenEmailClient() reads the key from OPENEMAIL_API_KEY. Create one in OpenEmail under Settings, API keys. It is shown once, and it belongs in an environment variable or your secrets store rather than in code. Pass new OpenEmailOptions { ApiKey = ... } when your configuration lives somewhere else.
A request body is a Body, or any IReadOnlyDictionary<string, object?>, whose keys keep the API's names, so replyTo, scheduledAt and contentType stay camelCase. A Body goes out in the order its keys were added. Options are named arguments, also under the API's names: idempotencyKey:, apiKey:, limit:, cursor:. What comes back is a JsonObject, read with sent["id"].
Every method is asynchronous and takes a CancellationToken last. Build one client and share it: it is safe to use from many threads at once, and it holds no connection of its own to dispose.
Every send carries an idempotency key, generated once per call and reused by its retries, so a retried request replays the original message rather than sending a second one. Pass your own with idempotencyKey: to make that hold across processes and restarts.
Reading the mailbox
var page = await client.Threads.ListAsync(folder: "inbox", limit: 25);
foreach (var thread in page)
{
var full = await client.Threads.GetAsync(thread["id"]!.GetValue<string>());
Console.WriteLine($"{thread["id"]} {full["messageCount"]} {((bool?)full["hasUnread"] == true ? "unread" : "read")}");
}
await foreach (var thread in client.Threads.IterateAsync(folder: "inbox", query: "invoice"))
{
Console.WriteLine(thread["id"]);
}
A row of a thread list carries the thread's id, and Threads.GetAsync returns the thread with its messages.
Every paginated resource has ListAsync for one page (a Page with Items, HasMore and NextCursor, which foreach walks), ListAllAsync for every page at once as a list, and IterateAsync, which returns an IAsyncEnumerable<JsonObject> that fetches the next page only when it gets there, so a break stops early. Addresses.ListAllAsync is the exception and returns the whole address book.
Errors
try
{
await client.Templates.SendAsync("order-shipped", new Body
{
["from"] = "dispatch@acme.com",
["to"] = "ada@example.com",
["props"] = new Body { ["orderId"] = "AC-4192" },
});
}
catch (OpenEmailApiException error) when (error.IsValidation)
{
Console.Error.WriteLine($"{error.Code} {error.Param} {error.RequestId}");
throw;
}
An API refusal throws an OpenEmailApiException. It carries Status, ErrorType, Code, Param, RequestId, DocUrl, RetryAfterSeconds, Fields and Body, plus IsValidation, IsNotFound, IsRateLimited and friends to branch on in a when filter. No response at all throws OpenEmailNetworkException, with IsTimeout when the deadline passed. Both derive from OpenEmailException. A mistake in the call itself, such as a malformed key or an empty id, throws ArgumentException before anything is sent, and a cancelled token throws OperationCanceledException as it does everywhere in .NET.
Webhooks
using System.Text.Json.Nodes;
using OpenEmail;
static JsonObject? Receive(string rawBody, string? signature)
{
try
{
return OpenEmailClient.VerifyWebhookSignature(rawBody, signature, Environment.GetEnvironmentVariable("OPENEMAIL_WEBHOOK_SECRET") ?? string.Empty);
}
catch (OpenEmailWebhookException)
{
return null;
}
}
Console.WriteLine(Receive("{}", "t=0,v1=0") is null ? "rejected" : "verified");
It checks the HMAC in constant time and rejects a delivery more than five minutes old, then returns the event. Pass the raw body: a parsed or re-encoded body no longer matches its signature. The second argument is the value of the X-OpenEmail-Signature header, and there are overloads that take the request headers themselves.
Disposable inboxes
var tempMail = OpenEmailClient.CreateTempMail();
var inbox = await tempMail.CreateAsync(new Body { ["ttlMinutes"] = 60 });
var messages = await tempMail.ListMessagesAsync(inbox["id"]!.GetValue<string>(), inboxToken: (string?)inbox["token"]);
Console.WriteLine(messages.Count);
CreateAsync needs no credential and is the only call that returns the inbox token, so keep it.
OAuth access tokens
An app a person connected to OpenEmail with OAuth, such as a command line tool or an agent, holds an access token rather than an API key. Pass it as AccessToken, or pass AccessTokenProvider, a function that returns it:
var current = "token-from-your-oauth-flow";
var client = new OpenEmailClient(new OpenEmailOptions
{
AccessTokenProvider = _ => Task.FromResult(current),
});
Console.WriteLine(client.Mode);
The provider runs before every request, so renew the token there when it is close to expiring and the client never has to be rebuilt. Pass ApiKey or an access token, not both. A client reads OPENEMAIL_ACCESS_TOKEN when you pass neither and OPENEMAIL_API_KEY is not set.
A token acts for a person, so before a sensitive change, such as deleting a domain or changing a webhook, it is asked for the same verification code the web app asks for. The request throws with IsStepUpRequired. Ask for a code, check it, then make the request again:
var domainId = "b3e1f0a4-6c2d-4e8a-9f17-2d5c8a0b4e6f";
try
{
await client.Domains.DeleteAsync(domainId);
}
catch (OpenEmailApiException error) when (error.IsStepUpRequired)
{
var challenge = await client.Security.BeginStepUpAsync();
Console.WriteLine((string?)challenge["method"] == "email" ? $"Enter the code we emailed to {challenge["sentTo"]}" : "Enter the code from your authenticator app");
await client.Security.VerifyStepUpAsync(new Body { ["code"] = Console.ReadLine()?.Trim() });
await client.Domains.DeleteAsync(domainId);
}
API keys are never asked for a code.
Configuring
using var http = new HttpClient();
var client = new OpenEmailClient(new OpenEmailOptions
{
ApiKey = Environment.GetEnvironmentVariable("OPENEMAIL_API_KEY"),
BaseUrl = "https://api.openemail.uk",
Timeout = TimeSpan.FromSeconds(30),
MaxRetries = 2,
HttpClient = http,
Headers = { ["X-Team"] = "billing" },
});
Console.WriteLine(client.Raw.BaseUrl);
Anything you leave out is read from the environment: OPENEMAIL_API_KEY, then OPENEMAIL_ACCESS_TOKEN, and OPENEMAIL_BASE_URL, where a bare host such as localhost:2222 gets its scheme added.
Use an https: origin: the client refuses to send an API key, an access token or an inbox token over plain http: unless the server is on this machine, at localhost, a 127.x.x.x address or ::1. A base URL on 0.0.0.0 is refused when the client is built, since that is the address a server listens on: use 127.0.0.1 with the same port. Timeout bounds a whole attempt, TimeSpan.Zero turns it off, and uploads wait at least ten minutes. Reads, sends and every write that is safe to repeat are retried on 408, 500, 502, 503 and 504 with backoff, and on a 429 only when it carries a Retry-After of a minute or less. Any other write is never retried. Every method takes apiKey: to act with another key for one call, so one process can serve several workspaces with one client, and the TempMail methods take inboxToken: instead.
HttpClient takes the client your application already has, so it works with IHttpClientFactory, a proxy or a test handler. The SDK never disposes a client it was handed, and when you pass none it shares one for the whole process.
An endpoint no method wraps yet is one client.Raw.RequestAsync() away, with the client's credential, base URL, timeout and retry policy applied:
var label = await client.Raw.RequestAsync("/labels", "POST", body: new Body { ["name"] = "Invoices" });
Console.WriteLine(label?["id"]);
The path must begin with a single /, and a path whose finished URL leaves the base URL's origin throws before a request is sent, so the credential never reaches another host.
Everything else, from templates, rules, tracking, calendar, contacts, audiences and broadcasts to roles, members, settings, mailbox imports and provider imports, plus the full reference for every method, lives in the documentation. What changed in each release is in the changelog.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Bcl.AsyncInterfaces (>= 8.0.0)
- System.Text.Json (>= 8.0.5)
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.0.1 | 55 | 10/12/2026 |