From cee708f4d8d9d38c4b808c990b4df37dfe033081 Mon Sep 17 00:00:00 2001 From: Campbell Alden Date: Sat, 1 Aug 2026 01:07:58 +0900 Subject: [PATCH] Flesh out the email implementation --- src/config/email.py | 5 +++-- src/services/email.py | 20 ++++++++++++-------- src/utils/__init__.py | 0 src/utils/secret.py | 14 ++++++++++++++ 4 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 src/utils/__init__.py create mode 100644 src/utils/secret.py diff --git a/src/config/email.py b/src/config/email.py index e1d2409..5993919 100644 --- a/src/config/email.py +++ b/src/config/email.py @@ -1,3 +1,4 @@ +from src.utils.secret import SecretBox from src.config.parse import assert_key_of_type from typing import Any from dataclasses import dataclass @@ -8,11 +9,11 @@ EmailAddress = str @dataclass class Email: - bird_api_key: str + bird_api_key: SecretBox[str] sender: EmailAddress @classmethod def from_dict(cls, config: dict[str, Any]) -> 'Email': assert_key_of_type(config, 'bird_api_key', str) assert_key_of_type(config, 'sender', str) - return Email(bird_api_key=config['bird_api_key'], sender=config['sender']) + return Email(bird_api_key=SecretBox(config['bird_api_key']), sender=config['sender']) diff --git a/src/services/email.py b/src/services/email.py index d6c4f5c..dc51ad0 100644 --- a/src/services/email.py +++ b/src/services/email.py @@ -1,5 +1,5 @@ -from dataclasses import dataclass, field -from email.utils import formatdate, make_msgid +import requests +from dataclasses import dataclass import abc from ..config.email import Email as EmailConfig, EmailAddress @@ -17,10 +17,6 @@ class EmailDTO: text: str # HTML alternate content. (Optional: Textual content is required) html: str | None = None - # A unique ID for identifying this Email - message_id: str = field(default_factory=make_msgid) - # The date that the email was sent - date: str = field(default_factory=lambda: formatdate(localtime=True)) class EmailService: @@ -30,12 +26,20 @@ class EmailService: class BirdEmailServiceImpl(EmailService): + BIRD_API_ENDPOINT = 'https://eu1.platform.bird.com/v1/email/messages' + def __init__(self, config: EmailConfig): self._config = config def send_email(self, email: EmailDTO): - # TODO: Implement Bird Specific Email sending and Error handling - pass + payload = {'from': email.sender, 'to': email.to, 'subject': email.subject, 'text': email.text} + + if email.html: + payload['html'] = email.html + + headers = {'Authorization': f'Bearer {self._config.bird_api_key.expose_secret()}'} + response = requests.post(BirdEmailServiceImpl.BIRD_API_ENDPOINT, json=payload, headers=headers) + return response.ok def get_service(config: EmailConfig) -> EmailService: diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/secret.py b/src/utils/secret.py new file mode 100644 index 0000000..1828827 --- /dev/null +++ b/src/utils/secret.py @@ -0,0 +1,14 @@ +class SecretBox[T]: + """A helper abstraction for making sure secrets aren't leaked accidentally""" + + def __init__(self, item: T): + self._item = item + + def expose_secret(self) -> T: + return self._item + + def __str__(self): + return '' + + def __repr__(self): + return f'SecretBox<{type(self._item)}>'