feat(email): expose provider-neutral reads
CI and Deploy / test (pull_request) Failing after 1m37s
CI and Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
cesnimda
2026-08-09 21:35:04 +02:00
parent 0e2a59a8ef
commit 536d403b08
4 changed files with 274 additions and 0 deletions
@@ -0,0 +1,123 @@
using System.Security.Claims;
using JobTrackerApi.Services.EmailProviders;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace JobTrackerApi.Controllers;
[ApiController]
[Route("api/email")]
[Authorize(AuthenticationSchemes = "local")]
public sealed class EmailController(IEmailProviderRegistry providers) : ControllerBase
{
public sealed record ProviderStatus(string Provider, string DisplayName, bool Connected, string? Address, bool CanRead, bool CanSend);
public sealed record MessageDetail(
string Id,
string ThreadId,
string Subject,
string From,
string To,
DateTimeOffset? Date,
string Snippet,
string BodyText,
IReadOnlyList<string> Labels,
IReadOnlyList<EmailAttachmentRef> Attachments);
[HttpGet("providers")]
public async Task<ActionResult<IReadOnlyList<ProviderStatus>>> GetProviders(CancellationToken cancellationToken)
{
var ownerUserId = GetOwnerUserId();
if (ownerUserId is null) return Unauthorized();
var statuses = new List<ProviderStatus>(providers.All.Count);
foreach (var provider in providers.All)
{
var connection = await provider.GetConnectionAsync(ownerUserId, cancellationToken);
statuses.Add(new ProviderStatus(
provider.ProviderKey,
GetDisplayName(provider.ProviderKey),
connection is not null,
connection?.Address,
CanRead: connection is not null,
CanSend: false));
}
return Ok(statuses);
}
[HttpGet("messages")]
public async Task<ActionResult<IReadOnlyList<EmailMessageSummary>>> Search(
[FromQuery] string provider,
[FromQuery] string? q,
[FromQuery] int limit = 25,
CancellationToken cancellationToken = default)
{
var resolved = providers.Get(provider);
if (resolved is null) return BadRequest("Unknown email provider.");
var ownerUserId = GetOwnerUserId();
if (ownerUserId is null) return Unauthorized();
if (await resolved.GetConnectionAsync(ownerUserId, cancellationToken) is null)
return Conflict(new ProblemDetails { Title = "Email provider is not connected." });
return Ok(await resolved.SearchAsync(ownerUserId, q, Math.Clamp(limit, 1, 100), cancellationToken));
}
[HttpGet("thread")]
public async Task<ActionResult<IReadOnlyList<EmailMessageSummary>>> GetThread(
[FromQuery] string provider,
[FromQuery] string threadId,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(threadId)) return BadRequest("threadId is required.");
var resolved = providers.Get(provider);
if (resolved is null) return BadRequest("Unknown email provider.");
var ownerUserId = GetOwnerUserId();
if (ownerUserId is null) return Unauthorized();
if (await resolved.GetConnectionAsync(ownerUserId, cancellationToken) is null)
return Conflict(new ProblemDetails { Title = "Email provider is not connected." });
return Ok(await resolved.ListThreadMessagesAsync(ownerUserId, threadId.Trim(), cancellationToken));
}
[HttpGet("message")]
public async Task<ActionResult<MessageDetail>> GetMessage(
[FromQuery] string provider,
[FromQuery] string messageId,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(messageId)) return BadRequest("messageId is required.");
var resolved = providers.Get(provider);
if (resolved is null) return BadRequest("Unknown email provider.");
var ownerUserId = GetOwnerUserId();
if (ownerUserId is null) return Unauthorized();
if (await resolved.GetConnectionAsync(ownerUserId, cancellationToken) is null)
return Conflict(new ProblemDetails { Title = "Email provider is not connected." });
var detail = await resolved.GetMessageAsync(ownerUserId, messageId.Trim(), cancellationToken);
return Ok(new MessageDetail(
detail.Id,
detail.ThreadId,
detail.Subject,
detail.From,
detail.To,
detail.Date,
detail.Snippet,
detail.BodyText,
detail.Labels,
detail.Attachments));
}
private string? GetOwnerUserId() =>
User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub");
private static string GetDisplayName(string provider) => provider.ToLowerInvariant() switch
{
"gmail" => "Gmail",
"microsoft" => "Outlook",
"imap" => "IMAP",
_ => provider,
};
}