using Api.Services.Contracts; using CvMatcher.Models.Responses; using Email.Data.Services; using Email.Models.Clients; using Email.Models.Requests; using Microsoft.Extensions.Options; using Models.Requests; using Models.Settings; using System.Net; namespace Api.Services; /// /// Implements by delegating all email dispatch to the internal email-api service via Refit. /// public sealed class EmailApiEmailSender : IEmailSender { private readonly IEmailApiClient _emailApi; private readonly ContactSettings _contact; private readonly SubscribeSettings _subscribe; private readonly FileStorageSettings _fileStorage; private readonly IEmailTemplateService _emailTemplates; private readonly ILogger _log; public EmailApiEmailSender( IEmailApiClient emailApi, IOptions contact, IOptions subscribe, IOptions fileStorage, IEmailTemplateService emailTemplates, ILogger log) { _emailApi = emailApi; _contact = contact.Value; _subscribe = subscribe.Value; _fileStorage = fileStorage.Value; _emailTemplates = emailTemplates; _log = log; } /// public async Task SendContactAsync(ContactRequest req, CancellationToken ct) { if (string.IsNullOrWhiteSpace(_contact.ToEmail)) { _log.LogDebug("Contact email skipped - ToEmail not configured"); throw new InvalidOperationException("Contact email recipient is not configured."); } _log.LogInformation("Preparing contact email from {SenderEmail} to {RecipientEmail}", req.Email, _contact.ToEmail); var htmlBody = $"""

New Contact Message

Name {req.Name}
Email {req.Email}
Subject {req.Subject}

Message

{req.Message}

"""; await _emailApi.SendAsync(new SendEmailRequest { To = [_contact.ToEmail], ReplyTo = req.Email, Subject = $"{_contact.SubjectPrefix} {req.Subject}".Trim(), HtmlBody = htmlBody }, ct); _log.LogInformation("Contact email sent successfully from {SenderEmail}", req.Email); } /// public async Task SendSubscribeAsync(SubscribeRequest req, CancellationToken ct) { if (string.IsNullOrWhiteSpace(_subscribe.ToEmail)) { _log.LogDebug("Subscription email skipped - ToEmail not configured"); throw new InvalidOperationException("Subscription email recipient is not configured."); } _log.LogInformation("Processing subscription request for {Email}", req.Email); var htmlBody = $"""

New Subscription Request

A new user has subscribed:

Email {req.Email}
"""; await _emailApi.SendAsync(new SendEmailRequest { To = [_subscribe.ToEmail], ReplyTo = req.Email, Subject = _subscribe.SubjectPrefix.Trim(), HtmlBody = htmlBody }, ct); _log.LogInformation("Subscription email sent successfully for {Email}", req.Email); } /// public async Task SendFileDownloadNotificationAsync(string fileName, string? userIp, CancellationToken ct) { if (string.IsNullOrWhiteSpace(_fileStorage.ToEmail)) { _log.LogDebug("File download notification skipped - ToEmail not configured"); return; } _log.LogInformation("Preparing file download notification for {FileName}", fileName); var htmlBody = $"""

File Download Notification

File {fileName}
Downloaded at {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC
IP Address {userIp ?? _emailTemplates.Get("email.notification.unknown-ip", "en")}
"""; await _emailApi.SendAsync(new SendEmailRequest { To = [_fileStorage.ToEmail], Subject = $"{_fileStorage.SubjectPrefix} {fileName}".Trim(), HtmlBody = htmlBody }, ct); _log.LogInformation("File download notification sent successfully for {FileName}", fileName); } /// public async Task SendMatchAsync(string? explicitTo, string subject, string body, string? attachmentPath, CancellationToken ct) { var operatorCopy = _emailTemplates.GetOperatorCopy("email.match.subject", "en"); var recipients = new List(); if (!string.IsNullOrWhiteSpace(explicitTo)) recipients.Add(explicitTo); if (!string.IsNullOrWhiteSpace(operatorCopy) && !recipients.Any(x => string.Equals(x, operatorCopy, StringComparison.OrdinalIgnoreCase))) recipients.Add(operatorCopy); if (recipients.Count == 0) { _log.LogDebug("Match email skipped - no recipients configured"); return; } string? relativeAttachment = null; if (!string.IsNullOrWhiteSpace(attachmentPath)) relativeAttachment = Path.GetFileName(attachmentPath); foreach (var recipient in recipients) { _log.LogInformation("Preparing CV match email to {RecipientEmail}", recipient); await _emailApi.SendAsync(new SendEmailRequest { To = [recipient], Subject = subject, HtmlBody = body, AttachmentPath = relativeAttachment }, ct); _log.LogInformation("CV match email sent successfully to {RecipientEmail}", recipient); } } /// public string BuildMatchEmailBody(string cvDocumentId, JobMatchResponse result, string? jobLabel, string language, string? jobSearchLink = null, int expiryDays = 7) { // Build HTML lists for strengths, gaps, and recommendations var strengths = result.Strengths?.Count > 0 ? "
    " + string.Join("", result.Strengths.Select(s => $"
  • {s}
  • ")) + "
" : "

"; var gaps = result.Gaps?.Count > 0 ? "
    " + string.Join("", result.Gaps.Select(g => $"
  • {g}
  • ")) + "
" : "

"; var recommendations = result.Recommendations?.Count > 0 ? "
    " + string.Join("", result.Recommendations.Select(r => $"
  • {r}
  • ")) + "
" : "

"; // Render the HTML template with substituted values // email.match.body is now stored as HTML in the database var body = _emailTemplates.Render("email.match.body", language, ("cvDocumentId", cvDocumentId), ("jobLabel", jobLabel ?? _emailTemplates.Get("email.match.fallback-na", language)), ("jobUrl", result.JobUrl ?? _emailTemplates.Get("email.match.fallback-na", language)), ("score", result.Score.ToString()), ("summary", WebUtility.HtmlEncode(result.Summary ?? string.Empty)), ("strengths", strengths), ("gaps", gaps), ("recommendations", recommendations)); // Append the job search footer if link is provided if (!string.IsNullOrWhiteSpace(jobSearchLink)) { body += _emailTemplates.Render("email.match.job-search-footer", language, ("jobSearchLink", jobSearchLink), ("expiryDays", expiryDays.ToString())); } return body; } /// public string BuildMatchEmailSubject(int score, string? jobLabel, string language) => _emailTemplates.Render("email.match.subject", language, ("score", score.ToString()), ("jobLabel", jobLabel ?? _emailTemplates.Get("email.match.subject-fallback-label", language))); public string GetManualJobLabel(string language) => _emailTemplates.Get("email.match.manual-job-label", language); }