From 5a86d6599c0bf476273c45e8f3011dc7619bb47f Mon Sep 17 00:00:00 2001 From: "Marcelo M. Maciel" <4993482+marcelo-maciel@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:08:20 -0300 Subject: [PATCH] docs(mailing): document Body as HTML and the new TextBody alternative MailRequest.Body always lands in the HTML part, which the page stated only in passing and the SendGrid line got wrong (it no longer sends Body as both parts). Records the new optional TextBody, and warns about the two ways bare text in Body bites: an action link that is never auto-linked, and interpolated values parsed as markup. Adds the changelog entry for the fix. --- src/content/docs/building-blocks/mailing.mdx | 14 ++++++++++---- src/content/docs/changelog/index.mdx | 6 +++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/content/docs/building-blocks/mailing.mdx b/src/content/docs/building-blocks/mailing.mdx index 3aea5b9c..479adb71 100644 --- a/src/content/docs/building-blocks/mailing.mdx +++ b/src/content/docs/building-blocks/mailing.mdx @@ -49,10 +49,15 @@ public class MailRequest( Collection? bcc = null, Collection? cc = null, IDictionary? attachmentData = null, - IDictionary? headers = null); + IDictionary? 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 ``, HTML-encode every interpolated value, and put the plain wording in `TextBody`. +::: ### Implementations @@ -71,12 +76,13 @@ Inject `IMailService` and call `SendAsync` - or better, do what Identity does an var mailRequest = new MailRequest( new Collection { 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 diff --git a/src/content/docs/changelog/index.mdx b/src/content/docs/changelog/index.mdx index 411e58d4..221f3d62 100644 --- a/src/content/docs/changelog/index.mdx +++ b/src/content/docs/changelog/index.mdx @@ -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 @@ -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 `` 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).