feat(email): add safe message detail
This commit is contained in:
@@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using JobTrackerApi.Data;
|
||||
using JobTrackerApi.Models;
|
||||
using JobTrackerApi.Services.EmailProviders;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace JobTrackerApi.Controllers
|
||||
{
|
||||
@@ -42,6 +44,8 @@ namespace JobTrackerApi.Controllers
|
||||
DateTime Date,
|
||||
string ContentPreview,
|
||||
string? ExternalThreadId,
|
||||
string? ExternalMessageId,
|
||||
string? Provider,
|
||||
string? ExternalFrom,
|
||||
string? ExternalTo,
|
||||
int LabelCount,
|
||||
@@ -84,27 +88,50 @@ namespace JobTrackerApi.Controllers
|
||||
query = query.Where(c => c.ExternalThreadId == null);
|
||||
}
|
||||
|
||||
var items = await query
|
||||
var rows = await query
|
||||
.OrderByDescending(c => c.Date)
|
||||
.Take(200)
|
||||
.Select(c => new CorrespondenceInboxItemDto(
|
||||
.Select(c => new
|
||||
{
|
||||
c.Id,
|
||||
c.JobApplicationId,
|
||||
c.JobApplication.Company != null ? c.JobApplication.Company.Name : null,
|
||||
c.JobApplication.JobTitle,
|
||||
CompanyName = c.JobApplication.Company != null ? c.JobApplication.Company.Name : null,
|
||||
JobTitle = c.JobApplication.JobTitle,
|
||||
c.From,
|
||||
c.Direction,
|
||||
c.Subject,
|
||||
c.Channel,
|
||||
c.Date,
|
||||
c.Content.Length <= 220 ? c.Content : c.Content.Substring(0, 220),
|
||||
ContentPreview = c.Content.Length <= 220 ? c.Content : c.Content.Substring(0, 220),
|
||||
c.ExternalThreadId,
|
||||
c.ExternalMessageId,
|
||||
c.Provider,
|
||||
c.ExternalFrom,
|
||||
c.ExternalTo,
|
||||
c.ExternalLabelsJson != null ? 1 : 0,
|
||||
c.AttachmentMetadataJson != null ? 1 : 0))
|
||||
c.ExternalLabelsJson,
|
||||
c.AttachmentMetadataJson,
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var items = rows.Select(c => new CorrespondenceInboxItemDto(
|
||||
c.Id,
|
||||
c.JobApplicationId,
|
||||
c.CompanyName,
|
||||
c.JobTitle,
|
||||
c.From,
|
||||
c.Direction,
|
||||
c.Subject,
|
||||
c.Channel,
|
||||
c.Date,
|
||||
c.ContentPreview,
|
||||
c.ExternalThreadId,
|
||||
c.ExternalMessageId,
|
||||
c.Provider,
|
||||
c.ExternalFrom,
|
||||
c.ExternalTo,
|
||||
DeserializeLabels(c.ExternalLabelsJson).Count,
|
||||
DeserializeAttachments(c.AttachmentMetadataJson).Count)).ToList();
|
||||
|
||||
return Ok(items);
|
||||
}
|
||||
|
||||
@@ -123,6 +150,25 @@ namespace JobTrackerApi.Controllers
|
||||
return Ok(messages);
|
||||
}
|
||||
|
||||
[HttpGet("message/{id:int}")]
|
||||
public async Task<ActionResult<EmailMessageDetailDto>> GetMessage([FromRoute] int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var message = await FindOwnedMessageAsync(id, cancellationToken);
|
||||
if (message is null) return NotFound();
|
||||
|
||||
return Ok(new EmailMessageDetailDto(
|
||||
message.ExternalMessageId ?? $"correspondence-{message.Id}",
|
||||
message.ExternalThreadId ?? string.Empty,
|
||||
message.Subject ?? string.Empty,
|
||||
message.ExternalFrom ?? message.From,
|
||||
message.ExternalTo ?? string.Empty,
|
||||
new DateTimeOffset(DateTime.SpecifyKind(message.Date, DateTimeKind.Local)),
|
||||
message.Content.Length <= 220 ? message.Content : message.Content[..220],
|
||||
message.Content,
|
||||
DeserializeLabels(message.ExternalLabelsJson),
|
||||
DeserializeAttachments(message.AttachmentMetadataJson)));
|
||||
}
|
||||
|
||||
public sealed record CreateCorrespondenceRequest(int JobApplicationId, string From, string Content);
|
||||
public sealed record CreateCorrespondenceRequestV2(
|
||||
int JobApplicationId,
|
||||
@@ -186,5 +232,27 @@ namespace JobTrackerApi.Controllers
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> DeserializeLabels(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return Array.Empty<string>();
|
||||
try { return JsonSerializer.Deserialize<List<string>>(json) ?? new List<string>(); }
|
||||
catch (JsonException) { return Array.Empty<string>(); }
|
||||
}
|
||||
|
||||
private static IReadOnlyList<EmailAttachmentRef> DeserializeAttachments(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return Array.Empty<EmailAttachmentRef>();
|
||||
try
|
||||
{
|
||||
return (JsonSerializer.Deserialize<List<CorrespondenceAttachmentMetadata>>(json) ?? new List<CorrespondenceAttachmentMetadata>())
|
||||
.Select(item => new EmailAttachmentRef(item.FileName, item.MimeType, item.SizeBytes, item.GmailAttachmentId, item.Inline))
|
||||
.ToList();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return Array.Empty<EmailAttachmentRef>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,24 +5,24 @@ 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);
|
||||
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)
|
||||
{
|
||||
@@ -82,7 +82,7 @@ public sealed class EmailController(IEmailProviderRegistry providers) : Controll
|
||||
}
|
||||
|
||||
[HttpGet("message")]
|
||||
public async Task<ActionResult<MessageDetail>> GetMessage(
|
||||
public async Task<ActionResult<EmailMessageDetailDto>> GetMessage(
|
||||
[FromQuery] string provider,
|
||||
[FromQuery] string messageId,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -97,7 +97,7 @@ public sealed class EmailController(IEmailProviderRegistry providers) : Controll
|
||||
return Conflict(new ProblemDetails { Title = "Email provider is not connected." });
|
||||
|
||||
var detail = await resolved.GetMessageAsync(ownerUserId, messageId.Trim(), cancellationToken);
|
||||
return Ok(new MessageDetail(
|
||||
return Ok(new EmailMessageDetailDto(
|
||||
detail.Id,
|
||||
detail.ThreadId,
|
||||
detail.Subject,
|
||||
|
||||
Reference in New Issue
Block a user