feat: enrich gmail correspondence metadata

This commit is contained in:
2026-04-01 16:27:34 +02:00
parent e5bcf9d5ea
commit f48136f04c
9 changed files with 133 additions and 8 deletions
+2
View File
@@ -66,6 +66,8 @@ namespace JobTrackerApi.Data
modelBuilder.Entity<GmailConnection>() modelBuilder.Entity<GmailConnection>()
.HasQueryFilter(x => CurrentUserId == null || x.OwnerUserId == CurrentUserId); .HasQueryFilter(x => CurrentUserId == null || x.OwnerUserId == CurrentUserId);
modelBuilder.Ignore<CorrespondenceAttachmentMetadata>();
modelBuilder.Entity<GmailConnection>() modelBuilder.Entity<GmailConnection>()
.HasIndex(x => new { x.OwnerUserId, x.GmailAddress }) .HasIndex(x => new { x.OwnerUserId, x.GmailAddress })
.IsUnique(); .IsUnique();
+16 -4
View File
@@ -258,7 +258,9 @@ public sealed class GmailControllerTests
DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(-1),
"Snippet", "Snippet",
"Body text", "Body text",
null)); null,
new[] { "INBOX", "IMPORTANT" },
new[] { new GmailMessageAttachment("cv.pdf", "application/pdf", 2048, "att-1", false) }));
var controller = CreateController(db, gmail.Object, "user-1"); var controller = CreateController(db, gmail.Object, "user-1");
@@ -272,6 +274,10 @@ public sealed class GmailControllerTests
Assert.Equal("thread-1", firstPayload.Message!.ExternalThreadId); Assert.Equal("thread-1", firstPayload.Message!.ExternalThreadId);
Assert.Equal("Maria Recruiter <maria@acme.test>", firstPayload.Message.ExternalFrom); Assert.Equal("Maria Recruiter <maria@acme.test>", firstPayload.Message.ExternalFrom);
Assert.Equal("user@example.test", firstPayload.Message.ExternalTo); Assert.Equal("user@example.test", firstPayload.Message.ExternalTo);
Assert.Equal("inbound", firstPayload.Message.Direction);
Assert.Contains("IMPORTANT", firstPayload.Message.ExternalLabels);
Assert.Single(firstPayload.Message.AttachmentMetadata);
Assert.Equal("cv.pdf", firstPayload.Message.AttachmentMetadata[0].FileName);
var second = await controller.Import(new GmailController.ImportGmailMessageRequest(job.Id, "msg-1"), CancellationToken.None); var second = await controller.Import(new GmailController.ImportGmailMessageRequest(job.Id, "msg-1"), CancellationToken.None);
var secondOk = Assert.IsType<OkObjectResult>(second.Result); var secondOk = Assert.IsType<OkObjectResult>(second.Result);
@@ -315,7 +321,9 @@ public sealed class GmailControllerTests
DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(-1),
"Snippet 1", "Snippet 1",
"Body text 1", "Body text 1",
null)); null,
Array.Empty<string>(),
Array.Empty<GmailMessageAttachment>()));
gmail.Setup(service => service.GetMessageAsync("user-1", "msg-2", It.IsAny<CancellationToken>())) gmail.Setup(service => service.GetMessageAsync("user-1", "msg-2", It.IsAny<CancellationToken>()))
.ReturnsAsync(new GmailMessageDetail( .ReturnsAsync(new GmailMessageDetail(
"msg-2", "msg-2",
@@ -326,7 +334,9 @@ public sealed class GmailControllerTests
DateTimeOffset.UtcNow, DateTimeOffset.UtcNow,
"Snippet 2", "Snippet 2",
"Body text 2", "Body text 2",
null)); null,
Array.Empty<string>(),
Array.Empty<GmailMessageAttachment>()));
var controller = CreateController(db, gmail.Object, "user-1"); var controller = CreateController(db, gmail.Object, "user-1");
var request = new GmailController.ImportGmailThreadRequest(job.Id, "thread-1", new[] { "msg-1", "msg-2" }); var request = new GmailController.ImportGmailThreadRequest(job.Id, "thread-1", new[] { "msg-1", "msg-2" });
@@ -398,7 +408,9 @@ public sealed class GmailControllerTests
DateTimeOffset.UtcNow, DateTimeOffset.UtcNow,
"New reply", "New reply",
"Reply body", "Reply body",
null)); null,
Array.Empty<string>(),
Array.Empty<GmailMessageAttachment>()));
var controller = CreateController(db, gmail.Object, "user-1"); var controller = CreateController(db, gmail.Object, "user-1");
var result = await controller.RefreshLinkedThreads(new GmailController.RefreshLinkedThreadsRequest(job.Id), CancellationToken.None); var result = await controller.RefreshLinkedThreads(new GmailController.RefreshLinkedThreadsRequest(job.Id), CancellationToken.None);
@@ -48,10 +48,13 @@ namespace JobTrackerApi.Controllers
string? Subject, string? Subject,
string? Channel, string? Channel,
DateTime? Date, DateTime? Date,
string? Direction,
string? ExternalMessageId, string? ExternalMessageId,
string? ExternalThreadId, string? ExternalThreadId,
string? ExternalFrom, string? ExternalFrom,
string? ExternalTo string? ExternalTo,
string? ExternalLabelsJson,
string? AttachmentMetadataJson
); );
// POST new message // POST new message
@@ -71,10 +74,13 @@ namespace JobTrackerApi.Controllers
From = request.From.Trim(), From = request.From.Trim(),
Subject = string.IsNullOrWhiteSpace(request.Subject) ? null : request.Subject.Trim(), Subject = string.IsNullOrWhiteSpace(request.Subject) ? null : request.Subject.Trim(),
Channel = string.IsNullOrWhiteSpace(request.Channel) ? null : request.Channel.Trim(), Channel = string.IsNullOrWhiteSpace(request.Channel) ? null : request.Channel.Trim(),
Direction = string.IsNullOrWhiteSpace(request.Direction) ? null : request.Direction.Trim(),
ExternalMessageId = string.IsNullOrWhiteSpace(request.ExternalMessageId) ? null : request.ExternalMessageId.Trim(), ExternalMessageId = string.IsNullOrWhiteSpace(request.ExternalMessageId) ? null : request.ExternalMessageId.Trim(),
ExternalThreadId = string.IsNullOrWhiteSpace(request.ExternalThreadId) ? null : request.ExternalThreadId.Trim(), ExternalThreadId = string.IsNullOrWhiteSpace(request.ExternalThreadId) ? null : request.ExternalThreadId.Trim(),
ExternalFrom = string.IsNullOrWhiteSpace(request.ExternalFrom) ? null : request.ExternalFrom.Trim(), ExternalFrom = string.IsNullOrWhiteSpace(request.ExternalFrom) ? null : request.ExternalFrom.Trim(),
ExternalTo = string.IsNullOrWhiteSpace(request.ExternalTo) ? null : request.ExternalTo.Trim(), ExternalTo = string.IsNullOrWhiteSpace(request.ExternalTo) ? null : request.ExternalTo.Trim(),
ExternalLabelsJson = string.IsNullOrWhiteSpace(request.ExternalLabelsJson) ? null : request.ExternalLabelsJson.Trim(),
AttachmentMetadataJson = string.IsNullOrWhiteSpace(request.AttachmentMetadataJson) ? null : request.AttachmentMetadataJson.Trim(),
Content = request.Content, Content = request.Content,
Date = request.Date ?? DateTime.Now, Date = request.Date ?? DateTime.Now,
}; };
@@ -1,4 +1,5 @@
using System.Security.Claims; using System.Security.Claims;
using System.Text.Json;
using JobTrackerApi.Data; using JobTrackerApi.Data;
using JobTrackerApi.Models; using JobTrackerApi.Models;
using JobTrackerApi.Services; using JobTrackerApi.Services;
@@ -414,12 +415,22 @@ public sealed class GmailController : ControllerBase
{ {
JobApplicationId = job.Id, JobApplicationId = job.Id,
From = isMe ? "Me" : "Company", From = isMe ? "Me" : "Company",
Direction = isMe ? "outbound" : "inbound",
Subject = string.IsNullOrWhiteSpace(detail.Subject) ? null : detail.Subject.Trim(), Subject = string.IsNullOrWhiteSpace(detail.Subject) ? null : detail.Subject.Trim(),
Channel = "Email", Channel = "Email",
ExternalMessageId = detail.Id, ExternalMessageId = detail.Id,
ExternalThreadId = string.IsNullOrWhiteSpace(detail.ThreadId) ? null : detail.ThreadId.Trim(), ExternalThreadId = string.IsNullOrWhiteSpace(detail.ThreadId) ? null : detail.ThreadId.Trim(),
ExternalFrom = string.IsNullOrWhiteSpace(detail.From) ? null : detail.From.Trim(), ExternalFrom = string.IsNullOrWhiteSpace(detail.From) ? null : detail.From.Trim(),
ExternalTo = string.IsNullOrWhiteSpace(detail.To) ? null : detail.To.Trim(), ExternalTo = string.IsNullOrWhiteSpace(detail.To) ? null : detail.To.Trim(),
ExternalLabelsJson = detail.Labels.Count == 0 ? null : JsonSerializer.Serialize(detail.Labels),
AttachmentMetadataJson = detail.Attachments.Count == 0 ? null : JsonSerializer.Serialize(detail.Attachments.Select(attachment => new CorrespondenceAttachmentMetadata
{
FileName = attachment.FileName,
MimeType = attachment.MimeType,
SizeBytes = attachment.SizeBytes,
GmailAttachmentId = attachment.GmailAttachmentId,
Inline = attachment.Inline,
})),
Content = string.IsNullOrWhiteSpace(detail.BodyText) ? detail.Snippet : detail.BodyText, Content = string.IsNullOrWhiteSpace(detail.BodyText) ? detail.Snippet : detail.BodyText,
Date = messageDate, Date = messageDate,
}; };
+9
View File
@@ -753,6 +753,9 @@ CREATE TABLE IF NOT EXISTS "TailoredCvDrafts" (
EnsureColumn(conn, "Correspondences", "ExternalThreadId", "ALTER TABLE Correspondences ADD COLUMN ExternalThreadId TEXT NULL;"); EnsureColumn(conn, "Correspondences", "ExternalThreadId", "ALTER TABLE Correspondences ADD COLUMN ExternalThreadId TEXT NULL;");
EnsureColumn(conn, "Correspondences", "ExternalFrom", "ALTER TABLE Correspondences ADD COLUMN ExternalFrom TEXT NULL;"); EnsureColumn(conn, "Correspondences", "ExternalFrom", "ALTER TABLE Correspondences ADD COLUMN ExternalFrom TEXT NULL;");
EnsureColumn(conn, "Correspondences", "ExternalTo", "ALTER TABLE Correspondences ADD COLUMN ExternalTo TEXT NULL;"); EnsureColumn(conn, "Correspondences", "ExternalTo", "ALTER TABLE Correspondences ADD COLUMN ExternalTo TEXT NULL;");
EnsureColumn(conn, "Correspondences", "Direction", "ALTER TABLE Correspondences ADD COLUMN Direction TEXT NULL;");
EnsureColumn(conn, "Correspondences", "ExternalLabelsJson", "ALTER TABLE Correspondences ADD COLUMN ExternalLabelsJson TEXT NULL;");
EnsureColumn(conn, "Correspondences", "AttachmentMetadataJson", "ALTER TABLE Correspondences ADD COLUMN AttachmentMetadataJson TEXT NULL;");
} }
// Record the migration as applied. // Record the migration as applied.
@@ -780,6 +783,9 @@ CREATE TABLE IF NOT EXISTS "TailoredCvDrafts" (
EnsureColumn(conn, "Correspondences", "ExternalThreadId", "ALTER TABLE Correspondences ADD COLUMN ExternalThreadId TEXT NULL;"); EnsureColumn(conn, "Correspondences", "ExternalThreadId", "ALTER TABLE Correspondences ADD COLUMN ExternalThreadId TEXT NULL;");
EnsureColumn(conn, "Correspondences", "ExternalFrom", "ALTER TABLE Correspondences ADD COLUMN ExternalFrom TEXT NULL;"); EnsureColumn(conn, "Correspondences", "ExternalFrom", "ALTER TABLE Correspondences ADD COLUMN ExternalFrom TEXT NULL;");
EnsureColumn(conn, "Correspondences", "ExternalTo", "ALTER TABLE Correspondences ADD COLUMN ExternalTo TEXT NULL;"); EnsureColumn(conn, "Correspondences", "ExternalTo", "ALTER TABLE Correspondences ADD COLUMN ExternalTo TEXT NULL;");
EnsureColumn(conn, "Correspondences", "Direction", "ALTER TABLE Correspondences ADD COLUMN Direction TEXT NULL;");
EnsureColumn(conn, "Correspondences", "ExternalLabelsJson", "ALTER TABLE Correspondences ADD COLUMN ExternalLabelsJson TEXT NULL;");
EnsureColumn(conn, "Correspondences", "AttachmentMetadataJson", "ALTER TABLE Correspondences ADD COLUMN AttachmentMetadataJson TEXT NULL;");
EnsureColumn(conn, "Attachments", "Purpose", "ALTER TABLE Attachments ADD COLUMN Purpose TEXT NULL;"); EnsureColumn(conn, "Attachments", "Purpose", "ALTER TABLE Attachments ADD COLUMN Purpose TEXT NULL;");
EnsureColumn(conn, "Attachments", "UseForAi", "ALTER TABLE Attachments ADD COLUMN UseForAi INTEGER NOT NULL DEFAULT 1;"); EnsureColumn(conn, "Attachments", "UseForAi", "ALTER TABLE Attachments ADD COLUMN UseForAi INTEGER NOT NULL DEFAULT 1;");
@@ -879,6 +885,9 @@ CREATE TABLE IF NOT EXISTS "TailoredCvDrafts" (
EnsureMySqlColumn(conn, "Correspondences", "ExternalThreadId", "ALTER TABLE `Correspondences` ADD COLUMN `ExternalThreadId` longtext NULL;"); EnsureMySqlColumn(conn, "Correspondences", "ExternalThreadId", "ALTER TABLE `Correspondences` ADD COLUMN `ExternalThreadId` longtext NULL;");
EnsureMySqlColumn(conn, "Correspondences", "ExternalFrom", "ALTER TABLE `Correspondences` ADD COLUMN `ExternalFrom` longtext NULL;"); EnsureMySqlColumn(conn, "Correspondences", "ExternalFrom", "ALTER TABLE `Correspondences` ADD COLUMN `ExternalFrom` longtext NULL;");
EnsureMySqlColumn(conn, "Correspondences", "ExternalTo", "ALTER TABLE `Correspondences` ADD COLUMN `ExternalTo` longtext NULL;"); EnsureMySqlColumn(conn, "Correspondences", "ExternalTo", "ALTER TABLE `Correspondences` ADD COLUMN `ExternalTo` longtext NULL;");
EnsureMySqlColumn(conn, "Correspondences", "Direction", "ALTER TABLE `Correspondences` ADD COLUMN `Direction` varchar(100) NULL;");
EnsureMySqlColumn(conn, "Correspondences", "ExternalLabelsJson", "ALTER TABLE `Correspondences` ADD COLUMN `ExternalLabelsJson` longtext NULL;");
EnsureMySqlColumn(conn, "Correspondences", "AttachmentMetadataJson", "ALTER TABLE `Correspondences` ADD COLUMN `AttachmentMetadataJson` longtext NULL;");
EnsureMySqlColumn(conn, "Attachments", "Purpose", "ALTER TABLE `Attachments` ADD COLUMN `Purpose` varchar(100) NULL;"); EnsureMySqlColumn(conn, "Attachments", "Purpose", "ALTER TABLE `Attachments` ADD COLUMN `Purpose` varchar(100) NULL;");
EnsureMySqlColumn(conn, "Attachments", "UseForAi", "ALTER TABLE `Attachments` ADD COLUMN `UseForAi` tinyint(1) NOT NULL DEFAULT 1;"); EnsureMySqlColumn(conn, "Attachments", "UseForAi", "ALTER TABLE `Attachments` ADD COLUMN `UseForAi` tinyint(1) NOT NULL DEFAULT 1;");
EnsureMySqlColumn(conn, "AspNetUsers", "ProfileCvText", "ALTER TABLE `AspNetUsers` ADD COLUMN `ProfileCvText` longtext NULL;"); EnsureMySqlColumn(conn, "AspNetUsers", "ProfileCvText", "ALTER TABLE `AspNetUsers` ADD COLUMN `ProfileCvText` longtext NULL;");
+51 -2
View File
@@ -27,7 +27,8 @@ public interface IGmailOAuthService
public sealed record GmailOAuthExchangeResult(string GmailAddress); public sealed record GmailOAuthExchangeResult(string GmailAddress);
public sealed record GmailMessageSummary(string Id, string ThreadId, string Subject, string From, string To, DateTimeOffset? Date, string Snippet); public sealed record GmailMessageSummary(string Id, string ThreadId, string Subject, string From, string To, DateTimeOffset? Date, string Snippet);
public sealed record GmailQueryMatchedMessage(GmailMessageSummary Message, IReadOnlyList<string> MatchedQueries); public sealed record GmailQueryMatchedMessage(GmailMessageSummary Message, IReadOnlyList<string> MatchedQueries);
public sealed record GmailMessageDetail(string Id, string ThreadId, string Subject, string From, string To, DateTimeOffset? Date, string Snippet, string BodyText, string? BodyHtml); public sealed record GmailMessageAttachment(string? FileName, string? MimeType, long? SizeBytes, string? GmailAttachmentId, bool Inline);
public sealed record GmailMessageDetail(string Id, string ThreadId, string Subject, string From, string To, DateTimeOffset? Date, string Snippet, string BodyText, string? BodyHtml, IReadOnlyList<string> Labels, IReadOnlyList<GmailMessageAttachment> Attachments);
internal sealed class GmailTokenResponse internal sealed class GmailTokenResponse
{ {
@@ -321,9 +322,13 @@ public sealed class GmailOAuthService : IGmailOAuthService
var threadId = root.TryGetProperty("threadId", out var threadEl) ? threadEl.GetString() ?? "" : ""; var threadId = root.TryGetProperty("threadId", out var threadEl) ? threadEl.GetString() ?? "" : "";
var snippet = root.TryGetProperty("snippet", out var snippetEl) ? snippetEl.GetString() ?? "" : ""; var snippet = root.TryGetProperty("snippet", out var snippetEl) ? snippetEl.GetString() ?? "" : "";
var labels = root.TryGetProperty("labelIds", out var labelIdsEl) && labelIdsEl.ValueKind == JsonValueKind.Array
? labelIdsEl.EnumerateArray().Where(x => x.ValueKind == JsonValueKind.String).Select(x => x.GetString()).Where(x => !string.IsNullOrWhiteSpace(x)).Cast<string>().ToList()
: new List<string>();
var payload = root.GetProperty("payload"); var payload = root.GetProperty("payload");
var headers = ReadHeaders(payload); var headers = ReadHeaders(payload);
var attachments = ReadAttachments(payload);
var bodyText = ExtractBody(payload, "text/plain"); var bodyText = ExtractBody(payload, "text/plain");
var bodyHtml = ExtractBody(payload, "text/html"); var bodyHtml = ExtractBody(payload, "text/html");
if (string.IsNullOrWhiteSpace(bodyText) && !string.IsNullOrWhiteSpace(bodyHtml)) if (string.IsNullOrWhiteSpace(bodyText) && !string.IsNullOrWhiteSpace(bodyHtml))
@@ -345,7 +350,9 @@ public sealed class GmailOAuthService : IGmailOAuthService
headers.TryGetValue("date", out var dateRaw) && DateTimeOffset.TryParse(dateRaw, out var parsedDate) ? parsedDate : null, headers.TryGetValue("date", out var dateRaw) && DateTimeOffset.TryParse(dateRaw, out var parsedDate) ? parsedDate : null,
snippet, snippet,
bodyText.Trim(), bodyText.Trim(),
string.IsNullOrWhiteSpace(bodyHtml) ? null : bodyHtml string.IsNullOrWhiteSpace(bodyHtml) ? null : bodyHtml,
labels,
attachments
); );
} }
catch (Exception ex) catch (Exception ex)
@@ -538,6 +545,48 @@ public sealed class GmailOAuthService : IGmailOAuthService
return result; return result;
} }
private static List<GmailMessageAttachment> ReadAttachments(JsonElement payload)
{
var results = new List<GmailMessageAttachment>();
ReadAttachmentsRecursive(payload, results);
return results;
}
private static void ReadAttachmentsRecursive(JsonElement payload, List<GmailMessageAttachment> results)
{
var body = payload.TryGetProperty("body", out var bodyEl) && bodyEl.ValueKind == JsonValueKind.Object
? bodyEl
: default;
var gmailAttachmentId = body.ValueKind == JsonValueKind.Object && body.TryGetProperty("attachmentId", out var attachmentIdEl) && attachmentIdEl.ValueKind == JsonValueKind.String
? attachmentIdEl.GetString()
: null;
var filename = payload.TryGetProperty("filename", out var filenameEl) ? filenameEl.GetString() : null;
var mimeType = payload.TryGetProperty("mimeType", out var mimeTypeEl) ? mimeTypeEl.GetString() : null;
var sizeBytes = body.ValueKind == JsonValueKind.Object && body.TryGetProperty("size", out var sizeEl) && sizeEl.ValueKind == JsonValueKind.Number
? sizeEl.GetInt64()
: (long?)null;
var disposition = payload.TryGetProperty("headers", out var headersEl) && headersEl.ValueKind == JsonValueKind.Array
? headersEl.EnumerateArray()
.Where(h => h.TryGetProperty("name", out var n) && string.Equals(n.GetString(), "Content-Disposition", StringComparison.OrdinalIgnoreCase))
.Select(h => h.TryGetProperty("value", out var v) ? v.GetString() : null)
.FirstOrDefault()
: null;
var isInline = !string.IsNullOrWhiteSpace(disposition) && disposition.Contains("inline", StringComparison.OrdinalIgnoreCase);
if (!string.IsNullOrWhiteSpace(gmailAttachmentId) || !string.IsNullOrWhiteSpace(filename))
{
results.Add(new GmailMessageAttachment(filename, mimeType, sizeBytes, gmailAttachmentId, isInline));
}
if (payload.TryGetProperty("parts", out var partsEl) && partsEl.ValueKind == JsonValueKind.Array)
{
foreach (var part in partsEl.EnumerateArray())
{
ReadAttachmentsRecursive(part, results);
}
}
}
private static string ExtractBody(JsonElement payload, string mimeType) private static string ExtractBody(JsonElement payload, string mimeType)
{ {
if (payload.TryGetProperty("mimeType", out var mimeTypeEl) && if (payload.TryGetProperty("mimeType", out var mimeTypeEl) &&
+23
View File
@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace JobTrackerApi.Models namespace JobTrackerApi.Models
@@ -11,13 +12,35 @@ namespace JobTrackerApi.Models
[JsonIgnore] [JsonIgnore]
public JobApplication JobApplication { get; set; } = null!; public JobApplication JobApplication { get; set; } = null!;
public string From { get; set; } = ""; // "Me" or "Company" public string From { get; set; } = ""; // "Me" or "Company"
public string? Direction { get; set; } // inbound, outbound, internal, unknown
public string? Subject { get; set; } public string? Subject { get; set; }
public string? Channel { get; set; } // e.g. Email, Call, Note public string? Channel { get; set; } // e.g. Email, Call, Note
public string? ExternalMessageId { get; set; } public string? ExternalMessageId { get; set; }
public string? ExternalThreadId { get; set; } public string? ExternalThreadId { get; set; }
public string? ExternalFrom { get; set; } public string? ExternalFrom { get; set; }
public string? ExternalTo { get; set; } public string? ExternalTo { get; set; }
public string? ExternalLabelsJson { get; set; }
public string? AttachmentMetadataJson { get; set; }
public string Content { get; set; } = ""; public string Content { get; set; } = "";
public DateTime Date { get; set; } = DateTime.Now; public DateTime Date { get; set; } = DateTime.Now;
[JsonIgnore]
public IReadOnlyList<string> ExternalLabels => string.IsNullOrWhiteSpace(ExternalLabelsJson)
? Array.Empty<string>()
: (System.Text.Json.JsonSerializer.Deserialize<List<string>>(ExternalLabelsJson) ?? new List<string>());
[JsonIgnore]
public IReadOnlyList<CorrespondenceAttachmentMetadata> AttachmentMetadata => string.IsNullOrWhiteSpace(AttachmentMetadataJson)
? Array.Empty<CorrespondenceAttachmentMetadata>()
: (System.Text.Json.JsonSerializer.Deserialize<List<CorrespondenceAttachmentMetadata>>(AttachmentMetadataJson) ?? new List<CorrespondenceAttachmentMetadata>());
}
public sealed class CorrespondenceAttachmentMetadata
{
public string? FileName { get; set; }
public string? MimeType { get; set; }
public long? SizeBytes { get; set; }
public string? GmailAttachmentId { get; set; }
public bool Inline { get; set; }
} }
} }
@@ -382,11 +382,13 @@ export default function Correspondence({ jobId, job }: { jobId: number; job: Job
<Box sx={{ maxWidth: "80%", borderRadius: 3, p: 1.25, border: `1px solid ${alpha(accent, theme.palette.mode === "dark" ? 0.32 : 0.22)}`, background: alpha(accent, theme.palette.mode === "dark" ? 0.14 : 0.1), color: "text.primary" }}> <Box sx={{ maxWidth: "80%", borderRadius: 3, p: 1.25, border: `1px solid ${alpha(accent, theme.palette.mode === "dark" ? 0.32 : 0.22)}`, background: alpha(accent, theme.palette.mode === "dark" ? 0.14 : 0.1), color: "text.primary" }}>
{m.subject ? <Typography sx={{ fontWeight: 800, mb: 0.5 }}>{m.subject}</Typography> : null} {m.subject ? <Typography sx={{ fontWeight: 800, mb: 0.5 }}>{m.subject}</Typography> : null}
<Typography sx={{ whiteSpace: "pre-wrap", lineHeight: 1.35 }}>{m.content}</Typography> <Typography sx={{ whiteSpace: "pre-wrap", lineHeight: 1.35 }}>{m.content}</Typography>
{(m.externalThreadId || m.externalFrom || m.externalTo) ? ( {(m.externalThreadId || m.externalFrom || m.externalTo || m.externalLabelsJson || m.attachmentMetadataJson) ? (
<Box sx={{ display: "flex", gap: 0.75, flexWrap: "wrap", mt: 1 }}> <Box sx={{ display: "flex", gap: 0.75, flexWrap: "wrap", mt: 1 }}>
{m.externalThreadId ? <Chip size="small" label={`Thread ${m.externalThreadId}`} variant="outlined" /> : null} {m.externalThreadId ? <Chip size="small" label={`Thread ${m.externalThreadId}`} variant="outlined" /> : null}
{m.externalFrom ? <Chip size="small" label={`From ${m.externalFrom}`} variant="outlined" /> : null} {m.externalFrom ? <Chip size="small" label={`From ${m.externalFrom}`} variant="outlined" /> : null}
{m.externalTo ? <Chip size="small" label={`To ${m.externalTo}`} variant="outlined" /> : null} {m.externalTo ? <Chip size="small" label={`To ${m.externalTo}`} variant="outlined" /> : null}
{m.externalLabelsJson ? <Chip size="small" label={`${JSON.parse(m.externalLabelsJson).length} Gmail label${JSON.parse(m.externalLabelsJson).length === 1 ? "" : "s"}`} variant="outlined" /> : null}
{m.attachmentMetadataJson ? <Chip size="small" label={`${JSON.parse(m.attachmentMetadataJson).length} attachment${JSON.parse(m.attachmentMetadataJson).length === 1 ? "" : "s"}`} variant="outlined" /> : null}
</Box> </Box>
) : null} ) : null}
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, alignItems: "flex-end", mt: 0.75 }}> <Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, alignItems: "flex-end", mt: 0.75 }}>
+11
View File
@@ -200,10 +200,19 @@ export interface SaveApplicationDraftsRequest {
recruiterMessageDraft?: string | null; recruiterMessageDraft?: string | null;
} }
export interface CorrespondenceAttachmentMetadata {
fileName?: string | null;
mimeType?: string | null;
sizeBytes?: number | null;
gmailAttachmentId?: string | null;
inline?: boolean;
}
export interface CorrespondenceMessage { export interface CorrespondenceMessage {
id: number; id: number;
jobApplicationId: number; jobApplicationId: number;
from: string; from: string;
direction?: string | null;
content: string; content: string;
subject?: string; subject?: string;
channel?: string; channel?: string;
@@ -212,6 +221,8 @@ export interface CorrespondenceMessage {
externalThreadId?: string | null; externalThreadId?: string | null;
externalFrom?: string | null; externalFrom?: string | null;
externalTo?: string | null; externalTo?: string | null;
externalLabelsJson?: string | null;
attachmentMetadataJson?: string | null;
} }
export interface GmailJobMatchReason { export interface GmailJobMatchReason {