Coverage for app/backend/src/couchers/email/queuing.py: 95%
35 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-28 16:00 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-28 16:00 +0000
1from pathlib import Path
2from typing import Any
4import yaml
5from sqlalchemy.orm.session import Session
7from couchers.config import config
8from couchers.context import CouchersContext
9from couchers.email.blocks import EmailBase, EmailFooter
10from couchers.email.rendering import render_email
11from couchers.i18n import LocalizationContext
12from couchers.jobs.enqueue import queue_job
13from couchers.metrics import emails_counter
14from couchers.proto.internal import jobs_pb2
15from couchers.templating import Jinja2Template
16from couchers.utils import now
19def _queue_email(session: Session, payload: jobs_pb2.SendEmailPayload) -> None:
20 """
21 This indirection is so that this can be easily mocked. Not sure how to do it better :(
22 """
24 # Import here to avoid circular dependency
25 from couchers.jobs.handlers import send_email # noqa: PLC0415
27 queue_job(
28 session,
29 job=send_email,
30 payload=payload,
31 priority=5,
32 )
34 emails_counter.inc()
37def queue_email(session: Session, payload: jobs_pb2.SendEmailPayload) -> None:
38 _queue_email(session, payload)
41def queue_userless_email(
42 context: CouchersContext, session: Session, recipient: str, email: EmailBase, source_data_header: str
43) -> None:
44 """
45 This is a simplified version of couchers.notifications.background._send_email_notification
47 It's for the few security emails where we don't have a user to email but send directly to an email address.
48 """
50 loc_context = context.localization
51 if not context.get_boolean_value("notification_translations_enabled", default=False): 51 ↛ 52line 51 didn't jump to line 52 because the condition on line 51 was never true
52 loc_context = LocalizationContext(locale="en", timezone=loc_context.timezone)
54 footer = EmailFooter(timezone_name=loc_context.localized_timezone, copyright_year=now().year, unsubscribe_info=None)
55 rendered = render_email(email, footer, loc_context)
57 queue_email(
58 session,
59 jobs_pb2.SendEmailPayload(
60 sender_name=config.NOTIFICATION_EMAIL_SENDER,
61 sender_email=config.NOTIFICATION_EMAIL_ADDRESS,
62 recipient=recipient,
63 subject=config.NOTIFICATION_PREFIX + rendered.subject,
64 plain=rendered.body_plaintext,
65 html=rendered.body_html,
66 html_related_parts=rendered.html_image_parts,
67 source_data=f"{source_data_header}; version={config.VERSION}",
68 ),
69 )
72_system_email_templates_dir = Path(__file__).parent / ".." / ".." / ".." / "templates" / "system"
75def queue_system_email(session: Session, recipient: str, template_name: str, template_args: dict[str, Any]) -> None:
76 source = (_system_email_templates_dir / f"{template_name}.md").read_text(encoding="utf8")
77 _, frontmatter_source, text_source = source.split("---", 2)
79 rendered_frontmatter = Jinja2Template(source=frontmatter_source, html=False).render(template_args)
80 frontmatter = yaml.load(rendered_frontmatter, Loader=yaml.FullLoader)
82 plain = Jinja2Template(source=text_source.strip(), html=False).render(template_args)
84 queue_email(
85 session,
86 jobs_pb2.SendEmailPayload(
87 sender_name=config.NOTIFICATION_EMAIL_SENDER,
88 sender_email=config.NOTIFICATION_EMAIL_ADDRESS,
89 recipient=recipient,
90 subject=config.NOTIFICATION_PREFIX + frontmatter["subject"],
91 plain=plain,
92 source_data=template_name,
93 ),
94 )