Code
·
94 lines
·
4339 bytes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94using 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<MailSettings>(builder.Configuration.GetSection("MailSettings"));
var mailSettings = builder.Configuration.GetSection("MailSettings").Get<MailSettings>() ?? 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<IPasswordProtector, AesPasswordProtector>();
builder.Services.AddSingleton<IAccountStore, LiteDbAccountStore>();
builder.Services.AddSingleton<IMailCacheStore, LiteDbMailCacheStore>();
builder.Services.AddSingleton<IContactStore, LiteDbContactStore>();
builder.Services.AddSingleton<IFolderSettingsStore, LiteDbFolderSettingsStore>();
builder.Services.AddSingleton<IMessageIndexStore, LiteDbMessageIndexStore>();
builder.Services.AddSingleton<ILogStore, LiteDbLogStore>();
builder.Services.AddSingleton<IBackgroundIndexingStatus, BackgroundIndexingStatus>();
builder.Services.AddScoped<IImapService, ImapService>();
builder.Services.AddScoped<ISmtpService, SmtpService>();
builder.Services.AddScoped<SessionAccountManager>();
builder.Services.AddScoped<LocalizationService>();
builder.Services.AddScoped<ImageSanitizer>();
builder.Services.AddHostedService<LogPruningService>();
// 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<ILoggerProvider, LiteDbLoggerProvider>();
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<IAccountStore>();
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();