44
55import html
66
7- from rootcoz_slack_digest .models import JobRow , WeekWindow
7+ from rootcoz_slack_digest .models import JobRow , MessageConfig , WeekWindow
88from rootcoz_slack_digest .utils import version_sort_key
99
1010
11+ def _safe_href (url : str ) -> str :
12+ """Only allow http/https URLs in href attributes."""
13+ if url and url .startswith (("http://" , "https://" )):
14+ return html .escape (url , quote = True )
15+ return ""
16+
17+
1118def format_digest_html (
1219 * ,
1320 window : WeekWindow ,
1421 rows : list [JobRow ],
1522 team : str ,
1623 tiers : list [str ] | None = None ,
24+ message : MessageConfig | None = None ,
1725) -> str :
1826 """Render digest rows as styled HTML email body."""
27+ msg = message or MessageConfig ()
1928 total_failures = sum (r .failure_count for r in rows )
2029 total_reviewed = sum (r .reviewed_count for r in rows )
2130 tier_display = ", " .join (html .escape (t ) for t in tiers ) if tiers else "all"
@@ -29,7 +38,7 @@ def format_digest_html(
2938
3039 html_parts = [
3140 "<html><body style='font-family: Arial, sans-serif; font-size: 14px;'>" ,
32- f"<h2>rootcoz weekly digest — { html .escape (window .label )} </h2>" ,
41+ f"<h2>{ html . escape ( msg . email_body_title ) } — { html .escape (window .label )} </h2>" ,
3342 f"<p><strong>Team:</strong> { team_html } · "
3443 f"<strong>Tiers:</strong> { tier_display } · "
3544 f"<strong>Jobs:</strong> { len (rows )} · "
@@ -53,15 +62,13 @@ def format_digest_html(
5362 html_parts .append ("<ul>" )
5463 for row in tier_rows :
5564 job_name = html .escape (row .job_name )
56- if row .jenkins_url :
57- name_html = f'<a href="{ html .escape (row .jenkins_url , quote = True )} ">{ job_name } </a>'
65+ href = _safe_href (row .jenkins_url )
66+ if href :
67+ name_html = f'<a href="{ href } ">{ job_name } </a>'
5868 else :
5969 name_html = f"<strong>{ job_name } </strong>"
60- rootcoz_html = (
61- f' · <a href="{ html .escape (row .rootcoz_url , quote = True )} ">rootcoz</a>'
62- if row .rootcoz_url
63- else ""
64- )
70+ rootcoz_href = _safe_href (row .rootcoz_url )
71+ rootcoz_html = f' · <a href="{ rootcoz_href } ">rootcoz</a>' if rootcoz_href else ""
6572 bundle_html = f" [{ html .escape (row .bundle )} ]" if row .bundle else ""
6673 html_parts .append (
6774 f"<li>{ name_html } { bundle_html } "
@@ -81,36 +88,44 @@ def format_celebration_html(
8188 total_jobs : int ,
8289 tiers : list [str ] | None = None ,
8390 jobs : list [JobRow ] | None = None ,
91+ message : MessageConfig | None = None ,
8492) -> str :
8593 """Render celebration message as HTML."""
94+ msg = message or MessageConfig ()
8695 tier_display = ", " .join (html .escape (t ) for t in tiers ) if tiers else "all"
8796 team_html = html .escape (team )
8897 if total_jobs > 0 :
89- body = (
90- f"All <strong>{ total_jobs } </strong> { tier_display } failures "
91- f"for <strong>{ team_html } </strong> have been reviewed! 👏"
98+ body = msg .email_celebration_reviewed_template .format (
99+ total_jobs = total_jobs ,
100+ lanes = tier_display ,
101+ team = team_html ,
92102 )
93103 else :
94- body = f"Zero { tier_display } failures for <strong>{ team_html } </strong> this week! 🚀"
104+ body = msg .email_celebration_no_failures_template .format (
105+ lanes = tier_display ,
106+ team = team_html ,
107+ )
95108
96109 parts = [
97110 "<html><body style='font-family: Arial, sans-serif;'>" ,
98- f"<h2>🎉 rootcoz weekly digest — { html .escape (window .label )} </h2>" ,
111+ f"<h2>🎉 { html . escape ( msg . email_body_title ) } — { html .escape (window .label )} </h2>" ,
99112 f"<p>✅ { body } </p>" ,
100113 ]
114+ max_links = msg .celebration_max_links
101115 if total_jobs > 0 and jobs :
102116 items : list [str ] = []
103- for job in jobs [:20 ]:
117+ for job in jobs [:max_links ]:
104118 bundle_html = f" [{ html .escape (job .bundle )} ]" if job .bundle else ""
105- if job .rootcoz_url :
119+ href = _safe_href (job .rootcoz_url )
120+ if href :
106121 items .append (
107- f'<li><a href="{ html .escape (job .rootcoz_url , quote = True )} ">'
108- f"{ html .escape (job .job_name )} </a>{ bundle_html } </li>"
122+ f'<li><a href="{ href } ">{ html .escape (job .job_name )} </a>{ bundle_html } </li>'
109123 )
110124 else :
111125 items .append (f"<li>{ html .escape (job .job_name )} { bundle_html } </li>" )
112126 parts .append ("<ul>" + "" .join (items ) + "</ul>" )
113- if len (jobs ) > 20 :
114- parts .append (f"<p><em>+{ len (jobs ) - 20 } more reviewed jobs</em></p>" )
127+ if len (jobs ) > max_links :
128+ remaining = len (jobs ) - max_links
129+ parts .append (f"<p><em>+{ remaining } more reviewed jobs</em></p>" )
115130 parts .append ("</body></html>" )
116131 return "\n " .join (parts )
0 commit comments