Skip to content

Commit

Permalink
Use dataclass for email data
Browse files Browse the repository at this point in the history
  • Loading branch information
mtomilov committed Feb 21, 2025
1 parent 09fcfcf commit 881700a
Show file tree
Hide file tree
Showing 27 changed files with 266 additions and 185 deletions.
21 changes: 11 additions & 10 deletions h/emails/flag_notification.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
from pyramid.renderers import render
from pyramid.request import Request

from h.i18n import TranslationString as _
from h.services.email import EmailTag
from h.services.email import EmailData, EmailTag


def generate(request, email, incontext_link):
"""
Generate an email to notify the group admin when a group member flags an annotation.
def generate(request: Request, email: str, incontext_link: str) -> EmailData:
"""Generate an email to notify the group admin when a group member flags an annotation.
:param request: the current request
:type request: pyramid.request.Request
:param email: the group admin's email address
:type email: text
:param incontext_link: the direct link to the flagged annotation
:type incontext_link: text
:returns: a 4-element tuple containing: recipients, subject, text, html
"""
context = {"incontext_link": incontext_link}

Expand All @@ -28,4 +23,10 @@ def generate(request, email, incontext_link):
"h:templates/emails/flag_notification.html.jinja2", context, request=request
)

return [email], subject, text, EmailTag.FLAG_NOTIFICATION, html
return EmailData(
recipients=[email],
subject=subject,
body=text,
tag=EmailTag.FLAG_NOTIFICATION,
html=html,
)
16 changes: 8 additions & 8 deletions h/emails/mention_notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

from h import links
from h.notification.mention import MentionNotification
from h.services.email import EmailTag
from h.services.email import EmailData, EmailTag


def generate(request: Request, notification: MentionNotification):
def generate(request: Request, notification: MentionNotification) -> EmailData:
selectors = notification.annotation.target[0].get("selector", [])
quote = next((s for s in selectors if s.get("type") == "TextQuoteSelector"), None)
username = notification.mentioning_user.username
Expand All @@ -31,10 +31,10 @@ def generate(request: Request, notification: MentionNotification):
"h:templates/emails/mention_notification.html.jinja2", context, request=request
)

return (
[notification.mentioned_user.email],
subject,
text,
EmailTag.MENTION_NOTIFICATION,
html,
return EmailData(
recipients=[notification.mentioned_user.email],
subject=subject,
body=text,
tag=EmailTag.MENTION_NOTIFICATION,
html=html,
)
20 changes: 9 additions & 11 deletions h/emails/reply_notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,14 @@
from h.models import Subscriptions
from h.notification.reply import Notification
from h.services import SubscriptionService
from h.services.email import EmailTag
from h.services.email import EmailData, EmailTag


def generate(request: Request, notification: Notification):
"""
Generate an email for a reply notification.
def generate(request: Request, notification: Notification) -> EmailData:
"""Generate an email for a reply notification.
:param request: the current request
:param notification: the reply notification data structure
:returns: a 4-element tuple containing: recipients, subject, text, html
"""

unsubscribe_token = request.find_service(SubscriptionService).get_unsubscribe_token(
Expand Down Expand Up @@ -51,10 +49,10 @@ def generate(request: Request, notification: Notification):
"h:templates/emails/reply_notification.html.jinja2", context, request=request
)

return (
[notification.parent_user.email],
subject,
text,
EmailTag.REPLY_NOTIFICATION,
html,
return EmailData(
recipients=[notification.parent_user.email],
subject=subject,
body=text,
tag=EmailTag.REPLY_NOTIFICATION,
html=html,
)
21 changes: 12 additions & 9 deletions h/emails/reset_password.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
from pyramid.renderers import render
from pyramid.request import Request

from h.i18n import TranslationString as _
from h.services.email import EmailTag
from h.models import User
from h.services.email import EmailData, EmailTag


def generate(request, user):
"""
Generate an email for a user password reset request.
def generate(request: Request, user: User) -> EmailData:
"""Generate an email for a user password reset request.
:param request: the current request
:type request: pyramid.request.Request
:param user: the user to whom to send the reset code
:type user: h.models.User
:returns: a 4-element tuple containing: recipients, subject, text, html
"""
serializer = request.registry.password_reset_serializer
code = serializer.dumps(user.username)
Expand All @@ -32,4 +29,10 @@ def generate(request, user):
"h:templates/emails/reset_password.html.jinja2", context, request=request
)

return [user.email], subject, text, EmailTag.RESET_PASSWORD, html
return EmailData(
recipients=[user.email],
subject=subject,
body=text,
tag=EmailTag.RESET_PASSWORD,
html=html,
)
24 changes: 13 additions & 11 deletions h/emails/signup.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
from pyramid.renderers import render
from pyramid.request import Request

from h.i18n import TranslationString as _
from h.services.email import EmailTag
from h.services.email import EmailData, EmailTag


def generate(request, user_id, email, activation_code):
"""
Generate an email for a user signup.
def generate(
request: Request, user_id: int, email: str, activation_code: str
) -> EmailData:
"""Generate an email for a user signup.
:param request: the current request
:type request: pyramid.request.Request
:param user_id: the new user's primary key ID
:type user_id: int
:param email: the new user's email address
:type email: text
:param activation_code: the activation code
:type activation_code: text
:returns: a 4-element tuple containing: recipients, subject, text, html
"""
context = {
"activate_link": request.route_url("activate", id=user_id, code=activation_code)
Expand All @@ -28,4 +24,10 @@ def generate(request, user_id, email, activation_code):
text = render("h:templates/emails/signup.txt.jinja2", context, request=request)
html = render("h:templates/emails/signup.html.jinja2", context, request=request)

return [email], subject, text, EmailTag.ACTIVATION, html
return EmailData(
recipients=[email],
subject=subject,
body=text,
tag=EmailTag.ACTIVATION,
html=html,
)
20 changes: 11 additions & 9 deletions h/emails/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,17 @@
import platform

from pyramid.renderers import render
from pyramid.request import Request

from h import __version__
from h.services.email import EmailTag
from h.services.email import EmailData, EmailTag


def generate(request, recipient):
"""
Generate a test email.
def generate(request: Request, recipient: str) -> EmailData:
"""Generate a test email.
:param request: the current request
:type request: pyramid.request.Request
:param recipient: the recipient of the test email
:type recipient: str
:returns: a 4-element tuple containing: recipients, subject, text, html
"""

context = {
Expand All @@ -29,4 +25,10 @@ def generate(request, recipient):
text = render("h:templates/emails/test.txt.jinja2", context, request=request)
html = render("h:templates/emails/test.html.jinja2", context, request=request)

return [recipient], "Test mail", text, EmailTag.TEST, html
return EmailData(
recipients=[recipient],
subject="Test mail",
body=text,
tag=EmailTag.TEST,
html=html,
)
39 changes: 22 additions & 17 deletions h/services/email.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# noqa: A005

import smtplib
from dataclasses import dataclass
from enum import StrEnum

import pyramid_mailer
Expand All @@ -22,33 +23,37 @@ class EmailTag(StrEnum):
TEST = "test"


@dataclass(frozen=True)
class EmailData:
recipients: list[str]
subject: str
body: str
tag: EmailTag
html: str | None = None

def to_message(self) -> pyramid_mailer.message.Message:
return pyramid_mailer.message.Message(
subject=self.subject,
recipients=self.recipients,
body=self.body,
html=self.html,
extra_headers={"X-MC-Tags": self.tag},
)


class EmailService:
"""A service for sending emails."""

def __init__(self, request: Request, mailer: IMailer) -> None:
self._request = request
self._mailer = mailer

def send(
self,
recipients: list[str],
subject: str,
body: str,
tag: EmailTag,
html: str | None = None,
) -> None:
extra_headers = {"X-MC-Tags": tag}
email = pyramid_mailer.message.Message(
subject=subject,
recipients=recipients,
body=body,
html=html,
extra_headers=extra_headers,
)
def send(self, email: EmailData) -> None:
message = email.to_message()
if self._request.debug: # pragma: no cover
logger.info("emailing in debug mode: check the `mail/` directory")
try:
self._mailer.send_immediately(email)
self._mailer.send_immediately(message)
except smtplib.SMTPRecipientsRefused as exc: # pragma: no cover
logger.warning(
"Recipient was refused when trying to send an email. Does the user have an invalid email address?",
Expand Down
4 changes: 2 additions & 2 deletions h/services/user_signup.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,13 @@ def _require_activation(self, user):
self.session.flush()

# Send the activation email
mail_params = signup.generate(
email = signup.generate(
request=self.request,
user_id=user.id,
email=user.email,
activation_code=user.activation.code,
)
tasks_mailer.send.delay(*mail_params)
tasks_mailer.send.delay(email)


def user_signup_service_factory(_context, request):
Expand Down
8 changes: 4 additions & 4 deletions h/subscribers.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ def send_reply_notifications(event):
if reply_notification.parent_user in mentioned_users:
return

send_params = emails.reply_notification.generate(request, reply_notification)
email = emails.reply_notification.generate(request, reply_notification)
try:
mailer.send.delay(*send_params)
mailer.send.delay(email)
except OperationalError as err: # pragma: no cover
# We could not connect to rabbit! So carry on
report_exception(err)
Expand All @@ -108,9 +108,9 @@ def send_mention_notifications(event):
notifications = mention.get_notifications(request, annotation, event.action)

for notification in notifications:
send_params = emails.mention_notification.generate(request, notification)
email = emails.mention_notification.generate(request, notification)
try:
mailer.send.delay(*send_params)
mailer.send.delay(email)
except OperationalError as err: # pragma: no cover
# We could not connect to rabbit! So carry on
report_exception(err)
1 change: 1 addition & 0 deletions h/tasks/celery.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

celery = Celery("h")
celery.conf.update(
accept_content=["json", "pickle"],
broker_url=os.environ.get(
"CELERY_BROKER_URL",
os.environ.get("BROKER_URL", "amqp://guest:guest@localhost:5672//"),
Expand Down
Loading

0 comments on commit 881700a

Please sign in to comment.