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
34 changes: 30 additions & 4 deletions src/content/docs/building-blocks/mailing.mdx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
7 changes: 6 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-09-25
lastUpdated: 2026-09-28
description: Release notes and version history for fullstackhero.
sidebar:
order: 1
Expand All @@ -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).
Expand Down
23 changes: 22 additions & 1 deletion src/content/docs/getting-started/troubleshooting.mdx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -127,6 +127,27 @@ docker rm -f <redis-container>
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"
Expand Down