using System.Security.Claims; using JobTrackerApi.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace JobTrackerApi.Controllers; /// /// Generic IMAP connection lifecycle for mailboxes with no dedicated OAuth provider. Unlike /// Gmail/Microsoft, there's no OAuth redirect — the caller submits host/username/password once, /// verifies them by connecting, then encrypts and stores them. /// [ApiController] [Route("api/imap")] [Authorize] public sealed class ImapController : ControllerBase { private readonly IImapService _imap; public ImapController(IImapService imap) { _imap = imap; } public sealed record ImapConnectRequest(string Host, int Port, bool UseSsl, string Username, string Password); public sealed record ImapConnectionStatusDto( bool Connected, string? Host, int? Port, bool? UseSsl, string? Username, DateTimeOffset? ConnectedAt, DateTimeOffset? LastSyncedAt, DateTimeOffset? LastSyncAttemptedAt, DateTimeOffset? LastSyncSucceededAt, string? LastSyncMode, string? LastSyncSource, string? LastSyncStatus, string? LastSyncError); [HttpGet("status")] public async Task> Status(CancellationToken cancellationToken) { var ownerUserId = GetRequiredOwnerUserId(); var connection = await _imap.GetConnectionAsync(ownerUserId, cancellationToken); return Ok(new ImapConnectionStatusDto( connection is not null, connection?.Host, connection?.Port, connection?.UseSsl, connection?.Username, connection?.ConnectedAt, connection?.LastSyncedAt, connection?.LastSyncAttemptedAt, connection?.LastSyncSucceededAt, connection?.LastSyncMode, connection?.LastSyncSource, connection?.LastSyncStatus, connection?.LastSyncError)); } [HttpPost("connect")] public async Task Connect([FromBody] ImapConnectRequest request, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(request.Host)) return BadRequest("Host is required."); if (request.Port <= 0 || request.Port > 65535) return BadRequest("Valid port is required."); if (string.IsNullOrWhiteSpace(request.Username)) return BadRequest("Username is required."); if (string.IsNullOrWhiteSpace(request.Password)) return BadRequest("Password is required."); var ownerUserId = GetRequiredOwnerUserId(); try { var result = await _imap.ConnectAsync(ownerUserId, request.Host, request.Port, request.UseSsl, request.Username, request.Password, cancellationToken); return Ok(new { username = result.Username }); } catch (InvalidOperationException ex) { return BadRequest(ex.Message); } } [HttpDelete("connection")] public async Task Disconnect(CancellationToken cancellationToken) { var ownerUserId = GetRequiredOwnerUserId(); await _imap.DisconnectAsync(ownerUserId, cancellationToken); return NoContent(); } private string GetRequiredOwnerUserId() { return User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub") ?? throw new InvalidOperationException("Authenticated user id is missing."); } }