|
|
|
@@ -3,7 +3,6 @@ using System.Text.Json;
|
|
|
|
|
using JobTrackerApi.Data;
|
|
|
|
|
using JobTrackerApi.Models;
|
|
|
|
|
using JobTrackerApi.Services;
|
|
|
|
|
using JobTrackerApi.Services.EmailProviders;
|
|
|
|
|
using Microsoft.AspNetCore.Authorization;
|
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
@@ -19,24 +18,15 @@ public sealed class GmailController : ControllerBase
|
|
|
|
|
private readonly IGmailJobMatchingService _matching;
|
|
|
|
|
private readonly JobTrackerContext _db;
|
|
|
|
|
private readonly IConfiguration _cfg;
|
|
|
|
|
private readonly IEmailProviderRegistry _providers;
|
|
|
|
|
|
|
|
|
|
public GmailController(IGmailOAuthService gmail, IGmailJobMatchingService matching, JobTrackerContext db, IConfiguration cfg, IEmailProviderRegistry? providers = null)
|
|
|
|
|
public GmailController(IGmailOAuthService gmail, IGmailJobMatchingService matching, JobTrackerContext db, IConfiguration cfg)
|
|
|
|
|
{
|
|
|
|
|
_gmail = gmail;
|
|
|
|
|
_matching = matching;
|
|
|
|
|
_db = db;
|
|
|
|
|
_cfg = cfg;
|
|
|
|
|
// Fall back to a single-Gmail registry so direct construction (tests) keeps working.
|
|
|
|
|
_providers = providers ?? new EmailProviderRegistry(new IEmailProvider[] { new GmailProvider(gmail) });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The email provider backing this controller's read paths (search + thread listing),
|
|
|
|
|
// via the provider-neutral seam. OAuth and Gmail-specific candidate ranking still use
|
|
|
|
|
// IGmailOAuthService directly until they are generalised.
|
|
|
|
|
private IEmailProvider Email => _providers.Get("gmail")
|
|
|
|
|
?? throw new InvalidOperationException("Gmail email provider is not registered.");
|
|
|
|
|
|
|
|
|
|
public sealed record GmailImportResultDto(int Imported, int Skipped, string? ThreadId);
|
|
|
|
|
public sealed record GmailImportMessageResultDto(int Imported, int Skipped, string MessageId, string? ThreadId, Correspondence? Message);
|
|
|
|
|
public sealed record ImportGmailMessageRequest(int JobApplicationId, string MessageId);
|
|
|
|
@@ -393,7 +383,7 @@ public sealed class GmailController : ControllerBase
|
|
|
|
|
.FirstOrDefaultAsync(x => x.Id == request.JobApplicationId.Value, cancellationToken);
|
|
|
|
|
if (job is null) return NotFound("Job application not found.");
|
|
|
|
|
|
|
|
|
|
var threadMessages = await Email.ListThreadMessagesAsync(ownerUserId, request.ThreadId.Trim(), cancellationToken);
|
|
|
|
|
var threadMessages = await _gmail.ListThreadMessagesAsync(ownerUserId, request.ThreadId.Trim(), cancellationToken);
|
|
|
|
|
var distinctMessageIds = threadMessages
|
|
|
|
|
.Where(message => !string.IsNullOrWhiteSpace(message.Id))
|
|
|
|
|
.Select(message => message.Id)
|
|
|
|
@@ -649,7 +639,7 @@ public sealed class GmailController : ControllerBase
|
|
|
|
|
_db.JobApplications.Add(job);
|
|
|
|
|
await _db.SaveChangesAsync(cancellationToken);
|
|
|
|
|
|
|
|
|
|
var threadMessages = await Email.ListThreadMessagesAsync(ownerUserId, request.ThreadId.Trim(), cancellationToken);
|
|
|
|
|
var threadMessages = await _gmail.ListThreadMessagesAsync(ownerUserId, request.ThreadId.Trim(), cancellationToken);
|
|
|
|
|
var distinctMessageIds = threadMessages.Select(message => message.Id).Where(static id => !string.IsNullOrWhiteSpace(id)).Distinct(StringComparer.Ordinal).ToList();
|
|
|
|
|
// Batch the "already imported?" check with a single query instead of one
|
|
|
|
|
// AnyAsync per message (N+1), mirroring RelinkThread below.
|
|
|
|
@@ -705,7 +695,7 @@ public sealed class GmailController : ControllerBase
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var threadMessages = await Email.ListThreadMessagesAsync(ownerUserId, threadId, cancellationToken);
|
|
|
|
|
var threadMessages = await _gmail.ListThreadMessagesAsync(ownerUserId, threadId, cancellationToken);
|
|
|
|
|
var distinctMessageIds = threadMessages.Select(message => message.Id).Where(static id => !string.IsNullOrWhiteSpace(id)).Distinct(StringComparer.Ordinal).ToList();
|
|
|
|
|
var existingMessageIds = await _db.Correspondences
|
|
|
|
|
.Where(message => message.JobApplicationId == job.Id && message.ExternalMessageId != null && distinctMessageIds.Contains(message.ExternalMessageId))
|
|
|
|
@@ -803,7 +793,7 @@ public sealed class GmailController : ControllerBase
|
|
|
|
|
public async Task<IActionResult> Messages([FromQuery] string? query, [FromQuery] int maxResults = 12, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
var ownerUserId = GetRequiredOwnerUserId();
|
|
|
|
|
var items = await Email.SearchAsync(ownerUserId, query, maxResults, cancellationToken);
|
|
|
|
|
var items = await _gmail.ListMessagesAsync(ownerUserId, query, maxResults, cancellationToken);
|
|
|
|
|
return Ok(items);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -907,7 +897,7 @@ public sealed class GmailController : ControllerBase
|
|
|
|
|
|
|
|
|
|
foreach (var threadId in linkedThreadIds)
|
|
|
|
|
{
|
|
|
|
|
var threadMessages = await Email.ListThreadMessagesAsync(ownerUserId, threadId, cancellationToken);
|
|
|
|
|
var threadMessages = await _gmail.ListThreadMessagesAsync(ownerUserId, threadId, cancellationToken);
|
|
|
|
|
var distinctThreadMessages = threadMessages
|
|
|
|
|
.Where(message => !string.IsNullOrWhiteSpace(message.Id))
|
|
|
|
|
.GroupBy(message => message.Id, StringComparer.Ordinal)
|
|
|
|
@@ -951,9 +941,9 @@ public sealed class GmailController : ControllerBase
|
|
|
|
|
|
|
|
|
|
private async Task<Correspondence> ImportSingleMessageAsync(string ownerUserId, JobApplication job, string messageId, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
var detail = await Email.GetMessageAsync(ownerUserId, messageId, cancellationToken);
|
|
|
|
|
var me = await Email.GetConnectionAsync(ownerUserId, cancellationToken);
|
|
|
|
|
var gmailAddress = me?.Address ?? string.Empty;
|
|
|
|
|
var detail = await _gmail.GetMessageAsync(ownerUserId, messageId, cancellationToken);
|
|
|
|
|
var me = await _gmail.GetConnectionAsync(ownerUserId, cancellationToken);
|
|
|
|
|
var gmailAddress = me?.GmailAddress ?? string.Empty;
|
|
|
|
|
var isMe = detail.From.Contains(gmailAddress, StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
var messageDate = detail.Date?.LocalDateTime ?? DateTime.Now;
|
|
|
|
|
|
|
|
|
@@ -974,7 +964,7 @@ public sealed class GmailController : ControllerBase
|
|
|
|
|
FileName = attachment.FileName,
|
|
|
|
|
MimeType = attachment.MimeType,
|
|
|
|
|
SizeBytes = attachment.SizeBytes,
|
|
|
|
|
GmailAttachmentId = attachment.ExternalAttachmentId,
|
|
|
|
|
GmailAttachmentId = attachment.GmailAttachmentId,
|
|
|
|
|
Inline = attachment.Inline,
|
|
|
|
|
})),
|
|
|
|
|
Content = string.IsNullOrWhiteSpace(detail.BodyText) ? detail.Snippet : detail.BodyText,
|
|
|
|
|