e9937accd8
Gmail and Graph request explicit send consent and classify provider rejection separately from uncertain transport failure. IMAP remains read-only; no send API is exposed.
124 lines
4.7 KiB
C#
124 lines
4.7 KiB
C#
using System.Security.Claims;
|
|
using JobTrackerApi.Services.EmailProviders;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
namespace JobTrackerApi.Controllers;
|
|
|
|
public sealed record EmailMessageDetailDto(
|
|
string Id,
|
|
string ThreadId,
|
|
string Subject,
|
|
string From,
|
|
string To,
|
|
DateTimeOffset? Date,
|
|
string Snippet,
|
|
string BodyText,
|
|
IReadOnlyList<string> Labels,
|
|
IReadOnlyList<EmailAttachmentRef> Attachments);
|
|
|
|
[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);
|
|
[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: connection?.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<EmailMessageDetailDto>> 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 EmailMessageDetailDto(
|
|
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,
|
|
};
|
|
}
|