Rask
Rask
v0.23.1-alpha.0.496
You're reading the Rask v0.23.1-alpha.0.496 guides. Updated .View source
All guides

Chapter 5 — Transactional email

Goal: email the customer an order receipt — with the email body written as a Rask component. You'll write: an email component and wire it into the job from chapter 4.

Rask.Mail is the same story as jobs: durable rows in your app.db, a background sender that delivers them over SMTP and retries on failure. The nice part is the body — it's a Rask component, so you write your email in C# with the same Div/H1 chain you already know, no templating language.

1. Write an email

Create Features/Shared/OrderReceipt.cs — a component whose Render() is the email body:

namespace Shop.Features.Shared;

public sealed partial class OrderReceipt : Component
{
    protected override Component? Render() =>
    [
        Div["OrderReceipt works. Edit Render() to build the email body."]
    ];
}

Give it the order data and build a real body. A component carries data on public properties (that's what its chain sets), so add an OrderId and a Total and render them:

public sealed partial class OrderReceipt : Component
{
    public Guid OrderId { get; set; }
    public decimal Total { get; set; }

    protected override Component? Render() =>
        Div[
            H1["Thanks for your order!"],
            P[$"Order {OrderId} — total ", Strong[$"{Total:C}"], "."],
            P["We'll email again when it ships."]
        ];
}

This is the one component in the tutorial written with plain tags rather than the Rask.Ui kit, and on purpose: a mail client never loads the kit's stylesheet, so kit components would arrive unstyled.

2. It's already wired

The mail battery is on, its queue table is mapped by RaskAppDbContext, and the first migration created it — so there is nothing to add and nothing to migrate. All that's left is your real sender address and, for production, an SMTP server. Both are settings rather than code — edit the Rask:Mail section rask new wrote into appsettings.json:

"Rask": {
  "Mail": {
    "From": "shop@example.com",
    // Dev: with no Smtp section, each message is written to this directory instead of sent.
    "PickupDirectory": "mail-pickup"
    // Prod: add "Smtp": { "Host": "smtp.example.com", "Port": 587, "User": "…" },
    // and put the password in the environment as Rask__Mail__Smtp__Password — never in this file.
  }
}

Zero-config in development. With no Smtp section, Rask.Mail doesn't try to reach a server — it writes each message to mail-pickup as an .eml file, so you can build and test the flow with no mail account. The SMTP password comes from the environment — user-secrets on your machine, the deploy's environment file in production (Chapter 11) — never from appsettings.json or Program.cs.

3. Send it from the job

Remember the SendOrderReceipt job from Chapter 4? That's exactly where the email belongs — off the request thread. The handler sends with nothing injected:

using Shop.Features.Orders;   // for Order

public sealed class SendOrderReceiptHandler : ICommandHandler<SendOrderReceipt>
{
    public async Task Handle(SendOrderReceipt job)
    {
        var order = await Order.Where(o => o.Id == job.OrderId).First(Current.Cancellation);
        if (order is null) return;

        // Hard-coded recipient for now — Order has no customer-email field yet; add one and use it here.
        await Mail.Send(
            Email.To("customer@example.com")
                 .Subject($"Your order {order.Id}")
                 .Body(OrderReceipt.OrderId(order.Id).Total(order.Total)));
    }
}

The read returns null for an order that has been deleted since the job was queued, which is why the handler checks before sending.

Email.To(...) is a fluent builder — chain Subject(...), Cc/Bcc, Attach(...), and Body(component), which renders your component to HTML right there. Note Body(OrderReceipt.OrderId(…).Total(…)) builds the component with its chain, not new OrderReceipt(...) — every Rask component is built that way (the framework enforces it, RASK014), and each public property is one step. Mail.Send just queues the row — nothing is injected, because it reaches the mail of the work it runs in — and the background sender delivers it. You now have the full chain: place order → enqueue job → job sends email, none of it on the customer's request.

Verify

  • With no SMTP host configured, placing an order writes a mail row and (within the poll interval) an .eml file in mail-pickup — body rendered from your OrderReceipt component.
  • Add a Smtp section under Rask:Mail for a real server (or a local catcher like Mailpit) and the receipt actually arrives.

Learn more: transactional email · background jobs

Next → Chapter 6: Caching the catalog

Confirm