Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update email sending to be configured as a service in a container (PP-893) #1654

Merged
merged 5 commits into from
Feb 8, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 5 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -286,18 +286,14 @@ Local analytics are enabled by default. S3 analytics can be enabled via the foll

#### Email

### Email sending

To use the features that require sending emails, for example to reset the password for logged-out users, you will need
to have a working SMTP server and set some environment variables:

```sh
export SIMPLIFIED_MAIL_SERVER=example.smtp.com
export SIMPLIFIED_MAIL_PORT=465
export SIMPLIFIED_MAIL_USERNAME=username
export SIMPLIFIED_MAIL_PASSWORD=password
export SIMPLIFIED_MAIL_SENDER=sender@example.com
```
- `PALACE_MAIL_SERVER` - The SMTP server to use. Required if you want to send emails.
- `PALACE_MAIL_PORT` - The port of the SMTP server. Default: 25. (optional)
- `PALACE_MAIL_USERNAME` - The username to use when connecting to the SMTP server. (optional)
- `PALACE_MAIL_PASSWORD` - The password to use when connecting to the SMTP server. (optional)
- `PALACE_MAIL_SENDER` - The email address to use as the sender of the emails. (optional)

## Running the Application

3 changes: 2 additions & 1 deletion api/admin/controller/base.py
Original file line number Diff line number Diff line change
@@ -22,11 +22,12 @@ class AdminController:
def __init__(self, manager):
self.manager = manager
self._db = self.manager._db
self.send_email = self.manager.services.email.send_email

@property
def admin_auth_providers(self):
if Admin.with_password(self._db).count() != 0:
return [PasswordAdminAuthenticationProvider()]
return [PasswordAdminAuthenticationProvider(self.send_email)]

return []

19 changes: 16 additions & 3 deletions api/admin/password_admin_authentication_provider.py
Original file line number Diff line number Diff line change
@@ -14,11 +14,12 @@
)
from core.model import Admin, Key
from core.model.key import KeyType
from core.util.email import EmailManager
from core.service.email.email import SendEmailCallable
from core.util.log import LoggerMixin
from core.util.problem_detail import ProblemDetail


class PasswordAdminAuthenticationProvider(AdminAuthenticationProvider):
class PasswordAdminAuthenticationProvider(AdminAuthenticationProvider, LoggerMixin):
NAME = "Password Auth"

SIGN_IN_TEMPLATE = sign_in_template.format(
@@ -33,6 +34,9 @@
label=label_style, input=input_style, button=button_style
)

def __init__(self, send_email: SendEmailCallable):
self.send_email = send_email

@staticmethod
def get_secret_key(db: Session) -> str:
key = Key.get_key(db, KeyType.ADMIN_SECRET_KEY, raise_exception=True).value
@@ -98,6 +102,13 @@

def send_reset_password_email(self, admin: Admin, reset_password_url: str) -> None:
subject = f"{AdminClientConfig.APP_NAME} - Reset password email"
if admin.email is None:
# This should never happen, but if it does, we should log it.
self.log.error(

Check warning on line 107 in api/admin/password_admin_authentication_provider.py

Codecov / codecov/patch

api/admin/password_admin_authentication_provider.py#L107

Added line #L107 was not covered by tests
"Admin has no email address, cannot send reset password email."
)
return

Check warning on line 110 in api/admin/password_admin_authentication_provider.py

Codecov / codecov/patch

api/admin/password_admin_authentication_provider.py#L110

Added line #L110 was not covered by tests

receivers = [admin.email]

mail_text = render_template_string(
@@ -111,7 +122,9 @@
reset_password_url=reset_password_url,
)

EmailManager.send_email(subject, receivers, text=mail_text, html=mail_html)
self.send_email(
subject=subject, receivers=receivers, text=mail_text, html=mail_html
)

def validate_token_and_extract_admin(
self, reset_password_token: str, admin_id: int, _db: Session
5 changes: 2 additions & 3 deletions core/jobs/playtime_entries.py
Original file line number Diff line number Diff line change
@@ -19,7 +19,6 @@
from core.model.identifier import Identifier, RecursiveEquivalencyCache
from core.model.time_tracking import PlaytimeEntry, PlaytimeSummary
from core.util.datetime_helpers import previous_months, utc_now
from core.util.email import EmailManager
from scripts import Script

if TYPE_CHECKING:
@@ -207,8 +206,8 @@ def do_run(self):
Configuration.REPORTING_EMAIL_ENVIRONMENT_VARIABLE
)
if recipient:
EmailManager.send_email(
email_subject,
self.services.email.send_email(
subject=email_subject,
receivers=[recipient],
text="",
attachments={attachment_name: temp.read()},
8 changes: 8 additions & 0 deletions core/service/container.py
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@

from core.service.analytics.configuration import AnalyticsConfiguration
from core.service.analytics.container import AnalyticsContainer
from core.service.email.configuration import EmailConfiguration
from core.service.email.container import Email
from core.service.logging.configuration import LoggingConfiguration
from core.service.logging.container import Logging
from core.service.search.configuration import SearchConfiguration
@@ -37,6 +39,11 @@ class Services(DeclarativeContainer):
config=config.search,
)

email = Container(
Email,
config=config.email,
)


def wire_container(container: Services) -> None:
container.wire(
@@ -67,6 +74,7 @@ def create_container() -> Services:
"logging": LoggingConfiguration().dict(),
"analytics": AnalyticsConfiguration().dict(),
"search": SearchConfiguration().dict(),
"email": EmailConfiguration().dict(),
}
)
wire_container(container)
14 changes: 14 additions & 0 deletions core/service/email/configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from pydantic import EmailStr, PositiveInt

from core.service.configuration import ServiceConfiguration


class EmailConfiguration(ServiceConfiguration):
class Config:
env_prefix = "PALACE_MAIL_"

server: str | None = None
port: PositiveInt = 25
username: str | None = None
password: str | None = None
sender: EmailStr | None = None
24 changes: 24 additions & 0 deletions core/service/email/container.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from dependency_injector import providers
from dependency_injector.containers import DeclarativeContainer
from dependency_injector.providers import Provider
from redmail import EmailSender

from core.service.email.email import SendEmailCallable, emailer_factory, send_email


class Email(DeclarativeContainer):
config = providers.Configuration()

emailer: Provider[EmailSender] = providers.Singleton(
emailer_factory,
host=config.server,
port=config.port,
username=config.username,
password=config.password,
)

send_email: SendEmailCallable = providers.Callable(
send_email,
emailer=emailer,
sender=config.sender,
)
59 changes: 59 additions & 0 deletions core/service/email/email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import os
from email.message import EmailMessage
from typing import Any, Protocol

from redmail import EmailSender

from core.config import CannotLoadConfiguration


def emailer_factory(
host: str | None, port: int, username: str | None, password: str | None
) -> EmailSender:
if host is None:
raise CannotLoadConfiguration(
"Mail server must be provided. Please set PALACE_MAIL_SERVER."
)

return EmailSender(
host=host,
port=port,
# Username and password are ignored here because the emailer library has them
# as required, but defaults them to None. So their types are not correct.
# PR here to fix the type hint upstream: https://github.com/Miksus/red-mail/pull/90
username=username, # type: ignore[arg-type]
password=password, # type: ignore[arg-type]
)


def send_email(
*,
emailer: EmailSender,
sender: str,
subject: str,
receivers: list[str] | str,
html: str | None = None,
text: str | None = None,
attachments: dict[str, str | os.PathLike[Any] | bytes] | None = None,
) -> EmailMessage:
return emailer.send(
subject=subject,
sender=sender,
receivers=receivers,
text=text,
html=html,
attachments=attachments,
)


class SendEmailCallable(Protocol):
def __call__(
self,
*,
subject: str,
receivers: list[str] | str,
html: str | None = None,
text: str | None = None,
attachments: dict[str, str | os.PathLike[Any] | bytes] | None = None,
) -> EmailMessage:
...
37 changes: 0 additions & 37 deletions core/util/email.py

This file was deleted.

Loading