feat: replace direct MailKit in cv-search-job with IEmailApiClient Refit call

- 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>
This commit is contained in:
2026-05-27 16:19:49 +03:00
parent 8126e7c112
commit 6fad147650
4 changed files with 63 additions and 58 deletions
@@ -1,43 +1,41 @@
using CvMatcher.Models.Responses;
using CvSearch.Data.Entities;
using MailKit.Net.Smtp;
using MailKit.Security;
using EmailApi.Models.Clients;
using EmailApi.Models.Requests;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using MimeKit;
using MyAi.Data.Services;
namespace CvSearchJob.Services;
public sealed class CvSearchEmailSender
{
private readonly IConfiguration _config;
private readonly IEmailApiClient _emailApi;
private readonly ITemplateService _templates;
private readonly IConfiguration _config;
private readonly ILogger<CvSearchEmailSender> _logger;
public CvSearchEmailSender(IConfiguration config, ITemplateService templates, ILogger<CvSearchEmailSender> logger)
public CvSearchEmailSender(
IEmailApiClient emailApi,
ITemplateService templates,
IConfiguration config,
ILogger<CvSearchEmailSender> logger)
{
_config = config;
_emailApi = emailApi;
_templates = templates;
_config = config;
_logger = logger;
}
public async Task SendResultsAsync(
string toEmail,
string? attachmentPath,
string? attachmentFileName,
IReadOnlyList<JobSearchResultEntity> results,
string language,
CancellationToken ct)
{
var smtpHost = _config["Smtp:Host"];
var smtpPort = int.TryParse(_config["Smtp:Port"], out var port) ? port : 587;
var smtpUser = _config["Smtp:Username"];
var smtpPass = _config["Smtp:Password"];
var useStartTls = bool.TryParse(_config["Smtp:UseStartTls"], out var tls) && tls;
var contactToEmail = _config["Contact:ToEmail"];
if (string.IsNullOrWhiteSpace(smtpHost)) return;
var recipients = new List<string>();
if (!string.IsNullOrWhiteSpace(toEmail)) recipients.Add(toEmail);
if (!string.IsNullOrWhiteSpace(contactToEmail) &&
@@ -46,39 +44,27 @@ public sealed class CvSearchEmailSender
if (recipients.Count == 0) return;
var body = BuildBody(results, language);
var htmlBody = BuildBody(results, language);
var subject = _templates.Render("email.search-results.subject", language,
("count", results.Count.ToString()));
var environmentName = Environment.GetEnvironmentVariable("APP_ENVIRONMENT_NAME") ?? "Development";
foreach (var recipient in recipients)
try
{
var msg = new MimeMessage();
msg.From.Add(MailboxAddress.Parse(smtpUser!));
msg.To.Add(MailboxAddress.Parse(recipient));
msg.Subject = $"[{environmentName}] {subject}";
var builder = new BodyBuilder { TextBody = body };
if (!string.IsNullOrWhiteSpace(attachmentPath) && File.Exists(attachmentPath))
builder.Attachments.Add(attachmentPath);
msg.Body = builder.ToMessageBody();
try
await _emailApi.SendAsync(new SendEmailRequest
{
using var client = new SmtpClient();
var tls2 = useStartTls ? SecureSocketOptions.StartTls : SecureSocketOptions.Auto;
await client.ConnectAsync(smtpHost, smtpPort, tls2, ct);
if (!string.IsNullOrWhiteSpace(smtpUser))
await client.AuthenticateAsync(smtpUser, smtpPass ?? string.Empty, ct);
await client.SendAsync(msg, ct);
await client.DisconnectAsync(true, ct);
_logger.LogInformation("Job search results email sent to {Recipient}", recipient);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send job search results email to {Recipient}", recipient);
}
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));
}
}
@@ -92,11 +78,17 @@ public sealed class CvSearchEmailSender
{
var r = results[i];
var matchResp = TryParseResult(r.ResultJson);
items.AppendLine($"{i + 1}. {r.JobTitle} ({r.Score}% match) [{r.ProviderName}]");
items.AppendLine($" {r.JobUrl}");
if (matchResp is not null && !string.IsNullOrWhiteSpace(matchResp.Summary))
items.AppendLine($" {matchResp.Summary}");
items.AppendLine();
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,
@@ -106,7 +98,11 @@ public sealed class CvSearchEmailSender
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)); }
try
{
return System.Text.Json.JsonSerializer.Deserialize<JobMatchResponse>(json,
new System.Text.Json.JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults.Web));
}
catch { return null; }
}
}