Move the mailer out of the mail router so jobs can send too #453

Closed
opened 2026-09-02 18:35:03 +02:00 by johnsturgeon · 0 comments
Owner

Everything needed to send mail lives inside app/routers/mail.py: the
ConnectionConfig, the Jinja2Templates instance, MessageSchema assembly,
and the FastMail call. A caller that is not a route cannot reach any of it.

app/routers/v2.py already imports send_welcome_email out of the router,
which is the shape working against itself -- a router importing from a router
so it can send an email.

Proposed

Move to app/mailer.py:

conf = ConnectionConfig(...)


async def _send(subject: str, recipients: list[EmailStr], template: str, body: dict) -> None:
    ...


async def send_welcome_email(request: Request, first_name: str, recipients: list[EmailStr]) -> None:
    ...

app/routers/mail.py keeps its two routes and imports from it. v2.py imports
from app.mailer instead of app.routers.mail.

send_welcome_email keeps its Request -- it is sent from routes only, and
url_for is the right way to build its links when a request is in hand.

Scope

Move only. No new senders, no behaviour change. Adding the picks-page-ready
mail (#209) comes after, and is then a new function plus a job rather than a
refactor tangled with a feature.

Notes for whoever does it

fastapi_mail is async and jobs run in APScheduler's thread pool, so a job
calling into this will need asyncio.run(...) at the boundary. Nothing to
solve now, but it is why the module wants to be callable without a request.

email_welcome.j2 never uses the discord_img_url key the body passes it.
Drop it in the move.

Everything needed to send mail lives inside `app/routers/mail.py`: the `ConnectionConfig`, the `Jinja2Templates` instance, `MessageSchema` assembly, and the `FastMail` call. A caller that is not a route cannot reach any of it. `app/routers/v2.py` already imports `send_welcome_email` out of the router, which is the shape working against itself -- a router importing from a router so it can send an email. ## Proposed Move to `app/mailer.py`: ```python conf = ConnectionConfig(...) async def _send(subject: str, recipients: list[EmailStr], template: str, body: dict) -> None: ... async def send_welcome_email(request: Request, first_name: str, recipients: list[EmailStr]) -> None: ... ``` `app/routers/mail.py` keeps its two routes and imports from it. `v2.py` imports from `app.mailer` instead of `app.routers.mail`. `send_welcome_email` keeps its `Request` -- it is sent from routes only, and `url_for` is the right way to build its links when a request is in hand. ## Scope Move only. No new senders, no behaviour change. Adding the picks-page-ready mail (#209) comes after, and is then a new function plus a job rather than a refactor tangled with a feature. ## Notes for whoever does it `fastapi_mail` is async and jobs run in APScheduler's thread pool, so a job calling into this will need `asyncio.run(...)` at the boundary. Nothing to solve now, but it is why the module wants to be callable without a request. `email_welcome.j2` never uses the `discord_img_url` key the body passes it. Drop it in the move.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
johnsturgeon/tgfp-web#453
No description provided.