Skip to content

feat: welcome email, prettier digest, complete ROADMAP 1.2#52

Merged
TheTrueAI merged 1 commit intomainfrom
pimp-email
Feb 26, 2026
Merged

feat: welcome email, prettier digest, complete ROADMAP 1.2#52
TheTrueAI merged 1 commit intomainfrom
pimp-email

Conversation

@TheTrueAI
Copy link
Owner

@TheTrueAI TheTrueAI commented Feb 26, 2026

Summary

  • Welcome email — new send_welcome_email() sent after DOI confirmation in pages/verify.py (fire-and-forget; failure doesn't affect confirmation flow)
  • Prettier digest emails — card-style job listings with score pill badges, location pins, "View Job" CTA buttons, match summary stats (excellent/good counts), target location in header, viewport meta for mobile, subscriber explanation in footer
  • send_daily_digest() updated — accepts target_location; job dicts now include location field from daily_task.py
  • ROADMAP 1.2 completed — all 6 sub-items checked off (cron job, secrets, full cycle test, welcome email, unsubscribe link, prettier digest)
  • 22 emailer tests (up from 7) covering cards, location rendering, match stats, welcome email content

Partially 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 clean
  • mypy . — no issues
  • pytest tests/ -x -q — 181 passed
  • Manual: subscribe with test email → click verify link → check welcome email arrives → trigger digest workflow → check digest email layout → click unsubscribe

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings February 26, 2026 15:00
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}"

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 ""
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 207 to 209
location_line = (
f"<p>Starting tomorrow, you'll receive a daily email with AI-matched "
f"jobs in <strong>{target_location}</strong>.</p>"
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 31 to 32
<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>
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@TheTrueAI TheTrueAI merged commit 5140456 into main Feb 26, 2026
1 check passed
@TheTrueAI TheTrueAI deleted the pimp-email branch February 26, 2026 15:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants