# Enviar e-mail no Django

> Enviar e-mail pelo Couryo no Django com send_mail e EmailMultiAlternatives, pelo backend do SDK Python ou por SMTP, e receber webhooks assinados.

Fonte: https://couryo.com/docs/django

O `send_mail`, o `EmailMessage` e os e-mails do próprio Django (recuperação de senha do `django.contrib.auth`, avisos de erro) passam a sair pelo Couryo trocando só o backend.

## Opção 1: backend do SDK

```bash title="Terminal"
pip install "couryo[django]"
```

No Django 6.1 ou mais novo, o backend vai em `MAILERS`:

```python title="settings.py"
import os

MAILERS = {
    "default": {
        "BACKEND": "couryo.django.EmailBackend",
        "OPTIONS": {"api_key": os.environ["COURYO_API_KEY"]},  # ck_test_... enquanto testa
    },
}
DEFAULT_FROM_EMAIL = "Exemplo <oi@exemplo.com.br>"
SERVER_EMAIL = "alertas@exemplo.com.br"
```

No Django 6.0 ou mais antigo, use `EMAIL_BACKEND` (o Django 6.1 ainda aceita, com aviso de descontinuação, mas não junto com `MAILERS`):

```python title="settings.py"
import os

EMAIL_BACKEND = "couryo.django.EmailBackend"
COURYO_API_KEY = os.environ["COURYO_API_KEY"]  # ck_test_... enquanto testa
DEFAULT_FROM_EMAIL = "Exemplo <oi@exemplo.com.br>"
SERVER_EMAIL = "alertas@exemplo.com.br"
```

O código que já existe não muda:

```python title="pedidos/emails.py"
from django.core.mail import EmailMultiAlternatives, send_mail
from django.template.loader import render_to_string

send_mail("Pedido recebido", "Obrigado pela compra!", None, ["ana@exemplo.com.br"])

contexto = {"pedido": pedido}
msg = EmailMultiAlternatives(
    subject=f"Pedido {pedido.numero} enviado",
    body=render_to_string("emails/pedido_enviado.txt", contexto),
    to=[pedido.cliente.email],
    headers={"X-Couryo-Tags": "tipo=pedido"},
)
msg.attach_alternative(render_to_string("emails/pedido_enviado.html", contexto), "text/html")
msg.send()
```

Anexos (`msg.attach(...)`) e `reply_to`, `cc` e `bcc` seguem para a API. O cabeçalho `X-Couryo-Stream: marketing` manda no fluxo de marketing; sem ele, o e-mail é transacional.

### Chamar a API direto

```python title="contas/views.py"
from couryo import Couryo

couryo = Couryo()  # lê COURYO_API_KEY

couryo.events.track(
    {"contact": {"email": usuario.email, "attributes": {"nome": usuario.first_name}}, "name": "user.signed_up"},
    idempotency_key=f"signup-{usuario.pk}",
)
```

Em views assíncronas, use `AsyncCouryo` com `await`.

## Opção 2: SMTP

No Django 6.1 ou mais novo:

```python title="settings.py"
MAILERS = {
    "default": {
        "BACKEND": "django.core.mail.backends.smtp.EmailBackend",
        "OPTIONS": {
            "host": "smtp.couryo.com",
            "port": 587,
            "use_tls": True,  # STARTTLS; para a porta 465, use "use_ssl": True no lugar
            "username": "couryo",
            "password": os.environ["COURYO_API_KEY"],
        },
    },
}
DEFAULT_FROM_EMAIL = "Exemplo <oi@exemplo.com.br>"
```

No Django 6.0 ou mais antigo:

```python title="settings.py"
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.couryo.com"
EMAIL_PORT = 587
EMAIL_USE_TLS = True  # STARTTLS; para a porta 465, use EMAIL_USE_SSL = True
EMAIL_HOST_USER = "couryo"
EMAIL_HOST_PASSWORD = os.environ["COURYO_API_KEY"]
DEFAULT_FROM_EMAIL = "Exemplo <oi@exemplo.com.br>"
```

`use_tls` e `use_ssl` (`EMAIL_USE_TLS` e `EMAIL_USE_SSL`) não podem ser `True` ao mesmo tempo. Detalhes e códigos de resposta em [SMTP](https://couryo.com/docs/smtp.md).

## Receber webhooks

```python title="webhooks/views.py"
import os

from couryo import WebhookVerificationError, verify_webhook
from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST


@csrf_exempt
@require_POST
def couryo_webhook(request):
    try:
        evento = verify_webhook(os.environ["COURYO_WEBHOOK_SECRET"], request.headers, request.body)
    except WebhookVerificationError:
        return HttpResponseBadRequest("assinatura inválida")

    if evento["type"] == "bounced":
        ...  # evento["data"]["recipient"], evento["data"]["explanation"]
    return HttpResponse(status=204)
```

`request.body` é o corpo cru, exatamente como a assinatura espera. A view fica fora do CSRF porque a assinatura já protege.

## Recuperação de senha do Django

A `PasswordResetView` usa o mailer padrão (`MAILERS["default"]`, ou o `EMAIL_BACKEND` no Django 6.0 e anteriores), então já sai pelo Couryo. Confira só o `DEFAULT_FROM_EMAIL` (num [domínio verificado](https://couryo.com/docs/domains.md)) e o domínio do link: em produção, o Django monta o link com o host da requisição, então mantenha o `ALLOWED_HOSTS` certo. Se o e-mail de senha não chega, o guia [Recuperação de senha não chega](https://couryo.com/guias/recuperacao-de-senha-nao-chega.md) passa pelas causas uma a uma.

## Problemas comuns

| Sintoma | O que fazer |
|---|---|
| `InvalidRequestError` com `code` `domain_not_verified` | o `DEFAULT_FROM_EMAIL` precisa estar num domínio verificado no projeto |
| Nada acontece e nenhum erro | `fail_silently=True` esconde erros; tire para ver a mensagem do Couryo |
| `SMTPAuthenticationError (535)` | usuário diferente de `couryo` ou chave inválida, revogada ou sem escopo `send` |
