6fad147650
- CvSearchEmailSender now injects IEmailApiClient instead of IConfiguration+MailKit - BuildBody updated to produce styled HTML job cards - CvSearchJobTask passes only filename (not full path) to email sender - IEmailApiClient registered in Program.cs with EmailApi:BaseUrl + InternalApiKey - MailKit removed from cv-search-job.csproj Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
109 lines
3.9 KiB
C#
109 lines
3.9 KiB
C#
using CvMatcher.Models.Responses;
|
|
using CvSearch.Data.Entities;
|
|
using EmailApi.Models.Clients;
|
|
using EmailApi.Models.Requests;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Logging;
|
|
using MyAi.Data.Services;
|
|
|
|
namespace CvSearchJob.Services;
|
|
|
|
public sealed class CvSearchEmailSender
|
|
{
|
|
private readonly IEmailApiClient _emailApi;
|
|
private readonly ITemplateService _templates;
|
|
private readonly IConfiguration _config;
|
|
private readonly ILogger<CvSearchEmailSender> _logger;
|
|
|
|
public CvSearchEmailSender(
|
|
IEmailApiClient emailApi,
|
|
ITemplateService templates,
|
|
IConfiguration config,
|
|
ILogger<CvSearchEmailSender> logger)
|
|
{
|
|
_emailApi = emailApi;
|
|
_templates = templates;
|
|
_config = config;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task SendResultsAsync(
|
|
string toEmail,
|
|
string? attachmentFileName,
|
|
IReadOnlyList<JobSearchResultEntity> results,
|
|
string language,
|
|
CancellationToken ct)
|
|
{
|
|
var contactToEmail = _config["Contact:ToEmail"];
|
|
|
|
var recipients = new List<string>();
|
|
if (!string.IsNullOrWhiteSpace(toEmail)) recipients.Add(toEmail);
|
|
if (!string.IsNullOrWhiteSpace(contactToEmail) &&
|
|
!recipients.Any(r => string.Equals(r, contactToEmail, StringComparison.OrdinalIgnoreCase)))
|
|
recipients.Add(contactToEmail);
|
|
|
|
if (recipients.Count == 0) return;
|
|
|
|
var htmlBody = BuildBody(results, language);
|
|
var subject = _templates.Render("email.search-results.subject", language,
|
|
("count", results.Count.ToString()));
|
|
|
|
try
|
|
{
|
|
await _emailApi.SendAsync(new SendEmailRequest
|
|
{
|
|
To = recipients,
|
|
Subject = subject,
|
|
HtmlBody = htmlBody,
|
|
AttachmentPath = attachmentFileName
|
|
}, ct);
|
|
|
|
_logger.LogInformation("Job search results email sent to {Recipients}",
|
|
string.Join(", ", recipients));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to send job search results email to {Recipients}",
|
|
string.Join(", ", recipients));
|
|
}
|
|
}
|
|
|
|
private string BuildBody(IReadOnlyList<JobSearchResultEntity> results, string language)
|
|
{
|
|
if (results.Count == 0)
|
|
return _templates.Get("email.search-results.empty", language);
|
|
|
|
var items = new System.Text.StringBuilder();
|
|
for (int i = 0; i < results.Count; i++)
|
|
{
|
|
var r = results[i];
|
|
var matchResp = TryParseResult(r.ResultJson);
|
|
var summary = matchResp?.Summary;
|
|
|
|
items.Append($"""
|
|
<div style="border:1px solid #dee2e6;border-radius:6px;padding:16px;margin-bottom:12px">
|
|
<strong style="color:#212529">{i + 1}. {r.JobTitle}</strong>
|
|
<span style="background:#28a745;color:#fff;padding:2px 8px;border-radius:12px;font-size:12px;margin-left:8px">{r.Score}% match</span>
|
|
<span style="color:#6c757d;font-size:12px;margin-left:4px">[{r.ProviderName}]</span><br>
|
|
<a href="{r.JobUrl}" style="color:#2c5282;font-size:13px">{r.JobUrl}</a>
|
|
{(string.IsNullOrWhiteSpace(summary) ? "" : $"<p style=\"margin:8px 0 0;color:#495057;font-size:14px;line-height:1.5\">{summary}</p>")}
|
|
</div>
|
|
""");
|
|
}
|
|
|
|
return _templates.Render("email.search-results.body", language,
|
|
("count", results.Count.ToString()),
|
|
("items", items.ToString()));
|
|
}
|
|
|
|
private static JobMatchResponse? TryParseResult(string json)
|
|
{
|
|
try
|
|
{
|
|
return System.Text.Json.JsonSerializer.Deserialize<JobMatchResponse>(json,
|
|
new System.Text.Json.JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults.Web));
|
|
}
|
|
catch { return null; }
|
|
}
|
|
}
|