Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions src/content/docs/building-blocks/mailing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,15 @@ public class MailRequest(
Collection<string>? bcc = null,
Collection<string>? cc = null,
IDictionary<string, byte[]>? attachmentData = null,
IDictionary<string, string>? headers = null);
IDictionary<string, string>? headers = null,
string? textBody = null);
```

The body is treated as HTML by both implementations (MailKit's `BodyBuilder.HtmlBody`; SendGrid sends it as both plain-text and HTML content). `From`/`DisplayName` on the request override the configured defaults per send.
`Body` is the **HTML** part in both implementations (MailKit's `BodyBuilder.HtmlBody`; SendGrid's `htmlContent`), and `TextBody` is the optional `text/plain` alternative sent alongside it as multipart/alternative. `From`/`DisplayName` on the request override the configured defaults per send.

:::caution[Write HTML in `Body`, not bare text]
Because `Body` always lands in the HTML part, plain text placed there is parsed as markup. Two consequences bite in practice: a bare URL is **not** turned into a link - most clients only auto-link inside `text/plain` - so an action link arrives as dead text the user cannot click; and any interpolated value (a person's name, a tenant's name) is read as markup rather than shown. Build real HTML with an `<a href>`, HTML-encode every interpolated value, and put the plain wording in `TextBody`.
:::

### Implementations

Expand All @@ -71,12 +76,13 @@ Inject `IMailService` and call `SendAsync` - or better, do what Identity does an
var mailRequest = new MailRequest(
new Collection<string> { user.Email },
"Confirm Your Email Address",
emailBody);
emailBody,
textBody: $"Please confirm your email address using the following link: {emailVerificationUri}");

jobService.Enqueue("email", () => mailService.SendAsync(mailRequest, cancellationToken));
```

Identity uses this shape for its email flows: email confirmation, password reset, and the welcome mail.
Identity uses this shape for its email flows: email confirmation, password reset, and the welcome mail. Its `EmailBodies` helper renders the HTML - the action link as a real anchor, every interpolated value HTML-encoded - while the `textBody` argument carries the same wording in plain text.

## Configuration

Expand Down
6 changes: 5 additions & 1 deletion src/content/docs/changelog/index.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Overview
lastUpdated: 2026-07-13
lastUpdated: 2026-08-06
description: Release notes and version history for fullstackhero.
sidebar:
order: 1
Expand All @@ -11,6 +11,10 @@ seo:

Notable changes to the kit, newest first.

## 2026-08-06

- **Mailing: e-mails now carry real HTML plus a plain-text alternative, so the password-reset link is clickable again (fix).** Every provider puts `MailRequest.Body` in the **HTML** part (MailKit's `BodyBuilder.HtmlBody`, SendGrid's `htmlContent`), but the password-reset and welcome mails passed bare text into it. A plain URL inside an HTML part is not auto-linked by most clients - auto-linking is `text/plain` behaviour - so **the reset link arrived as dead text** and the user could not finish the flow. The welcome mail also interpolated the user-supplied first name straight into that markup, and SendGrid was handed `Body` as *both* parts, shipping raw markup to text-only clients. `MailRequest` gains an optional **`TextBody`** for the `text/plain` alternative: `SmtpMailService` emits both parts as multipart/alternative, and `SendGridMailService` maps them separately onto `plainTextContent`/`htmlContent`. Identity builds its bodies through a new `EmailBodies` helper that renders the action link as a real `<a href>` and HTML-encodes every interpolated value; the four tenant billing mails gained their plain twin, so no message goes out HTML-only. **Action for deployments:** none - `TextBody` is optional and appended last, so existing callers keep compiling. If you send mail from your own code, put HTML in `Body` and the plain wording in `TextBody`; a bare URL in `Body` will not be clickable. See [#1351](https://github.com/fullstackhero/dotnet-starter-kit/pull/1351).

## 2026-07-13

- **Dashboard: tenants can now edit their own branding from Settings.** A new **Settings → Branding** tab lets a tenant admin holding `Tenants.UpdateTheme` customise their **light and dark palettes** and **brand asset URLs** (logo, dark-mode logo, favicon) with a live preview - mirroring the operator's existing tenant-branding card, but self-service and with no `tenant:` header, since the theme endpoints are already scoped to the current tenant. The tab renders only for holders of that permission; a direct-URL visit without it hits the API's `403`, surfaced as an error band. Editing is draft-based - a **Reset to defaults** action and per-palette reset are available, and unsaved edits are preserved while you work (a co-admin's concurrent change appears on a manual refresh rather than overwriting your form).
Expand Down