feat: welcome email, prettier digest, complete ROADMAP 1.2#52
Conversation
There was a problem hiding this comment.
Pull request overview
This PR completes ROADMAP 1.2 by implementing a welcome email feature, enhancing digest email styling with card-based layouts, and adding location-based personalization. The changes improve the user experience by providing clearer email communications with better visual hierarchy and more contextual information.
Changes:
- Added welcome email sent after DOI confirmation with subscription details and privacy policy link
- Enhanced digest email with card-style job listings, score pill badges, location pins, match statistics, and target location in header
- Extended test coverage from 7 to 22 tests for email functionality
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
tests/test_emailer.py |
Added 15 new tests covering location display, match statistics, subscriber explanation, and comprehensive welcome email functionality |
stellenscout/pages/verify.py |
Added fire-and-forget welcome email sending after successful subscription confirmation with proper secret injection for email service |
stellenscout/emailer.py |
Refactored job rows from table to card layout, added send_welcome_email() function, updated send_daily_digest() to accept target_location parameter, added match statistics and subscriber explanation |
daily_task.py |
Added location field to job dictionaries and passed target_location to send_daily_digest() |
ROADMAP.md |
Marked all 1.2 sub-items as completed |
AGENTS.md |
Updated documentation to reflect new email templates and verification page behavior |
Comments suppressed due to low confidence (4)
stellenscout/emailer.py:214
- User-provided privacy_url is inserted directly into an href attribute without validation or escaping. While href attributes have some protection in modern email clients, this could still be exploited with javascript: URLs or data: URLs. Consider validating that the URL starts with http:// or https:// and escaping it with
html.escape()or using a URL validation library.
privacy_line = (
f'<p style="margin-top:8px"><a href="{privacy_url}" style="color:#9ca3af">Privacy Policy</a></p>'
stellenscout/emailer.py:57
- The impressum line constructed from environment variables (IMPRESSUM_NAME, IMPRESSUM_ADDRESS, IMPRESSUM_EMAIL) is inserted directly into HTML without escaping. While these are typically controlled by the application operator, they still represent configuration data that should be HTML-escaped for defense in depth. Consider using
html.escape()from Python's standard library.
def _impressum_line() -> str:
"""Return a one-line impressum string for email footers (§ 5 DDG)."""
name = os.environ.get("IMPRESSUM_NAME", "")
address = os.environ.get("IMPRESSUM_ADDRESS", "").replace("\n", ", ")
email = os.environ.get("IMPRESSUM_EMAIL", "")
parts = [p for p in (name, address, email) if p]
return " · ".join(parts) if parts else "StellenScout"
stellenscout/emailer.py:127
- The unsubscribe URL is inserted directly into an href attribute without escaping. While this URL is generated server-side, using
html.escape()would provide defense in depth against potential injection if the URL generation logic changes. The same applies to the apply_url on line 40 which comes from external SerpApi data.
{f'<br><a href="{unsubscribe_url}" style="color:#9ca3af">Unsubscribe</a>' if unsubscribe_url else ""}
stellenscout/emailer.py:40
- The apply_url (from external SerpApi data) is inserted directly into an href attribute without validation or escaping. This could allow javascript: or data: URLs to be injected. Consider validating that URLs start with http:// or https:// and using
html.escape()for defense in depth.
<a href="{apply_url}"
stellenscout/emailer.py
Outdated
| impressum = _impressum_line() | ||
|
|
||
| location_subtitle = ( | ||
| f'<p style="margin:4px 0 0;opacity:.85;font-size:14px">Jobs in {target_location}</p>' if target_location else "" |
There was a problem hiding this comment.
User-provided target_location is inserted directly into HTML without escaping, creating a potential XSS vulnerability. Although this value comes from the database and was entered by the subscriber themselves, it's still untrusted user input that should be escaped before inserting into HTML. Consider using html.escape() from Python's standard library.
stellenscout/emailer.py
Outdated
| location_line = ( | ||
| f"<p>Starting tomorrow, you'll receive a daily email with AI-matched " | ||
| f"jobs in <strong>{target_location}</strong>.</p>" |
There was a problem hiding this comment.
User-provided target_location is inserted directly into HTML without escaping in the welcome email. This creates a potential XSS vulnerability. Although this value comes from the database, it's untrusted user input that should be HTML-escaped. Consider using html.escape() from Python's standard library.
stellenscout/emailer.py
Outdated
| <div style="font-weight:bold;font-size:15px;color:#111827">{job["title"]}</div> | ||
| <div style="color:#6b7280;font-size:14px;margin-top:2px">{job["company"]}</div> |
There was a problem hiding this comment.
Job title and company name from external API (SerpApi) are inserted directly into HTML without escaping, creating a Cross-Site Scripting (XSS) vulnerability in email content. If SerpApi returns malicious HTML/JavaScript in job titles or company names, it will be rendered in recipient email clients. The same issue exists with the location field on line 22. Consider using html.escape() from Python's standard library to escape these values before inserting them into HTML.
c90c0c5 to
994e225
Compare
Summary
send_welcome_email()sent after DOI confirmation inpages/verify.py(fire-and-forget; failure doesn't affect confirmation flow)send_daily_digest()updated — acceptstarget_location; job dicts now includelocationfield fromdaily_task.pyPartially addresses #28 (improve job cards) for digest emails — apply links are now prominent "View Job" buttons in card layout.
Test plan
ruff check . && ruff format --check .— all cleanmypy .— no issuespytest tests/ -x -q— 181 passed🤖 Generated with Claude Code