diff --git a/src/content/docs/building-blocks/mailing.mdx b/src/content/docs/building-blocks/mailing.mdx index 365dc963..3b832cf8 100644 --- a/src/content/docs/building-blocks/mailing.mdx +++ b/src/content/docs/building-blocks/mailing.mdx @@ -1,6 +1,6 @@ --- title: Mailing building block -lastUpdated: 2026-09-25 +lastUpdated: 2026-09-28 description: SMTP or SendGrid email abstraction behind a single IMailService, with multi-recipient delivery and HTML body support. sidebar: label: Mailing @@ -82,12 +82,12 @@ Always pair it with a `TextBody` alternative. ### Implementations -- **`SmtpMailService`** - uses MailKit + MimeKit. Reads `MailOptions:Smtp:*` for host, port, credentials; connects with STARTTLS per send. +- **`SmtpMailService`** - uses MailKit + MimeKit. Reads `MailOptions:Smtp:*` for host, port, credentials and connection security; opens a connection per send with the `MailOptions:Smtp:Security` mode (STARTTLS unless configured otherwise). - **`SendGridMailService`** - uses the SendGrid SDK via the shared `ISendGridClient`. One API call per `MailRequest`; the first `To` address is the primary recipient, `Cc`/`Bcc`/`ReplyTo`/attachments map onto the SendGrid message. ### Options -- **`MailOptions`** - `From`, `DisplayName`, `UseSendGrid` (bool), `Smtp` (`Host` / `Port` / `UserName` / `Password`), `SendGrid` (`ApiKey` plus optional `From` / `DisplayName` overrides). +- **`MailOptions`** - `From`, `DisplayName`, `UseSendGrid` (bool), `Smtp` (`Host` / `Port` / `UserName` / `Password` / `Security`), `SendGrid` (`ApiKey` plus optional `From` / `DisplayName` overrides). ## How modules consume Mailing @@ -119,12 +119,38 @@ Identity uses this shape for its email flows: email confirmation, password reset "Host": "smtp.example.com", "Port": 587, "UserName": "smtp-user", - "Password": "set-via-secrets" + "Password": "set-via-secrets", + "Security": "StartTls" } } } ``` +`Security` is MailKit's `SecureSocketOptions`, bound by name. It defaults to `StartTls`, which is what the service always did before the setting existed, so leaving it out changes nothing. + +| Value | Use it for | +|---|---| +| `StartTls` (default) | Submission on port 587: connect in plain text, then upgrade with STARTTLS. Fails with `NotSupportedException` if the server does not offer STARTTLS. | +| `SslOnConnect` | Implicit TLS, usually port 465. | +| `StartTlsWhenAvailable` | Upgrade if the server offers STARTTLS, otherwise stay in plain text. | +| `Auto` | Let MailKit pick the TLS mode; if the server supports no SSL or TLS, the connection continues unencrypted. | +| `None` | Plain SMTP with no TLS, for a local catcher such as Mailpit, MailHog or smtp4dev. Never for a real provider: credentials and mail would cross the network unencrypted. | + +An unknown name (a typo such as `Plain`) makes the options fail to bind, so the API does not start rather than guessing a mode. + +### Local mail catcher (Docker Compose) + +`deploy/docker/docker-compose.yml` ships a [Mailpit](https://mailpit.axllent.org) service (`axllent/mailpit:v1.31.3`) and points the API at it, so confirmation, password-reset and welcome e-mails work without an SMTP account: + +```yaml +MailOptions__Smtp__Host: mailpit +MailOptions__Smtp__Port: "1025" +MailOptions__Smtp__Security: "None" +MailOptions__From: ${FSH_MAIL_FROM:-no-reply@fsh.local} +``` + +Mailpit's SMTP port stays on the compose network. Its inbox UI is published on the host loopback only, at `http://localhost:8025` (`FSH_MAILPIT_PORT`), because it holds live password-reset and confirmation links. Nothing sent there leaves the host: for real delivery, set `MailOptions__Smtp__*` on the `api` service to your provider, set `FSH_MAIL_FROM` to a sender it accepts (`appsettings.Production.json` leaves `MailOptions:From` blank), and remove the `mailpit` service. + ### SendGrid ```jsonc diff --git a/src/content/docs/changelog/index.mdx b/src/content/docs/changelog/index.mdx index f01d8a12..1d2dd7f5 100644 --- a/src/content/docs/changelog/index.mdx +++ b/src/content/docs/changelog/index.mdx @@ -1,6 +1,6 @@ --- title: Overview -lastUpdated: 2026-09-25 +lastUpdated: 2026-09-28 description: Release notes and version history for fullstackhero. sidebar: order: 1 @@ -11,6 +11,11 @@ seo: Notable changes to the kit, newest first. +## 2026-09-28 + +- **Mailing & Docker Compose: e-mail works out of the box, and SMTP connection security is configurable (fix).** `SmtpMailService` always connected with STARTTLS, so any server that does not offer it was refused before a single envelope was sent, and the compose stack inherited the `smtp.ethereal.email` host with empty credentials: every confirmation, password-reset and welcome e-mail failed, and a user registered by an operator could not sign in until someone confirmed the address by hand. A new **`MailOptions:Smtp:Security`** setting (MailKit `SecureSocketOptions`, by name: `None`, `Auto`, `SslOnConnect`, `StartTls`, `StartTlsWhenAvailable`) chooses the mode; it defaults to `StartTls`, so existing configuration behaves exactly as before, and an unknown name fails the options binding instead of guessing. `deploy/docker/docker-compose.yml` now ships a pinned [Mailpit](https://mailpit.axllent.org) (`axllent/mailpit:v1.31.3`) and points the API at it with `Security` set to `None`; SMTP stays on the compose network and the inbox UI is published on the host loopback only, at `http://localhost:8025` (`FSH_MAILPIT_PORT`), because it holds live reset and confirmation links. For real delivery, set `MailOptions__Smtp__*` to your provider, set `FSH_MAIL_FROM` to a sender it accepts (the compose default is `no-reply@fsh.local`, because `appsettings.Production.json` leaves `MailOptions:From` blank), and remove the service. See [Mailing](/docs/building-blocks/mailing/) and [#1409](https://github.com/fullstackhero/dotnet-starter-kit/pull/1409). +- **Docker Compose: no more `libgssapi_krb5.so.2` errors in the API and migrator logs (fix).** The chiseled .NET images do not ship the Kerberos library, and Npgsql tries GSS encryption by default (`GSS Encryption Mode=Prefer`), so the first connection logged `Cannot load library libgssapi_krb5.so.2` on every start. Both compose connection strings now append `GSS Encryption Mode=Disable`; keep it when you swap in your own Postgres unless the server uses Kerberos. See [Troubleshooting](/docs/getting-started/troubleshooting/) and [#1409](https://github.com/fullstackhero/dotnet-starter-kit/pull/1409). + ## 2026-09-25 - **Dependencies: .NET Aspire 13.5.4 and every NuGet package to latest.** The AppHost SDK and `Aspire.Hosting.*` move 13.4.0 → 13.5.4 together (mixing 13.4 and 13.5 packages fails at runtime). The .NET 10 platform packages (ASP.NET Core, EF Core, Extensions, SignalR) go 10.0.8 → 10.0.12, OpenTelemetry 1.15 → 1.19, Asp.Versioning 10.2, Npgsql EF 10.0.3, Scalar 2.17, QuestPDF 2026.9, Hangfire 1.8.25, MailKit/MimeKit 4.18, Testcontainers 4.15, and the rest to latest stable. Three majors: **StackExchange.Redis 3.3** (the same API as 2.13.17 on a rewritten IO core, now **RESP3 by default** - Valkey and ElastiCache both speak it; `Execute("FLUSHALL")`-style admin commands now need `AllowAdmin`), **NSubstitute 6** and **xunit.runner.visualstudio 4** (still runs xUnit v2). `Microsoft.OpenApi` and `MessagePack` stay on their 2.x lines on purpose. The new SonarAnalyzer adds **S8969** (redundant null-forgiving `!`), which is fatal under warnings-as-errors: the kit's own code is cleaned up, but **if you've added code, expect S8969 build errors after pulling** - delete the flagged `!`, and the compiler will tell you if one was actually needed. Asp.Versioning 10.2's `AV0029`/`AV0030` advisories and Aspire's `ASPIRE010` (CLI bundle) are suppressed; the kit keeps one OpenAPI document per version and runs Aspire via `dotnet run`. On the first launch Aspire 13.5 recreates the persistent Postgres and Valkey containers; data volumes are kept and the Postgres image stays on 18, so no wipe is needed. See [#1396](https://github.com/fullstackhero/dotnet-starter-kit/pull/1396). diff --git a/src/content/docs/getting-started/troubleshooting.mdx b/src/content/docs/getting-started/troubleshooting.mdx index b7081722..1d2be988 100644 --- a/src/content/docs/getting-started/troubleshooting.mdx +++ b/src/content/docs/getting-started/troubleshooting.mdx @@ -1,6 +1,6 @@ --- title: Troubleshooting -lastUpdated: 2026-09-25 +lastUpdated: 2026-09-28 description: Symptom → cause → fix for the failure modes you actually hit on first run and in the daily dev loop. sidebar: order: 6 @@ -127,6 +127,27 @@ docker rm -f docker volume rm fsh-starter-redis-data ``` +### `Cannot load library libgssapi_krb5.so.2` in the API log (Docker Compose) + +**Symptom:** the API or migrator container logs `Cannot load library libgssapi_krb5.so.2` and `libgssapi_krb5.so.2: cannot open shared object file` on the first database connection, then carries on normally. + +The API and DbMigrator images are .NET *chiseled* images, which do not ship the Kerberos +library. Npgsql's `GSS Encryption Mode` defaults to `Prefer`, so it tries to load that +library before falling back to a normal connection. The message is noise, not a failure. +`deploy/docker/docker-compose.yml` appends `GSS Encryption Mode=Disable` to both +connection strings; if you point the stack at your own Postgres, keep that setting in +your connection string unless the server actually uses Kerberos. + +### E-mails never arrive (Docker Compose) + +**Symptom:** confirmation, password-reset or welcome e-mails do not show up; the API logs `An error occurred while sending email`, with `The SMTP server does not support the STARTTLS extension` or an authentication error; a user an operator registered cannot sign in because the address was never confirmed. + +The compose stack sends every e-mail to its bundled Mailpit service. Open +`http://localhost:8025` on the Docker host (`FSH_MAILPIT_PORT`) to read them and follow +the links. If you switched to a real provider, check that `MailOptions__Smtp__Security` +matches it: `StartTls` for port 587, `SslOnConnect` for 465, and `None` only for a local +catcher like Mailpit. See [Mailing](/docs/building-blocks/mailing/#local-mail-catcher-docker-compose). + ## Database & migrations ### "relation ... does not exist"