Ahrara 0.1.0

dotnet add package Ahrara --version 0.1.0
                    
NuGet\Install-Package Ahrara -Version 0.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Ahrara" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Ahrara" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Ahrara" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Ahrara --version 0.1.0
                    
#r "nuget: Ahrara, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Ahrara@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Ahrara&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Ahrara&version=0.1.0
                    
Install as a Cake Tool

Ahrara for .NET

Create an email address from a .NET application or test, wait for messages, and extract verification codes or links. Requires .NET 8+; install with dotnet add package Ahrara. Received emails are standard System.Net.Mail.MailMessage objects, with bodies and attachments. The SDK connects directly to the relay; you do not need to run the Ahrara CLI.

Receive one email

using AhraraSdk;
using System.Net.Mail;

await using var inbox = await Ahrara.CreateInboxAsync();
// Submit inbox.Address to your application.
using MailMessage email = await inbox.WaitForEmailAsync(cancellationToken: cancellationToken);
Console.WriteLine(email.Subject);
Console.WriteLine(email.Body);

The default wait has no time limit. Creation returns only when reception is ready, and messages received before the wait remain available. Set an explicit timeout when appropriate:

using var email = await inbox.WaitForEmailAsync(
    timeout: TimeSpan.FromSeconds(30),
    filter: new EmailFilter { Subject = "Verification" },
    cancellationToken: cancellationToken);

A finite single wait throws TimeoutException when it expires. Cancellation throws OperationCanceledException without disconnecting the inbox. Reception errors propagate as AhraraException, with a machine-readable Code. Concurrent waits on one inbox are queued and run one at a time. Disposal interrupts waits, which then throw ObjectDisposedException, and releases resources; native cleanup failures throw AhraraException.

The caller owns every returned MailMessage, including its attachments and alternate views. Dispose each message after use. Its content is held in managed memory and remains usable after closing the inbox.

For a private relay, pass InboxOptions with ServerUrl, ServerPublicKey, and optional TlsCaPath. The public key must be independently verified. ConnectTimeoutMilliseconds bounds connection setup; creation cancellation waits for that bounded attempt to finish and then cleans it up.

Receive a stream or use a callback

await foreach (var email in inbox.WaitForEmailsAsync(
    duration: TimeSpan.FromMinutes(2)).WithCancellation(cancellationToken))
{
    using (email) await ProcessEmailAsync(email);
    if (EnoughEmailsReceived()) break;
}

await inbox.ReceiveEmailsAsync(
    async (email, cancellation) =>
    {
        using (email) await ProcessEmailAsync(email, cancellation);
    },
    duration: TimeSpan.FromMinutes(2),
    cancellationToken: cancellationToken);

These APIs are lazy and process one email at a time, without prefetching. A duration is a total receiving duration, measured from first enumeration and including consumer or callback processing time. Omitting it receives indefinitely. Duration expiry ends normally; cancellation throws OperationCanceledException. Callbacks are awaited sequentially and their errors propagate. An in-progress callback is awaited before the receiver returns.

Breaking enumeration, cancelling, or a callback failure leaves the inbox usable. Delivered messages remain in local history, but receiving operations do not deliver them again. Unmatched messages remain eligible for later filters. The async iterator supports the standard C# async stream cancellation model.

Filter and extract values

var filter = new EmailFilter
{
    Sender = "accounts@example.test",
    Subject = "Verification",
    HasAttachments = false
};
using var message = await inbox.WaitForEmailAsync(filter: filter);

var code = await inbox.WaitForRegexMatchAsync(
    @"Code: (?P<code>\d{6})", timeout: TimeSpan.FromSeconds(30), filter: filter);
Console.WriteLine(code.Groups.Single(group => group.Name == "code").Value);

await foreach (var match in inbox.WaitForRegexMatchesAsync(
    new RegexQuery(@"code: (?P<code>\d+)") { IgnoreCase = true },
    duration: TimeSpan.FromMinutes(1), filter: filter).WithCancellation(cancellationToken))
{
    Console.WriteLine(match.Value);
}

var link = await inbox.WaitForXPathAsync("//a[@class='verify']/@href", filter: filter);
await foreach (var value in inbox.WaitForXPathValuesAsync(
    "//a/@href", duration: TimeSpan.FromMinutes(1), filter: filter))
{
    Console.WriteLine(value);
}

Single extraction consumes one matching email and returns the first result. Plural extraction continues across all emails during the total duration, yielding every occurrence in each email. No match leaves an email available for another filter or query. A match consumes the email once; early termination does not replay remaining occurrences from that email. Finite single waits throw TimeoutException; plural duration expiry ends normally. Cancellation leaves the inbox usable.

RegexQuery selects text (default), html, subject, or raw and configures case, multiline anchors, and dot/newline behavior. RegexMatch includes EmailId, the matching Value, and indexed/named groups. Group zero is the entire match; unmatched optional groups have a null value. Queries use the shared Rust regex syntax; look-around and backreferences are not supported.

All EmailFilter conditions combine with AND. Sender, subject, To/Cc, text, and header values use case-insensitive substrings. Recipient, envelope sender, and Message-ID match the whole value, ignoring case. Headers keys match header names case-insensitively; any repeated value may satisfy a condition. Set new EmailFilter { Regex = true, Subject = @"^(Verification|Confirm email)$" } to interpret all string conditions as case-insensitive patterns for the shared Rust engine, not System.Text.RegularExpressions.Regex. RegexQuery is case-sensitive unless IgnoreCase is set. See the shared filter rules for anchoring and limits.

An operation filter retains nonmatching stored mail. Set InboxOptions.Filter to discard nonmatching incoming mail before local storage; this global filter also combines with each operation filter. Invalid expressions and filter values raise AhraraException without consuming messages.

Reopen an identity and preserve history

var options = new InboxOptions
{
    ProfilePath = Path.GetFullPath("test-state/inbox")
};

await using (var first = await Ahrara.CreateInboxAsync(options))
{
    // Receive mail. The profile retains its address, identity, and encrypted history.
}

await using var reopened = await Ahrara.CreateInboxAsync(options);
var page = await reopened.ListEmailsAsync();

Without ProfilePath, storage is temporary and removed on disposal. With it, history and receiving state survive later executions. Protect this directory and exclude it from version control. Close its active inbox before reopening it.

inbox.ExportCredentials() returns InboxCredentials containing Address and the secret RecoveryKey. Pass it as InboxOptions.Credentials to restore that address and identity elsewhere. Credentials alone do not copy history. To import an existing identity key directly, use RecoveryKey plus optional Address in the options; do not combine these fields with Credentials. The core verifies that the address belongs to the supplied identity.

Credentials and options redact their ToString() output. Explicit serialization still contains the key: never log, commit, or share it.

Read and manage stored messages

var page = await inbox.ListEmailsAsync(limit: 20, subject: "Verification");
Directory.CreateDirectory("downloads");
foreach (var summary in page.Emails)
{
    using var email = await inbox.GetEmailAsync(summary.Id);
    var attachmentIndex = 0;
    foreach (var attachment in email.Attachments)
    {
        // Standard .NET attachment content is already available.
        // Choose the destination and overwrite policy in the application.
        var path = Path.Combine("downloads", $"{summary.Id}-{attachmentIndex++}.bin");
        await using var destination = new FileStream(path, FileMode.CreateNew);
        await attachment.ContentStream.CopyToAsync(destination);
    }
}
// Pass page.NextCursor as afterId to retrieve another page when it is not null.
await inbox.DeleteEmailAsync(emailId);
await inbox.ClearEmailsAsync();

Listing and reading do not consume mail. GetRawEmailAsync returns original message bytes; GetHtmlAsync returns sanitized HTML with remote resources blocked by default. GetEmailId recovers the storage identifier from an original message returned by this inbox, without adding headers to the message. Objects created by the caller or returned by another inbox are rejected. Attachments expose their bytes through the standard ContentStream; application code controls saving and naming. Missing messages raise AhraraException with code email_not_found. Deletion and clearing affect only this inbox's local storage.

Body contains the complete decoded plain text when present, otherwise HTML with IsBodyHtml set. Available text and HTML bodies are also exposed as AlternateViews. Headers retains repeated source headers. Structured addresses populate From, Sender, To, CC, Bcc, and ReplyToList. Inline content is included in Attachments with ContentDisposition.Inline and ContentId.

This is a projection into the standard .NET mail model: From and Sender select the first mailbox, while the full source headers remain available. It does not reproduce an arbitrary nested MIME tree or populate AlternateView.LinkedResources. Use GetRawEmailAsync when the original MIME structure and bytes matter. The standard object's HTML is the original content; use GetHtmlAsync when sanitized HTML is needed. The SDK does not render HTML.

The API follows .NET MailMessage ownership and attachment conventions.

Product Compatible and additional computed target framework versions.
.NET 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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.1.0 89 10/12/2026
0.0.1 156 10/11/2026