Update email templates to HTML format and fix EmailApiEmailSender

- Convert email.match.body, email.match.job-search-footer, email.search-results.body, and email.search-results.empty templates from plain text to proper HTML format in InitialSchema migration
- Update EmailApiEmailSender.BuildMatchEmailBody() to work with HTML templates instead of plain text
- Add WebUtility.HtmlEncode() for security when inserting dynamic content (summary)
- Templates now use semantic HTML tags (table, h2, h3, ul, li, p, div, hr, a) instead of plain text with newlines
- All 32 email template variants (16 keys × 2 languages) and 8 html.job-search.* templates seeded via migration

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 19:16:02 +03:00
parent f9530b168f
commit 978dd3a069
2 changed files with 112 additions and 23 deletions
+12 -7
View File
@@ -6,6 +6,7 @@ using EmailApi.Models.Requests;
using Microsoft.Extensions.Options;
using Models.Requests;
using Models.Settings;
using System.Net;
namespace Api.Services;
@@ -194,31 +195,35 @@ public sealed class EmailApiEmailSender : IEmailSender
/// <inheritdoc />
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
? "<ul style=\"color:#495057;padding-left:20px;line-height:1.8\">" +
? "<ul style=\"color:#495057;padding-left:20px;line-height:1.8;margin:0\">" +
string.Join("", result.Strengths.Select(s => $"<li>{s}</li>")) + "</ul>"
: "<p style=\"color:#6c757d\">—</p>";
: "<p style=\"color:#6c757d;margin:0\">—</p>";
var gaps = result.Gaps?.Count > 0
? "<ul style=\"color:#495057;padding-left:20px;line-height:1.8\">" +
? "<ul style=\"color:#495057;padding-left:20px;line-height:1.8;margin:0\">" +
string.Join("", result.Gaps.Select(g => $"<li>{g}</li>")) + "</ul>"
: "<p style=\"color:#6c757d\">—</p>";
: "<p style=\"color:#6c757d;margin:0\">—</p>";
var recommendations = result.Recommendations?.Count > 0
? "<ul style=\"color:#495057;padding-left:20px;line-height:1.8\">" +
? "<ul style=\"color:#495057;padding-left:20px;line-height:1.8;margin:0\">" +
string.Join("", result.Recommendations.Select(r => $"<li>{r}</li>")) + "</ul>"
: "<p style=\"color:#6c757d\">—</p>";
: "<p style=\"color:#6c757d;margin:0\">—</p>";
// 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 ?? "N/A"),
("jobUrl", result.JobUrl ?? "N/A"),
("score", result.Score.ToString()),
("summary", result.Summary ?? string.Empty),
("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,