Fix email templates for Outlook compatibility and move HTML out of code

- Replace div-based layouts with table-based HTML throughout (max-width/border-radius/display:inline-block ignored by Outlook)
- email.match.body: width:100% table with per-cell borders and fixed 130px label column
- email.match.job-search-footer: table-based button with bgcolor attribute
- email.search-results.empty: div replaced with full-width table
- email.search-results.body: remove div wrapper around items
- Add email.search-results.scan-summary and email.search-results.item templates
- CvSearchEmailSender: remove all hardcoded HTML; render via IEmailTemplateService

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 20:01:58 +03:00
parent 8f90a4cfda
commit 2838885e22
2 changed files with 121 additions and 61 deletions
@@ -89,7 +89,7 @@ public sealed class CvSearchEmailSender
/// </summary>
private string BuildBody(IReadOnlyList<JobSearchResultEntity> results, IReadOnlyList<string> keywords, IReadOnlyList<string> providerNames, string language)
{
var scanSummary = BuildScanSummary(keywords, providerNames);
var scanSummary = BuildScanSummary(keywords, providerNames, language);
if (results.Count == 0)
return scanSummary + _emailTemplates.Get("email.search-results.empty", language);
@@ -98,18 +98,18 @@ public sealed class CvSearchEmailSender
for (int i = 0; i < results.Count; i++)
{
var r = results[i];
var matchResp = TryParseResult(r.ResultJson);
var summary = matchResp?.Summary;
var summary = TryParseResult(r.ResultJson)?.Summary;
var summaryHtml = string.IsNullOrWhiteSpace(summary)
? ""
: $"<p style=\"margin:8px 0 0;color:#495057;font-size:14px;line-height:1.5;\">{summary}</p>";
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>
""");
items.Append(_emailTemplates.Render("email.search-results.item", language,
("index", (i + 1).ToString()),
("jobTitle", r.JobTitle),
("score", r.Score.ToString()),
("providerName", r.ProviderName),
("jobUrl", r.JobUrl),
("summary", summaryHtml)));
}
return _emailTemplates.Render("email.search-results.body", language,
@@ -118,25 +118,23 @@ public sealed class CvSearchEmailSender
}
/// <summary>
/// Builds the scan summary block showing the CV keywords and providers used for the search.
/// Renders the scan summary block via template, passing keyword tags and provider list as data.
/// Keyword tags are built here because they are variable-count inline elements, not structural HTML.
/// </summary>
private static string BuildScanSummary(IReadOnlyList<string> keywords, IReadOnlyList<string> providerNames)
private string BuildScanSummary(IReadOnlyList<string> keywords, IReadOnlyList<string> providerNames, string language)
{
var keywordsHtml = keywords.Count > 0
? string.Join(" ", keywords.Select(k =>
$"<span style=\"display:inline-block;background:#e9ecef;border-radius:4px;padding:2px 8px;margin:2px 2px 2px 0;font-size:12px\">{k}</span>"))
: "<span style=\"color:#6c757d;font-size:12px;font-style:italic\">none detected</span>";
$"<span style=\"display:inline-block;background:#e9ecef;padding:2px 8px;margin:2px 2px 2px 0;font-size:12px;\">{k}</span>"))
: "<span style=\"color:#6c757d;font-size:12px;font-style:italic;\">none detected</span>";
var providersText = providerNames.Count > 0
var providers = providerNames.Count > 0
? string.Join(", ", providerNames)
: "none";
return $"""
<div style="background:#f8f9fa;border:1px solid #dee2e6;border-radius:6px;padding:14px 16px;margin-bottom:18px;font-size:13px;color:#495057">
<div style="margin-bottom:8px"><strong>Keywords used:</strong>&nbsp;{keywordsHtml}</div>
<div><strong>Providers scanned:</strong>&nbsp;{providersText}</div>
</div>
""";
return _emailTemplates.Render("email.search-results.scan-summary", language,
("keywordsHtml", keywordsHtml),
("providers", providers));
}
/// <summary>