Correo¶
El correo en milpa sigue el patrón Mailable de Laravel: una clase encapsula "qué
correo es" (asunto, template, contexto, adjuntos) y la facade Mail lo envía, síncrono
o encolado.
from milpa.Core.Mail import Mail
Mail.send(WelcomeMailable(name="Calcifux"), to=["calcifux@example.com"])
Anatomía: Mailable + MailContent¶
Un Mailable hereda de la ABC Mailable (milpa/Core/Mail/Mailable.py) e implementa
build(), que devuelve un MailContent:
from milpa.Core.Mail.Mailable import Mailable, MailContent
from milpa.Core.Translate import t, current_locale
class WelcomeMailable(Mailable):
def __init__(self, name: str):
# SOLO primitivos serializables (ver "Encolar" más abajo).
self._name = name
def build(self) -> MailContent:
return MailContent(
subject=t("emails.welcome.subject", {"name": self._name}),
template="mymodule/mail/welcome.html.j2",
context={"name": self._name, "locale": current_locale()},
)
build() arma el payload puro — no toca SMTP ni Jinja directamente.
Campos de MailContent¶
| Campo | Tipo | Para qué | Laravel |
|---|---|---|---|
subject |
str |
Asunto (ya traducido por ti). | ->subject() |
template |
str |
Ruta del template Jinja (compartido o modulo/...). |
->view() |
context |
dict |
Variables del template. | ->with() |
from_email / from_name |
str \| None |
Remitente; si None, usa el default de settings. |
->from() |
inline_assets |
dict[str, Path] |
CID → ruta (imagen embebida). En HTML: <img src="cid:logo">. |
$message->embed() |
attachments |
list[Path] |
Adjuntos por ruta (archivos en disco). | ->attach() |
data_attachments |
list[DataAttachment] |
Adjuntos por bytes en memoria. | ->attachData() |
cleanup_paths |
list[Path] |
Rutas a borrar tras enviar (opt-in). | File::delete() en finally |
Enviar: la facade Mail¶
Síncrono — Mail.send¶
Construye y manda en el acto por SMTP. No usa redis ni worker; bloquea hasta que SMTP responde. Ideal para local sin broker, tests, o cuando necesitas confirmar el envío.
Encolado — Mail.queue¶
Encola el envío en Celery (no bloquea). Parámetros:
queue: cola de Celery (ej."emails");None= cola por defecto.init_kwargs: los argumentos primitivos para reinstanciar el Mailable en el worker. Deben coincidir con el__init__. Desde 0.4.1, si el__init__del Mailable exige argumentos y omitesinit_kwargs,Mail.queuerevienta de inmediato con unValueErroraccionable (en el proceso que encola), en vez de fallar en silencio en el worker al reinstanciar.
mailable = WelcomeMailable(name="Calcifux")
Mail.queue(mailable, to=["calcifux@example.com"], queue="emails",
init_kwargs={"name": "Calcifux"})
Si el broker está caído, Mail.queue lanza QueueUnavailableError (un mensaje claro,
no un 500 técnico). En un endpoint conviene traducirlo a un 503:
from milpa.Core.CeleryApp import QueueUnavailableError
try:
Mail.queue(mailable, to=to, init_kwargs={...})
except QueueUnavailableError as e:
raise HTTPException(status_code=503, detail=str(e))
El contrato del constructor: solo primitivos¶
Al encolar, el Mailable se reinstancia en el worker desde su dotted path +
init_kwargs, y build() corre allí (worker-side). Por eso el constructor solo
debe recibir primitivos serializables (str, int, listas de str, ids). Nada de sesiones
de BD ni clientes HTTP: no se serializan. Si necesitas más datos, pasa un id y recupéralo
en build().
Ventaja: si build() genera bytes (un PDF), esos bytes no viajan por la cola — se
generan en el worker.
Adjuntos¶
Por bytes (recomendado)¶
Sin tocar disco, sin cleanup:
from milpa.Core.Mail.Mailable import DataAttachment
content.data_attachments.append(
DataAttachment("reporte.pdf", pdf_bytes, "application/pdf")
)
Por archivo + cleanup opt-in¶
Si el PDF ya vive en disco como temporal, adjúntalo por ruta y declara su limpieza:
content.attachments.append(temp_path)
content.cleanup_paths.append(temp_path) # el Mailer lo borra tras enviar (finally)
El framework nunca borra un attachments por su cuenta: solo lo que declares en
cleanup_paths. Un asset persistente (un PDF fijo) va en attachments y NO en
cleanup_paths.
Logo inline por CID¶
content.inline_assets["logo"] = Path("app/Resources/Images/Emails/logo.png")
content.context["logo_cid"] = "logo"
En el template: <img src="cid:logo">. (El header SMTP usa <logo>; en el HTML va sin
ángulos.)
Drivers (MAIL_DRIVER)¶
| Driver | Comportamiento |
|---|---|
smtp (default) |
Envío real por SMTP, según MAIL_* y MAIL_ENCRYPTION (""/tls/ssl). |
log |
Loguea el correo completo, no lo envía. Útil en dev sin SMTP. |
null / array |
No-op: lo descarta. |
En local apunta MAIL_HOST/MAIL_PORT a Mailpit (docker compose up -d) y ve los
correos en http://localhost:8025.
Monolingüe vs. i18n¶
| Caso | Patrón |
|---|---|
| i18n | subject con t(), template que extiende los layouts y usa t(). El locale viene del ambiente (Accept-Language). |
| Monolingüe | subject literal, template con texto fijo (sin t(), sin extends). |
Una app es monolingüe salvo que decidas traducir. Ver Localización. El locale se captura al encolar y se restaura en el worker, así el correo sale en el idioma del request que lo disparó.
Ejemplos del módulo Example¶
El módulo Example trae Mailables de referencia y endpoints en /example/mail/*:
| Mailable | Demuestra |
|---|---|
MailableCheck |
Smoke básico, logo por CID, adjuntos por ruta, i18n. |
MailableSignedCheck |
Layout firmado (footer con datos del remitente + aviso de privacidad). |
MailableAttachmentCheck |
Adjunto por bytes vs. por archivo + cleanup. |
PedidoListoMailable |
Monolingüe: subject literal, template sin i18n. |