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 |
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/