Coverage for app/backend/src/couchers/email/queuing.py: 95%
49 statements
« prev ^ index » next coverage.py v7.17.3, created at 2026-11-12 12:41 +0000
« prev ^ index » next coverage.py v7.17.3, created at 2026-11-12 12:41 +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.email.zz import add_open_pixel, add_zz_to_html_links, generate_zz
12from couchers.event_log import log_event
13from couchers.i18n import LocalizationContext
14from couchers.jobs.enqueue import queue_job
15from couchers.metrics import emails_counter
16from couchers.proto.internal import jobs_pb2
17from couchers.templating import Jinja2Template
18from couchers.utils import now
21def _queue_email(session: Session, payload: jobs_pb2.SendEmailPayload) -> None:
22 """
23 This indirection is so that this can be easily mocked. Not sure how to do it better :(
24 """
26 # Import here to avoid circular dependency
27 from couchers.jobs.handlers import send_email # noqa: PLC0415
29 queue_job(
30 session,
31 job=send_email,
32 payload=payload,
33 priority=15,
34 )
36 emails_counter.inc()
39def queue_email(session: Session, payload: jobs_pb2.SendEmailPayload) -> None:
40 _queue_email(session, payload)
43def queue_email_with_zz(context: CouchersContext, session: Session, payload: jobs_pb2.SendEmailPayload) -> None:
44 """
45 Queues an email, adding a zz id to its html links (see couchers/email/zz.py) and logging it as sent.
46 """
47 zz = generate_zz()
48 if payload.html: 48 ↛ 54line 48 didn't jump to line 54 because the condition on line 48 was always true
49 if context.get_boolean_value("email_zz_links_enabled", default=False):
50 payload.html = add_zz_to_html_links(payload.html, zz)
51 if context.get_boolean_value("email_open_pixel_enabled", default=False):
52 payload.html = add_open_pixel(payload.html, zz)
54 properties: dict[str, str | int] = {"zz": zz, "source_data": payload.source_data}
55 if payload.notification_id:
56 properties["notification_id"] = payload.notification_id
57 log_event(context, session, "email.sent", properties)
59 queue_email(session, payload)
62def queue_userless_email(
63 context: CouchersContext, session: Session, recipient: str, email: EmailBase, source_data_header: str
64) -> None:
65 """
66 This is a simplified version of couchers.notifications.background._send_email_notification
68 It's for the few security emails where we don't have a user to email but send directly to an email address.
69 """
71 loc_context = context.localization
72 if not context.get_boolean_value("notification_translations_enabled", default=True): 72 ↛ 73line 72 didn't jump to line 73 because the condition on line 72 was never true
73 loc_context = LocalizationContext(locale="en", timezone=loc_context.timezone)
75 footer = EmailFooter(copyright_year=now().year, unsubscribe_info=None)
76 rendered = render_email(email, footer, loc_context)
78 queue_email_with_zz(
79 context,
80 session,
81 jobs_pb2.SendEmailPayload(
82 sender_name=config.NOTIFICATION_EMAIL_SENDER,
83 sender_email=config.NOTIFICATION_EMAIL_ADDRESS,
84 recipient=recipient,
85 subject=config.NOTIFICATION_PREFIX + rendered.subject,
86 plain=rendered.body_plaintext,
87 html=rendered.body_html,
88 html_related_parts=rendered.html_image_parts,
89 source_data=f"{source_data_header}; version={config.VERSION}",
90 ),
91 )
94_system_email_templates_dir = Path(__file__).parent / ".." / ".." / ".." / "templates" / "system"
97def queue_system_email(
98 session: Session,
99 recipient: str,
100 template_name: str,
101 template_args: dict[str, Any],
102 attachments: list[jobs_pb2.EmailPart] | None = None,
103) -> None:
104 source = (_system_email_templates_dir / f"{template_name}.md").read_text(encoding="utf8")
105 _, frontmatter_source, text_source = source.split("---", 2)
107 rendered_frontmatter = Jinja2Template(source=frontmatter_source, html=False).render(template_args)
108 frontmatter = yaml.load(rendered_frontmatter, Loader=yaml.FullLoader)
110 plain = Jinja2Template(source=text_source.strip(), html=False).render(template_args)
112 queue_email(
113 session,
114 jobs_pb2.SendEmailPayload(
115 sender_name=config.NOTIFICATION_EMAIL_SENDER,
116 sender_email=config.NOTIFICATION_EMAIL_ADDRESS,
117 recipient=recipient,
118 subject=config.NOTIFICATION_PREFIX + frontmatter["subject"],
119 plain=plain,
120 source_data=template_name,
121 attachments=attachments or [],
122 ),
123 )