feat: enrich gmail correspondence metadata
This commit is contained in:
@@ -48,10 +48,13 @@ namespace JobTrackerApi.Controllers
|
||||
string? Subject,
|
||||
string? Channel,
|
||||
DateTime? Date,
|
||||
string? Direction,
|
||||
string? ExternalMessageId,
|
||||
string? ExternalThreadId,
|
||||
string? ExternalFrom,
|
||||
string? ExternalTo
|
||||
string? ExternalTo,
|
||||
string? ExternalLabelsJson,
|
||||
string? AttachmentMetadataJson
|
||||
);
|
||||
|
||||
// POST new message
|
||||
@@ -71,10 +74,13 @@ namespace JobTrackerApi.Controllers
|
||||
From = request.From.Trim(),
|
||||
Subject = string.IsNullOrWhiteSpace(request.Subject) ? null : request.Subject.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(),
|
||||
ExternalThreadId = string.IsNullOrWhiteSpace(request.ExternalThreadId) ? null : request.ExternalThreadId.Trim(),
|
||||
ExternalFrom = string.IsNullOrWhiteSpace(request.ExternalFrom) ? null : request.ExternalFrom.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,
|
||||
Date = request.Date ?? DateTime.Now,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using JobTrackerApi.Data;
|
||||
using JobTrackerApi.Models;
|
||||
using JobTrackerApi.Services;
|
||||
@@ -414,12 +415,22 @@ public sealed class GmailController : ControllerBase
|
||||
{
|
||||
JobApplicationId = job.Id,
|
||||
From = isMe ? "Me" : "Company",
|
||||
Direction = isMe ? "outbound" : "inbound",
|
||||
Subject = string.IsNullOrWhiteSpace(detail.Subject) ? null : detail.Subject.Trim(),
|
||||
Channel = "Email",
|
||||
ExternalMessageId = detail.Id,
|
||||
ExternalThreadId = string.IsNullOrWhiteSpace(detail.ThreadId) ? null : detail.ThreadId.Trim(),
|
||||
ExternalFrom = string.IsNullOrWhiteSpace(detail.From) ? null : detail.From.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,
|
||||
Date = messageDate,
|
||||
};
|
||||
|
||||
@@ -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", "ExternalFrom", "ALTER TABLE Correspondences ADD COLUMN ExternalFrom 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.
|
||||
@@ -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", "ExternalFrom", "ALTER TABLE Correspondences ADD COLUMN ExternalFrom 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", "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", "ExternalFrom", "ALTER TABLE `Correspondences` ADD COLUMN `ExternalFrom` 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", "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;");
|
||||
|
||||
@@ -27,7 +27,8 @@ public interface IGmailOAuthService
|
||||
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 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
|
||||
{
|
||||
@@ -321,9 +322,13 @@ public sealed class GmailOAuthService : IGmailOAuthService
|
||||
|
||||
var threadId = root.TryGetProperty("threadId", out var threadEl) ? threadEl.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 headers = ReadHeaders(payload);
|
||||
var attachments = ReadAttachments(payload);
|
||||
var bodyText = ExtractBody(payload, "text/plain");
|
||||
var bodyHtml = ExtractBody(payload, "text/html");
|
||||
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,
|
||||
snippet,
|
||||
bodyText.Trim(),
|
||||
string.IsNullOrWhiteSpace(bodyHtml) ? null : bodyHtml
|
||||
string.IsNullOrWhiteSpace(bodyHtml) ? null : bodyHtml,
|
||||
labels,
|
||||
attachments
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -538,6 +545,48 @@ public sealed class GmailOAuthService : IGmailOAuthService
|
||||
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)
|
||||
{
|
||||
if (payload.TryGetProperty("mimeType", out var mimeTypeEl) &&
|
||||
|
||||
Reference in New Issue
Block a user