Skip to content

Email

The Email module provides a provider-agnostic email sending abstraction with typed models, batch sending, effects composition, and built-in test doubles. Providers like SES implement IEmailService; the runtime ships with an in-memory fallback.

Quick Start

using Deepstaging.Email;

// Send a single email
var result = await emailService.SendAsync(new EmailMessage
{
    From = new EmailAddress("noreply@example.com"),
    To = [new EmailAddress("user@example.com")],
    Subject = "Welcome!",
    HtmlBody = "<h1>Welcome to our platform</h1>",
    TextBody = "Welcome to our platform"
});

// Check result
if (result.Status == EmailStatus.Accepted)
    Console.WriteLine($"Sent: {result.ProviderMessageId}");

Features

Feature Description
IEmailService Core abstraction — SendAsync, SendBatchAsync
EmailMessage Rich model — From, To, Cc, Bcc, ReplyTo, Subject, HtmlBody, TextBody, Headers, Attachments, Tags, Metadata
EmailResult Result with EmailMessageId, ProviderMessageId, EmailStatus, error details
Batch sending SendBatchAsync for bulk delivery with per-message results
Webhook events EmailBounced, EmailClicked, EmailComplained, EmailDelivered, EmailOpened
Effects module EmailModule[EffectsModule(typeof(IEmailService))] for Eff<RT, T> composition
TypedIds EmailAddress, EmailMessageId
Test double TestEmailService with call recording and seedable state
InMemory fallback InMemoryEmailService registered by default via TryAddSingleton

Core Interface

public interface IEmailService
{
    Task<EmailResult> SendAsync(EmailMessage message, CancellationToken ct = default);
    Task<IReadOnlyList<EmailResult>> SendBatchAsync(
        IReadOnlyList<EmailMessage> messages, CancellationToken ct = default);
}

Effects Composition

// EmailModule generates Eff<RT, T> effect methods
from result in EmailModule.Email.SendAsync<AppRuntime>(new EmailMessage
{
    From = new EmailAddress("noreply@example.com"),
    To = [new EmailAddress("user@example.com")],
    Subject = "Order Confirmation",
    HtmlBody = html
})
select result;

For multi-step composition, error handling, and cross-module pipelines, see Effects Composition.

Models

EmailAttachment

Property Type Description
FileName string File name (e.g., "report.pdf")
ContentType string MIME type (e.g., "application/pdf")
Content ReadOnlyMemory<byte> File content
Disposition string "attachment" (default) or "inline"
ContentId string? For inline images — referenced as cid:{id} in HTML

EmailHeader

public sealed record EmailHeader(string Name, string Value);
// e.g., new EmailHeader("List-Unsubscribe", "<mailto:unsub@example.com>")

EmailStatus

Value Description
Accepted Provider accepted for delivery
Delivered Confirmed delivered to recipient's mail server
BouncedHard Permanent failure — address invalid
BouncedSoft Temporary failure — mailbox full, server down
Complained Recipient marked as spam
Rejected Provider rejected (quota, policy)
Failed Provider error

BounceType

Value Description
Hard Permanent — address invalid, domain doesn't exist
Soft Temporary — mailbox full, server down, rate limit

Webhook Events

Email providers deliver status updates via webhooks. These event records are used with the integration events system:

Event When
EmailDelivered Provider confirmed delivery
EmailBounced Hard or soft bounce (BounceType)
EmailComplained Recipient marked as spam
EmailOpened Recipient opened the email
EmailClicked Recipient clicked a link

Templating

Use Scriban templates to separate email content from handler logic. Define HTML and plain-text templates with typed models — variables are validated at compile time.

<!-- Templates/Email/OrderConfirmation.scriban-html -->
<h1>Thanks for your order, {{ customer_name }}!</h1>
<p>Order <strong>#{{ order_id }}</strong> — {{ item_count }} items totalling ${{ total | math.format "0.00" }}</p>
<table>
{{ for item in items }}
  <tr><td>{{ item.name }}</td><td>{{ item.quantity }}</td><td>${{ item.price | math.format "0.00" }}</td></tr>
{{ end }}
</table>
<!-- Templates/Email/OrderConfirmation.scriban-txt -->
Thanks for your order, {{ customer_name }}!
Order #{{ order_id }} — {{ item_count }} items totalling ${{ total | math.format "0.00" }}

{{ for item in items }}
  - {{ item.name }} x{{ item.quantity }} — ${{ item.price | math.format "0.00" }}
{{ end }}
var model = new OrderConfirmationModel(order.CustomerName, order.Id, order.Items, order.Total);
var html = Template.Render("Email/OrderConfirmation", model);
var text = Template.RenderText("Email/OrderConfirmation", model);

await emailService.SendAsync(new EmailMessage
{
    From = new EmailAddress("orders@example.com"),
    To = [new EmailAddress(order.CustomerEmail)],
    Subject = $"Order #{order.Id} Confirmed",
    HtmlBody = html,
    TextBody = text
});

For syntax reference, best practices, and file conventions, see Scriban Templating.

Providers

Provider Package
Amazon SES Deepstaging.Ses
InMemory (default) Built-in — InMemoryEmailService
Test Built-in — TestEmailService

Sub-Pages

Page Description
Effects Composition Eff<RT, T> pipelines — multi-step send, error handling, cross-module composition
Generated Code What [EffectsModule] emits — capability interface, effect methods, DI registration
Testing TestEmailService — call recording, custom responses, exception injection

Source

src/Core/Deepstaging.Runtime/Email/