Coverage for app / backend / src / couchers / email / queuing.py: 100%
36 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-13 02:44 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-13 02:44 +0000
1from pathlib import Path
2from typing import Any
4import yaml
5from sqlalchemy.orm.session import Session
7from couchers.config import config
8from couchers.i18n import LocalizationContext
9from couchers.jobs.enqueue import queue_job
10from couchers.metrics import emails_counter
11from couchers.proto.internal import jobs_pb2
12from couchers.templating import Jinja2Template, template_folder
13from couchers.utils import now
16def _queue_email(session: Session, payload: jobs_pb2.SendEmailPayload) -> None:
17 """
18 This indirection is so that this can be easily mocked. Not sure how to do it better :(
19 """
21 # Import here to avoid circular dependency
22 from couchers.jobs.handlers import send_email # noqa: PLC0415
24 queue_job(
25 session,
26 job=send_email,
27 payload=payload,
28 priority=5,
29 )
31 emails_counter.inc()
34def queue_email(session: Session, payload: jobs_pb2.SendEmailPayload) -> None:
35 _queue_email(session, payload)
38def queue_userless_email(
39 session: Session, recipient: str, subject: str, template_name: str, template_args: dict[str, Any]
40) -> None:
41 """
42 This is a simplified version of couchers.notifications.background._send_email_notification
44 It's for the few security emails where we don't have a user to email but send directly to an email address.
45 """
47 # Not yet localizable
48 loc_context = LocalizationContext.en_utc()
50 template_args = {
51 **template_args,
52 "header_subject": subject,
53 "footer_timezone_name": loc_context.localized_timezone,
54 "footer_copyright_year": now().year,
55 "footer_email_is_critical": True, # Results in no unsubscribe footer.
56 }
58 # Format plaintext template
59 plain_tmplt_body = (template_folder / f"{template_name}.txt").read_text()
60 plain_tmplt_footer = (template_folder / "_footer.txt").read_text()
61 plain_tmplt = Jinja2Template(source=plain_tmplt_body + plain_tmplt_footer, html=False)
62 plain = plain_tmplt.render(template_args, loc_context)
64 # Format html template
65 html_tmplt = Jinja2Template(
66 source=(template_folder / "generated_html" / f"{template_name}.html").read_text(), html=True
67 )
68 html = html_tmplt.render(template_args, loc_context)
70 queue_email(
71 session,
72 jobs_pb2.SendEmailPayload(
73 sender_name=config["NOTIFICATION_EMAIL_SENDER"],
74 sender_email=config["NOTIFICATION_EMAIL_ADDRESS"],
75 recipient=recipient,
76 subject=config["NOTIFICATION_PREFIX"] + subject,
77 plain=plain,
78 html=html,
79 source_data=config["VERSION"] + f"/{template_name}",
80 ),
81 )
84_system_email_templates_dir = Path(__file__).parent / ".." / ".." / ".." / "templates" / "system"
87def queue_system_email(session: Session, recipient: str, template_name: str, template_args: dict[str, Any]) -> None:
88 source = (_system_email_templates_dir / f"{template_name}.md").read_text(encoding="utf8")
89 _, frontmatter_source, text_source = source.split("---", 2)
91 loc_context = LocalizationContext.en_utc()
92 rendered_frontmatter = Jinja2Template(source=frontmatter_source, html=False).render(template_args, loc_context)
93 frontmatter = yaml.load(rendered_frontmatter, Loader=yaml.FullLoader)
95 plain = Jinja2Template(source=text_source.strip(), html=False).render(template_args, loc_context)
97 queue_email(
98 session,
99 jobs_pb2.SendEmailPayload(
100 sender_name=config["NOTIFICATION_EMAIL_SENDER"],
101 sender_email=config["NOTIFICATION_EMAIL_ADDRESS"],
102 recipient=recipient,
103 subject=config["NOTIFICATION_PREFIX"] + frontmatter["subject"],
104 plain=plain,
105 source_data=template_name,
106 ),
107 )