Our app can store data and migrate its schema by now. The next thing almost every real application needs is a way to talk to its users. So let's wire up the built-in mailer and send our first message.
Enabling the mailer
Like every battery, Mail is optioned through a switch. Set EXT_MAIL = 1 and
the framework expands your fluid with a ready-to-use Mail instance. The mailer
speaks SMTP and ships both a synchronous and an asynchronous client, so it fits whatever
half of your app you are calling it from.
All credentials and server settings live in your app config. The only thing the mailer
insists on is that MAIL_USERNAME and MAIL_PASSWORD are either
both present or both absent. Everything else has a sensible default:
[general]
SECRET_KEY = supersecret
[data]
DATABASE_URI = sqlite:///app.db
[extensions]
EXT_SQLALCHEMY = 1
EXT_MAIL = 1
[mail]
MAIL_SERVER = localhost
MAIL_PORT = 1025
MAIL_USE_TLS = 0
MAIL_DEFAULT_SENDER = noreply@example.org
For local testing you do not need a real mail server. A throwaway debugging SMTP is just
one command away: python -m aiosmtpd -n -l localhost:1025. Every mail you send
will then be printed straight to that terminal instead of leaving your machine.
The config values
These are the settings the extension reads when your app initializes:
MAIL_SERVER— SMTP host. Default localhost.MAIL_PORT— SMTP port. Default 587.MAIL_USE_TLS— implicit TLS for the async client. Default True.MAIL_USE_STARTTLS— upgrade the connection via STARTTLS. Default False.MAIL_TIMEOUT— connection timeout in seconds. Default 10.MAIL_USERNAME/MAIL_PASSWORD— login credentials, all or nothing.MAIL_DEFAULT_SENDER— the From address used when you don't pass one.
Sending your first mail
You reach the mailer the same way you reached the database: through the shared extension
registry in webfluid.core.ext. Since our project is growing, this is also a
good moment to give recurring logic a proper home. We'll start a services
package inside the fluid directory and let it own the mailing:
from webfluid.core.ext import mail
def welcome(address: str, value: str):
# The body is a mapping of MIME subtype -> content. So you can
# ship a plain text and an html alternative in one go.
mail.send(
address,
subject="Welcome aboard!",
body={
"plain": f"Saved your model: {value}. Thanks for trying WebFluid!",
"html": f"<p>Saved your model <b>{value}</b>.</p>"
}
)
# send() returns immediately. Per default it hands the actual
# delivery to a background thread (fake_async=True), so your
# request is never blocked by a slow mail server.
And we call it right after a model is created. We keep working on the app factory we switched to in the Migrate chapter:
from webfluid.core.ext import db
from fastapi import Request
from fastapi.exceptions import HTTPException
from sqlalchemy import select
from fluid.models import MyModel
from fluid.services.notify import welcome
async def add_model(request: Request):
data = await request.json()
value = data.get("value")
if not value:
raise HTTPException(status_code=400, detail="Bad Request")
async with db.async_executor(model=MyModel) as e:
model = await e.insert(MyModel(value), flush=True)
# Fire and forget. The threaded sender does the rest.
welcome("friend@example.org", model.value)
return {"model_id": model.id, "value": model.value}
Sync, async and raw clients
The mailer mirrors the rest of the framework: everything has an async counterpart. Inside an async handler you can await the delivery directly and keep full control over when it actually happened:
from webfluid.core.ext import mail
# Blocking, but offloaded to a thread per default:
mail.send("to@example.org", "Subject", {"plain": "Hello!"})
# Truly awaited delivery:
await mail.send_async("to@example.org", "Subject", {"plain": "Hello!"})
# Force the sync send to block instead of threading:
mail.send("to@example.org", "Subject", {"plain": "Hello!"}, fake_async=False)
Both send and send_async accept attachments,
from_email, cc and bcc. An attachment is just a
small dictionary:
await mail.send_async(
"to@example.org",
"Your report",
{"plain": "See attached."},
attachments=[{
"bytes": pdf_bytes, # bytes or str
"type": "pdf", # MIME application subtype
"filename": "report.pdf"
}]
)
And if our conventions ever get in your way, both mail.client() and
mail.async_client() hand you a bare (logged-in) SMTP connection as a context
manager, so you can drive the protocol yourself.
The mailer keeps no connection pool. Every send opens and closes its own SMTP session. That is perfectly fine for transactional mail, but if you are blasting out large batches you'll want to grab a single client() context and reuse it for the whole run.
Continue reading
From here you can continue straight with Babel.