fix(api): avoid NRE in MailSender on rdv query notifications

The API did not bind the Smtp section and its appsettings lacked
Site.Owner, so MailSender threw a NullReferenceException and
PostQuery returned 500. Bind Smtp in the API, add Owner/Smtp to the
api appsettings template, and log/skip sending when mail
configuration is incomplete.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Paul Schneider 2026-10-21 12:20:38 +01:00
commit 0e9f61bd24
Signed by: notazof
GPG key ID: AD4B9B545FBD7D12
5 changed files with 52 additions and 2 deletions

View file

@ -1,5 +1,13 @@
# Changelog # Changelog
## [1.0.8-rc30] - unstable
### Added
### Changed
### Fixed
## [1.0.8-rc29] - unstable ## [1.0.8-rc29] - unstable
### Added ### Added
@ -19,6 +27,14 @@
* [PostIt] Mise a jour d'Avalonia vers 12.1.4. * [PostIt] Mise a jour d'Avalonia vers 12.1.4.
### Fixed
* [Yavsc.Api, Yavsc.Server] Une demande de rendez-vous ne provoque plus
d'erreur 500 (NullReferenceException dans MailSender) : l'API lie
desormais la section Smtp, le modele appsettings-api fournit Site.Owner
et Smtp, et un envoi sans configuration mail complete est journalise
puis ignore.
## [1.0.8-rc28] - unstable ## [1.0.8-rc28] - unstable

View file

@ -6,7 +6,17 @@
"https://preprod-blogs.$DOMAIN" "https://preprod-blogs.$DOMAIN"
], ],
"Bills": "bills", "Bills": "bills",
"TempDir": "temp" "TempDir": "temp",
"Owner": {
"Name": "Paul Schneider",
"EMail": "paul@pschneider.fr"
}
},
"Smtp": {
"Host": "mercure.pschneider.fr",
"Port": 465,
"UserName": "paul",
"Password": "$SMTP_PASSWORD"
}, },
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {

View file

@ -12,6 +12,7 @@ using Yavsc.Models;
using Yavsc; using Yavsc;
using Yavsc.Server.Helpers; using Yavsc.Server.Helpers;
using Yavsc.Services; using Yavsc.Services;
using Yavsc.Settings;
using Yavsc.Server.Hubs; using Yavsc.Server.Hubs;
using Yavsc.Api.Services; using Yavsc.Api.Services;
@ -71,6 +72,7 @@ internal class Program
// Binds the "Site" section (Bills, TempDir, Avatars, …) used by // Binds the "Site" section (Bills, TempDir, Avatars, …) used by
// the document-generation flow and BillingController. // the document-generation flow and BillingController.
services.Configure<SiteSettings>(builder.Configuration.GetSection("Site")); services.Configure<SiteSettings>(builder.Configuration.GetSection("Site"));
services.Configure<SmtpSettings>(builder.Configuration.GetSection("Smtp"));
services.AddPodcasts(builder.Configuration); services.AddPodcasts(builder.Configuration);
services.AddInvoicing(builder.Configuration); services.AddInvoicing(builder.Configuration);

View file

@ -104,6 +104,12 @@ namespace Yavsc.Services
try try
{ {
logger.LogInformation($"SendEmail for {email} : {subject}"); logger.LogInformation($"SendEmail for {email} : {subject}");
if (string.IsNullOrWhiteSpace(siteSettings?.Owner?.EMail)
|| string.IsNullOrWhiteSpace(smtpSettings?.Host))
{
logger.LogError("Email not sent: mail configuration is incomplete (Site:Owner:EMail and Smtp:Host are required). To={To}, Subject={Subject}", email, subject);
return string.Empty;
}
MimeMessage msg = new(); MimeMessage msg = new();
msg.From.Add(BuildMailboxAddress(siteSettings.Owner.Name, siteSettings.Owner.EMail)); msg.From.Add(BuildMailboxAddress(siteSettings.Owner.Name, siteSettings.Owner.EMail));
msg.To.Add(BuildMailboxAddress(name, email)); msg.To.Add(BuildMailboxAddress(name, email));
@ -139,7 +145,7 @@ namespace Yavsc.Services
} }
catch (FormatException ex) catch (FormatException ex)
{ {
logger.LogError(ex, "Refusing to send email because the recipient or sender address is malformed. To={To}, From={From}", email, siteSettings.Owner.EMail); logger.LogError(ex, "Refusing to send email because the recipient or sender address is malformed. To={To}, From={From}", email, siteSettings?.Owner?.EMail);
logger.LogError(ex.Message); logger.LogError(ex.Message);
if (ex.InnerException!=null) if (ex.InnerException!=null)
logger.LogError(ex.InnerException.Message); logger.LogError(ex.InnerException.Message);

View file

@ -121,6 +121,22 @@ namespace Yavsc.Org.Tests
Assert.Equal(string.Empty, result); Assert.Equal(string.Empty, result);
} }
[Fact]
public async Task SendEmailAsync_skips_when_owner_is_not_configured()
{
var factory = new RejectingSmtpClientFactory();
var sender = new MailSender(
Options.Create(new SiteSettings { Authority = "example.com" }),
Options.Create(new SmtpSettings()),
NullLoggerFactory.Instance,
new TestStringLocalizer(),
factory);
var result = await sender.SendEmailAsync("Alice", "alice@example.com", "Rdv", "hello");
Assert.Equal(string.Empty, result);
}
private sealed class RejectingSmtpClientFactory : ISmtpClientFactory private sealed class RejectingSmtpClientFactory : ISmtpClientFactory
{ {
public Yavsc.Interfaces.ISmtpClient CreateClient() => new RejectingSmtpClient(); public Yavsc.Interfaces.ISmtpClient CreateClient() => new RejectingSmtpClient();