using InboxIntel.Application.Abstractions; using InboxIntel.Application.DTOs; using InboxIntel.Domain.Enums; using InboxIntel.Infrastructure.Persistence; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace InboxIntel.Api.Controllers; /// /// Single-email quick actions. All writes are scoped to the authenticated user's /// UserId so one user cannot mutate another user's email. /// Read/star are non-destructive and execute without confirmation. /// Trash is reversible and also executes without a separate confirm step — /// the single-email context makes the intent unambiguous. /// public class EmailController : ApiControllerBase { private readonly ICleanupService _cleanup; private readonly IUnsubscribeService _unsub; private readonly IAiService _ai; private readonly AppDbContext _db; public EmailController(ICleanupService cleanup, IUnsubscribeService unsub, IAiService ai, AppDbContext db) { _cleanup = cleanup; _unsub = unsub; _ai = ai; _db = db; } /// Full email detail including body text, for the inline detail view. [HttpGet("{id:guid}")] public async Task Get(Guid id, CancellationToken ct) { var e = await _db.Emails .Where(e => e.Id == id && e.UserId == UserId) .Select(e => new EmailDetailDto( e.Id, e.GmailMessageId, e.Subject, e.Snippet, e.BodyText, e.Sender!.Address, e.Sender.DisplayName, e.SentAtUtc, e.IsUnread, e.IsStarred, e.HasAttachments, e.SizeEstimateBytes, e.Category, e.HasListUnsubscribe, e.SupportsOneClickUnsubscribe)) .FirstOrDefaultAsync(ct); return e is null ? NotFound() : Ok(e); } /// One-line AI summary, fetched on demand (returns the snippet verbatim if AI is disabled). [HttpGet("{id:guid}/summary")] public async Task Summary(Guid id, CancellationToken ct) { var e = await _db.Emails.FirstOrDefaultAsync(e => e.Id == id && e.UserId == UserId, ct); if (e is null) return NotFound(); var summary = await _ai.SummarizeEmailAsync(e.Subject, e.Snippet, e.BodyText, ct); return Ok(new { summary }); } [HttpPost("{id:guid}/read")] public Task MarkRead(Guid id, CancellationToken ct) => Act(id, CleanupActionType.MarkRead, ct); [HttpPost("{id:guid}/unread")] public Task MarkUnread(Guid id, CancellationToken ct) => Act(id, CleanupActionType.MarkUnread, ct); [HttpPost("{id:guid}/star")] public Task Star(Guid id, CancellationToken ct) => Act(id, CleanupActionType.Star, ct); [HttpPost("{id:guid}/unstar")] public Task Unstar(Guid id, CancellationToken ct) => Act(id, CleanupActionType.Unstar, ct); [HttpPost("{id:guid}/trash")] public Task Trash(Guid id, CancellationToken ct) => Act(id, CleanupActionType.Trash, ct); [HttpPost("{id:guid}/untrash")] public Task Untrash(Guid id, CancellationToken ct) => Act(id, CleanupActionType.Archive, ct); /// /// Inline unsubscribe. Detects the unsubscribe mechanism for the email's sender, /// then executes it (HTTP one-click or HTTP link). mailto targets cannot be sent /// automatically — the mailto URI is returned so the user can action it themselves. /// [HttpPost("{id:guid}/unsubscribe")] public async Task Unsubscribe(Guid id, CancellationToken ct) { var email = await _db.Emails.FirstOrDefaultAsync(e => e.Id == id && e.UserId == UserId, ct); if (email is null) return NotFound(); if (!email.HasListUnsubscribe) return BadRequest(new { error = "Email has no List-Unsubscribe header." }); await _unsub.DetectAsync(UserId, ct); var item = await _db.UnsubscribeItems .FirstOrDefaultAsync(u => u.UserId == UserId && u.SenderId == email.SenderId, ct); if (item is null) return BadRequest(new { error = "No unsubscribe target found." }); if (item.Method == UnsubscribeMethod.MailTo) return Ok(new { method = "mailto", target = item.UnsubscribeTarget }); var result = await _unsub.ProcessQueueAsync(UserId, new UnsubscribeRequestDto(new[] { item.Id }, Confirmed: true), ct); return result.Succeeded ? Ok(new { method = "auto", succeeded = result.Value!.SucceededCount > 0 }) : BadRequest(new { error = result.Error }); } private async Task Act(Guid id, CleanupActionType action, CancellationToken ct) { var req = new CleanupRequestDto(action, new[] { id }, null, null, Confirmed: true); var result = await _cleanup.ExecuteAsync(UserId, req, ct); return result.Succeeded ? Ok() : BadRequest(new { error = result.Error }); } }