feat(email): add safe message detail
CI and Deploy / test (pull_request) Failing after 1m29s
CI and Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
cesnimda
2026-08-09 21:44:24 +02:00
parent 6f0e9bb516
commit a20775c24a
6 changed files with 262 additions and 23 deletions
@@ -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>();
}
}
}
}