using MailSharp.MailClient.Models; using MailSharp.MailClient.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; namespace MailSharp.MailClient.Controllers.Api; [ApiController] [Route("api/auth")] public class AuthApiController( IAccountStore accountStore, SessionAccountManager sessionManager, IImapService imapService, IOptions mailSettings, LocalizationService localizer, ILogger logger) : ControllerBase { private readonly MailSettings _settings = mailSettings.Value; // Must stay reachable while logged out - the login page calls this first to decide whether // to show the login form or redirect straight to the mail app. [AllowAnonymous] [HttpGet("state")] public IActionResult State() { var accounts = sessionManager.GetLoggedInAccountIds() .Select(id => accountStore.Get(id)) .Where(a => a != null) .Select(a => AccountDto.From(a!)) .ToList(); var activeId = sessionManager.GetActiveAccountId(); var active = activeId != null ? accountStore.Get(activeId.Value) : null; return Ok(new AuthStateDto { IsLoggedIn = sessionManager.IsLoggedIn, Account = active != null ? AccountDto.From(active) : null, Accounts = accounts }); } [AllowAnonymous] [HttpGet("login-defaults")] public IActionResult LoginDefaults() => Ok(new LoginDefaultsDto { ImapHost = _settings.DefaultImapServer, ImapPort = _settings.DefaultImapPort, ImapSecurity = _settings.DefaultImapSecurity, SmtpHost = _settings.DefaultSmtpServer, SmtpPort = _settings.DefaultSmtpPort, SmtpSecurity = _settings.DefaultSmtpSecurity }); // The login call itself must obviously work without already being authenticated. [AllowAnonymous] [HttpPost("login")] public async Task Login(LoginRequest model) { if (string.IsNullOrWhiteSpace(model.EmailAddress) || string.IsNullOrWhiteSpace(model.Password)) return BadRequest(new { error = localizer["login_error_email_password_required"] }); var username = string.IsNullOrWhiteSpace(model.Username) ? model.EmailAddress : model.Username; Account account; if (model.Advanced) { if (string.IsNullOrWhiteSpace(model.ImapHost) || string.IsNullOrWhiteSpace(model.SmtpHost)) return BadRequest(new { error = localizer["login_error_servers_required"] }); account = new Account { DisplayName = model.EmailAddress, EmailAddress = model.EmailAddress, Username = username, ImapHost = model.ImapHost, ImapPort = model.ImapPort, ImapSecurity = model.ImapSecurity, SmtpHost = model.SmtpHost, SmtpPort = model.SmtpPort, SmtpSecurity = model.SmtpSecurity }; } else { var domain = model.EmailAddress.Split('@').Last(); account = new Account { DisplayName = model.EmailAddress, EmailAddress = model.EmailAddress, Username = username, ImapHost = $"imap.{domain}", ImapPort = _settings.DefaultImapPort, ImapSecurity = _settings.DefaultImapSecurity, SmtpHost = $"smtp.{domain}", SmtpPort = _settings.DefaultSmtpPort, SmtpSecurity = _settings.DefaultSmtpSecurity }; } // New accounts start with the app-wide defaults (Settings shows real numbers immediately, // not blank inputs) rather than 0 - see also SettingsApiController.Get, which applies the // same fallback for accounts created before these settings existed. account.MessagesPerPage = _settings.MessageListPageSize; account.ContactsPerPage = _settings.ContactMailsPageSize; try { await imapService.GetFoldersAsync(account, model.Password); } catch (Exception ex) { logger.LogWarning(ex, "Login connectivity check failed for {EmailAddress} against {ImapHost}", account.EmailAddress, account.ImapHost); return BadRequest(new { error = localizer.Format("login_error_connect_failed", ex.Message) }); } var existing = accountStore.GetAll().FirstOrDefault(a => a.EmailAddress.Equals(account.EmailAddress, StringComparison.OrdinalIgnoreCase) && a.ImapHost.Equals(account.ImapHost, StringComparison.OrdinalIgnoreCase)); // The very first account ever created on a fresh install becomes admin automatically, so // there's always someone who can reach the Maintenance page without a separate setup step. if (existing == null && accountStore.GetAll().Count == 0) { account.IsAdmin = true; } var isNewAccount = existing == null; existing ??= accountStore.Add(account, model.Password); await sessionManager.AddOrUpdateAsync(existing.Id); logger.LogInformation("Account {AccountId} ({EmailAddress}) logged in{NewAccount}", existing.Id, existing.EmailAddress, isNewAccount ? " (new account, admin=" + existing.IsAdmin + ")" : ""); return Ok(AccountDto.From(existing)); } [HttpPost("switch")] public async Task Switch(SwitchAccountRequest request) { await sessionManager.SetActiveAccountIdAsync(request.AccountId); return Ok(); } [HttpPost("remove")] public async Task Remove(RemoveAccountRequest request) { await sessionManager.RemoveAsync(request.AccountId); return Ok(new { isLoggedIn = sessionManager.IsLoggedIn }); } [HttpPost("logout")] public async Task Logout() { await sessionManager.LogoutAsync(); return Ok(); } }