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 bed3fe42..9783b483 100644 --- a/src/content/docs/changelog/index.mdx +++ b/src/content/docs/changelog/index.mdx @@ -25,6 +25,10 @@ The transactional outbox was rebuilt so that any module can publish, every tenan - **The `fsh` CLI and `dotnet new` template are now on NuGet as stable `10.0.0`.** The two distribution packages that 10.0.0 had been waiting on have shipped: `FullStackHero.CLI` (install with `dotnet tool install -g FullStackHero.CLI` - no more `--prerelease`) and `FullStackHero.NET.StarterKit` (`dotnet new install FullStackHero.NET.StarterKit`). Because `fsh new` scaffolds *from* that template, the one-command flow is now end-to-end: `dotnet tool install -g FullStackHero.CLI && fsh new MyApp` produces a fully renamed project - unique JWT signing key, generated Docker secrets, `npm install` run, initial commit on `main`. The [Install](/docs/getting-started/install/) and [CLI](/docs/cli/) pages now lead with the CLI as the recommended path; `git clone` and the GitHub template remain available for reading the source or zero-install runs. See the [10.0.0 release](https://github.com/fullstackhero/dotnet-starter-kit/releases/tag/10.0.0). +## 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-11 A security & reliability audit pass across the backend. Every finding was reproduced with a failing test and adversarially verified before fixing; the suite stays green (warnings-as-errors, Testcontainers integration tests).