using MailSharp.MailClient.Extensions; using MailSharp.MailClient.Models; using MailSharp.MailClient.Services; using MailSharp.MailClient.Services.Logging; using Microsoft.Extensions.Logging; using System.Text.Json.Serialization; var builder = WebApplication.CreateBuilder(args); builder.Services.Configure(builder.Configuration.GetSection("MailSettings")); var mailSettings = builder.Configuration.GetSection("MailSettings").Get() ?? new MailSettings(); builder.Services.AddControllersWithViews(options => { // Cc/Bcc and similar optional string fields on request DTOs (e.g. ComposeModel) are non-nullable // reference types with a "" default, not [Required] - without this, MVC's implicit-required // validation for non-nullable reference types rejects them whenever the form posts an empty value. options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true; }).AddJsonOptions(options => { options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); }); builder.Services.AddHttpContextAccessor(); builder.Services.AddMailSharpAuthentication(mailSettings); builder.Services.AddHttpClient("ImageDownload", client => { client.Timeout = TimeSpan.FromSeconds(10); client.DefaultRequestHeaders.UserAgent.ParseAdd("MailSharp/1.0"); }); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddHostedService(); // Persists every log entry to LiteDB (see LiteDbLogStore) alongside the default console provider, // so logs survive an app restart / running as a Windows Service and are visible on the Maintenance // page - registered on the logging builder's own service collection (shares the same DI container // as builder.Services) so the provider can resolve ILogStore via normal constructor injection. builder.Logging.Services.AddSingleton(); var app = builder.Build(); // One-time upgrade path for installs that predate the IsAdmin field: if there's exactly one // account and nobody is an admin yet, that account is the obvious sole candidate - promote it // automatically so there's no separate manual step. Installs with multiple accounts and no admin // still need the explicit claim-admin bootstrap (see SettingsApiController.ClaimAdmin), since it's // not obvious which of several accounts should get the promotion. using (var scope = app.Services.CreateScope()) { var accountStore = scope.ServiceProvider.GetRequiredService(); var accounts = accountStore.GetAll(); if (accounts.Count == 1 && !accounts[0].IsAdmin) { accounts[0].IsAdmin = true; accountStore.Update(accounts[0]); } } if (!app.Environment.IsDevelopment()) { app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); // No separate CSRF header check needed: the auth cookie is SameSite=Lax, so it is never sent // on a cross-site POST (the classic CSRF vector) - a forged request from another site simply // arrives unauthenticated and gets rejected by RequireAuthorization() below. app.UseAuthentication(); app.UseAuthorization(); // All API controllers require a logged-in account by default; the few endpoints that must stay // reachable while logged out (auth/state, login, login-defaults, and the whole language // controller) are marked [AllowAnonymous] explicitly. app.MapControllers().RequireAuthorization(); app.MapControllerRoute( name: "default", pattern: "{controller=Mail}/{action=Index}/{id?}"); await app.RunAsync();