From 978dd3a069561aefe97401abda1c50ce451e9cc6 Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 1 Jun 2026 19:16:02 +0300 Subject: [PATCH] Update email templates to HTML format and fix EmailApiEmailSender MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- Apis/api/Services/EmailApiEmailSender.cs | 19 +-- .../20260601133043_InitialSchema.cs | 116 +++++++++++++++--- 2 files changed, 112 insertions(+), 23 deletions(-) diff --git a/Apis/api/Services/EmailApiEmailSender.cs b/Apis/api/Services/EmailApiEmailSender.cs index cd51feb..384515d 100644 --- a/Apis/api/Services/EmailApiEmailSender.cs +++ b/Apis/api/Services/EmailApiEmailSender.cs @@ -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 /// 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 ?? "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, diff --git a/Apis/email-data/Migrations/20260601133043_InitialSchema.cs b/Apis/email-data/Migrations/20260601133043_InitialSchema.cs index 0568ed2..b790a60 100644 --- a/Apis/email-data/Migrations/20260601133043_InitialSchema.cs +++ b/Apis/email-data/Migrations/20260601133043_InitialSchema.cs @@ -44,33 +44,117 @@ namespace Email.Data.Migrations Row("email.match.subject", "en", "MyAi.ro CV Match: {{score}}% - {{jobLabel}}", "Subject for the CV match result email"); Row("email.match.subject", "ro", "MyAi.ro Potrivire CV: {{score}}% - {{jobLabel}}", "Subiect email rezultat potrivire CV"); - // Match result email — body + // Match result email — body (HTML formatted) Row("email.match.body", "en", - "CV Matcher result\n\nCV Document ID: {{cvDocumentId}}\nJob: {{jobLabel}}\nJob URL: {{jobUrl}}\nScore: {{score}}%\n\nSummary:\n{{summary}}\n\nStrengths:\n{{strengths}}\n\nGaps:\n{{gaps}}\n\nRecommendations:\n{{recommendations}}", - "Body for the CV match result email"); + @"

        CV Match Report

        + + + + + + + + + + + + + + + + + +
        CV ID{{cvDocumentId}}
        Job{{jobLabel}}
        URL{{jobUrl}}
        Score{{score}}%
        +

        Summary

        +

        {{summary}}

        +

        Strengths

        +
        {{strengths}}
        +

        Gaps

        +
        {{gaps}}
        +

        Recommendations

        +
        {{recommendations}}
        ", + "Body for the CV match result email (HTML formatted)"); Row("email.match.body", "ro", - "Rezultat potrivire CV\n\nID document CV: {{cvDocumentId}}\nJob: {{jobLabel}}\nURL job: {{jobUrl}}\nScor: {{score}}%\n\nRezumat:\n{{summary}}\n\nPuncte forte:\n{{strengths}}\n\nLipsuri:\n{{gaps}}\n\nRecomandări:\n{{recommendations}}", - "Corpul emailului pentru rezultatul potrivirii CV"); + @"

        Report Potrivire CV

        + + + + + + + + + + + + + + + + + +
        ID Document CV{{cvDocumentId}}
        Job{{jobLabel}}
        URL{{jobUrl}}
        Scor{{score}}%
        +

        Rezumat

        +

        {{summary}}

        +

        Puncte Forte

        +
        {{strengths}}
        +

        Lipsuri

        +
        {{gaps}}
        +

        Recomandări

        +
        {{recommendations}}
        ", + "Corpul emailului pentru rezultatul potrivirii CV (format HTML)"); - // Match result email — job search CTA footer + // Match result email — job search CTA footer (HTML formatted) Row("email.match.job-search-footer", "en", - "\n\n---\nWant to find more jobs matching your CV?\nClick: {{jobSearchLink}}\n(link valid for {{expiryDays}} days)", - "Job search CTA appended to match result email"); + @"
        +

        Want to find more jobs matching your CV?

        +

        Search Jobs

        +

        (link valid for {{expiryDays}} days)

        ", + "Job search CTA appended to match result email (HTML formatted)"); Row("email.match.job-search-footer", "ro", - "\n\n---\nVrei sa gasesti mai multe joburi potrivite CV-ului tau?\nClick: {{jobSearchLink}}\n(link valabil {{expiryDays}} zile)", - "CTA cautare joburi adaugat la emailul de potrivire CV"); + @"
        +

        Vrei să găsești mai multe joburi potrivite CV-ului tău?

        +

        Caută Joburi

        +

        (link valabil {{expiryDays}} zile)

        ", + "CTA cautare joburi adaugat la emailul de potrivire CV (format HTML)"); // Job search results email — subject Row("email.search-results.subject", "en", "MyAi.ro: {{count}} jobs matching your CV", "Subject for job search results email"); Row("email.search-results.subject", "ro", "MyAi.ro: {{count}} joburi potrivite CV-ului tau", "Subiect email rezultate cautare joburi"); - // Job search results email — body preamble (items appended in code) - Row("email.search-results.body", "en", "MyAi.ro found {{count}} jobs matching your CV:\n\n{{items}}", "Body preamble for job search results email"); - Row("email.search-results.body", "ro", "MyAi.ro a gasit {{count}} joburi potrivite CV-ului tau:\n\n{{items}}", "Corpul emailului de rezultate cautare joburi"); + // Job search results email — body preamble (items appended in code) - HTML formatted + Row("email.search-results.body", "en", + @"

        Job Search Results

        +

        MyAi.ro found {{count}} jobs matching your CV:

        +
        +{{items}} +
        ", + "Body preamble for job search results email (HTML formatted)"); + Row("email.search-results.body", "ro", + @"

        Rezultate Căutare Joburi

        +

        MyAi.ro a găsit {{count}} joburi potrivite CV-ului tău:

        +
        +{{items}} +
        ", + "Corpul emailului de rezultate cautare joburi (format HTML)"); - // Job search results email — no results found - Row("email.search-results.empty", "en", "MyAi.ro found no jobs matching your CV. Try again later or update your CV.", "No results message for job search results email"); - Row("email.search-results.empty", "ro", "MyAi.ro nu a gasit joburi care sa corespunda CV-ului tau. Incercati mai tarziu sau ajustati CV-ul.", "Mesaj fara rezultate pentru emailul de cautare joburi"); + // Job search results email — no results found - HTML formatted + Row("email.search-results.empty", "en", + @"
        +

        +No jobs found
        +MyAi.ro found no jobs matching your CV at this moment. Please try again later or update your CV to improve your match results. +

        +
        ", + "No results message for job search results email (HTML formatted)"); + Row("email.search-results.empty", "ro", + @"
        +

        +Niciun job găsit
        +MyAi.ro nu a găsit joburi potrivite CV-ului tău în acest moment. Te rugăm să încerci din nou mai târziu sau să-ți actualizezi CV-ul pentru a obține rezultate mai bune. +

        +
        ", + "Mesaj fara rezultate pentru emailul de cautare joburi (format HTML)"); // HTML job-search start page messages Row("html.job-search.started.title", "en", "Job search started", "Title for job search started page");