83:def validate_html(html: str) -> List[str]:
84-    """Basic structural validation of rendered HTML. Returns list of problems."""
85-    problems = []
86-    if not html or not html.strip():
87-        problems.append("Empty HTML body")
88-        return problems
89-    from bs4 import BeautifulSoup
90-    soup = BeautifulSoup(html, "lxml")
91-    if soup.find(True) is None:
92-        problems.append("No HTML elements found")
93-    for tag in soup.find_all(["script", "iframe", "object", "embed", "form"]):
94-        problems.append(f"Disallowed tag <{tag.name}> present")
95-    for img in soup.find_all("img"):
96-        if not (img.get("src") or "").strip():
97-            problems.append("img without src")
98-    leftovers = re.findall(r"%([A-Za-z0-9_]+)%", html)
99-    if leftovers:
100-        problems.append(f"Unsubstituted variables remain: {sorted(set(leftovers))[:10]}")
101-    return problems
102-
103-
104-def html_to_pdf(html: str) -> bytes:
105-    """Real HTML → PDF conversion via WeasyPrint."""
106-    try:
107-        return WeasyHTML(string=html).write_pdf()
108-    except Exception as e:
===CALLERS===
app/tasks/send_tasks.py:196:        html_problems = message_builder.validate_html(result_html)
app/api/routers/templates.py:183:    problems = message_builder.validate_html(res.text)
app/api/routers/attachments.py:131:        problems = validate_html(html_body)
app/api/routers/attachments.py:181:    problems = validate_html(tpl.body_html)
app/api/routers/test_send.py:67:    problems = message_builder.validate_html(body_html) if body_html else {}
