Skip to main content

Email Utility Services

The email utility services in this project provide a structured framework for generating, sending, and testing system emails. This system is used for critical user flows such as password recovery and account creation, as well as administrative verification of SMTP configurations.

The EmailData Contract

The core of the email system is the EmailData dataclass, located in backend/app/utils.py. It serves as a standardized container for email content after it has been rendered from a template but before it is dispatched via SMTP.

@dataclass
class EmailData:
html_content: str
subject: str

Functions that generate specific types of emails, such as generate_test_email, generate_reset_password_email, and generate_new_account_email, all return an instance of EmailData. This ensures that the dispatch logic in send_email receives a consistent interface regardless of the email's purpose.

Backend Email Engine

The backend implements a two-stage process for email delivery: template rendering and SMTP dispatch.

Template Rendering

The render_email_template function uses Jinja2 to inject dynamic data into HTML templates. These templates are expected to be located in the backend/app/email-templates/build/ directory.

def render_email_template(*, template_name: str, context: dict[str, Any]) -> str:
template_str = (
Path(__file__).parent / "email-templates" / "build" / template_name
).read_text()
html_content = Template(template_str).render(context)
return html_content

SMTP Dispatch

The send_email function handles the actual delivery using the emails library. It is highly dependent on the project's configuration settings. A critical safety check is performed at the start of the function: it will raise an AssertionError if settings.emails_enabled is False.

def send_email(
*,
email_to: str,
subject: str = "",
html_content: str = "",
) -> None:
assert settings.emails_enabled, "no provided configuration for email variables"
message = emails.Message(
subject=subject,
html=html_content,
mail_from=(settings.EMAILS_FROM_NAME, settings.EMAILS_FROM_EMAIL),
)
# ... SMTP configuration logic ...
response = message.send(to=email_to, smtp=smtp_options)
logger.info(f"send email result: {response}")

Administrative Testing Integration

The project includes a dedicated utility service for testing email delivery from the frontend. This is primarily used by administrators to verify that SMTP settings are correctly configured.

Frontend UtilsService

The UtilsService class in frontend/src/client/sdk.gen.ts provides a static method to trigger a test email.

class UtilsService {
public static testEmail(data: UtilsTestEmailData): CancelablePromise<UtilsTestEmailResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/utils/test-email/',
query: {
email_to: data.emailTo
},
errors: {
422: 'Validation Error'
}
});
}
}

Backend Test Route

The corresponding backend route in backend/app/api/routes/utils.py is protected by the get_current_active_superuser dependency, ensuring only administrators can trigger test emails.

@router.post(
"/test-email/",
dependencies=[Depends(get_current_active_superuser)],
status_code=201,
)
def test_email(email_to: EmailStr) -> Message:
email_data = generate_test_email(email_to=email_to)
send_email(
email_to=email_to,
subject=email_data.subject,
html_content=email_data.html_content,
)
return Message(message="Test email sent")

Configuration Requirements

For the email services to function, several environment variables must be configured in the backend. These are managed via the Settings class in backend/app/core/config.py.

SettingDescription
SMTP_HOSTThe address of the SMTP server.
SMTP_PORTThe port for the SMTP server (default: 587).
EMAILS_FROM_EMAILThe sender address for all outgoing emails.
emails_enabledA boolean flag derived from the presence of SMTP settings. If False, send_email will fail.

Verification in Development

In development and testing environments, the project uses Mailcatcher to intercept and verify outgoing emails without sending them to real addresses. This is demonstrated in the Playwright integration tests found in frontend/tests/reset-password.spec.ts.

The tests use a helper to find the last email sent to a specific recipient and then navigate to the Mailcatcher web interface to verify the content:

const emailData = await findLastEmail({
request,
filter: (e) => e.recipients.includes(`<${email}>`),
timeout: 5000,
})

await page.goto(
`${process.env.MAILCATCHER_HOST}/messages/${emailData.id}.html`,
)

This approach allows for end-to-end verification of the email flow—from the user triggering an action (like "Forgot Password") to the actual delivery of the rendered HTML content.