commit 2959442a903f67337d71af32efa9a22b8fffeadc
parent c5d6449cf874081252752c0f1c7f96d397509c1f
Author: Pablo Murad <pablo@pablomurad.com>
Date: Sat, 21 Mar 2026 20:08:58 -0300
fixed email
Diffstat:
3 files changed, 38 insertions(+), 2 deletions(-)
diff --git a/email/configure_mailgun.py b/email/configure_mailgun.py
@@ -80,12 +80,25 @@ def prompt_api_key_twice() -> str:
return key
+def print_mailgun_operator_hints() -> None:
+ print()
+ print("Requisitos Mailgun (evita 401 «Invalid private key» / Forbidden):")
+ print(" • API HTTP: Basic user ``api``, password = **Private API key** ou **domain sending key**")
+ print(" (não use a password SMTP do painel).")
+ print(" • **IP allowlist:** se estiver activa no painel Mailgun (API), inclua o **IP público")
+ print(" deste servidor** (o mesmo que faz os pedidos HTTPS à Mailgun).")
+ print(" • Envio: ``POST /v3/<domínio>/messages`` — o domínio na URL deve coincidir com o")
+ print(" domínio verificado no painel (ex.: runv.club).")
+ print()
+
+
def interactive_config(*, email_package_root: str) -> tuple[dict[str, Any], dict[str, str]]:
print()
print("=== Configurador de email para Mailgun API ===")
print()
print("Aviso: este script foi feito para Mailgun. Não pré-configura nenhuma credencial.")
print()
+ print_mailgun_operator_hints()
print("Tipo de chave Mailgun (recomendado: domain sending key — menor privilégio):")
print(" 1) Domain sending key (recomendado)")
@@ -150,6 +163,17 @@ def write_json_atomic(path: Path, data: dict[str, Any], *, mode: int, dry_run: b
log().info("Escrito %s (%o)", path, mode)
+def _print_test_failure_hint(exc: BaseException) -> None:
+ msg = str(exc).lower()
+ if "401" not in msg and "403" not in msg and "forbidden" not in msg:
+ return
+ print(
+ "\nDica: com chave e domínio correctos, 401/403 na API Mailgun costuma ser **IP allowlist** "
+ "no painel — adicione o IP público de **esta máquina** (curl ifconfig.me no servidor).",
+ file=sys.stderr,
+ )
+
+
def run_test_send(*, dry_run: bool) -> None:
pub = json.loads(STATE_PATH.read_text(encoding="utf-8"))
admin = str(pub.get("admin_email", "")).strip()
@@ -239,6 +263,7 @@ def main() -> int:
run_test_send(dry_run=args.dry_run)
except Exception as e:
log().error("%s", e)
+ _print_test_failure_hint(e)
return 1
print("Teste concluído.")
return 0
@@ -271,6 +296,7 @@ def main() -> int:
log().info("Teste enviado.")
except Exception as e:
log().warning("Teste falhou: %s", e)
+ _print_test_failure_hint(e)
print_summary(public, dry_run=args.dry_run)
print("Teste posterior: sudo python3 email/configure_mailgun.py --test")
diff --git a/email/docs/INSTALL.md b/email/docs/INSTALL.md
@@ -20,11 +20,17 @@ Em tempo de execução, **`RUNV_MAILGUN_API_KEY`** (se definida) **tem prioridad
- **US:** `https://api.mailgun.net/v3/<domínio>/messages` (o configurador usa sempre este endpoint; é o mesmo eixo que o SMTP **`smtp.mailgun.org`** nas credenciais SMTP do painel.)
- **EU:** `https://api.eu.mailgun.net/v3/<domínio>/messages` — só para contas/domínios alojados na região UE; nesse caso **edite** `mailgun_region` (`eu`) e `api_base_url` em `/etc/runv-email.json` após correr o script, ou a API devolverá erros de autenticação/domínio.
+### IP allowlist (API)
+
+Se no painel Mailgun estiver activa a **restrição por IP** para a API, qualquer servidor que chame `api.mailgun.net` tem de ter o **seu IP público** na lista. Sem isso, a API pode responder **401** / «Invalid private key» / **Forbidden** mesmo com chave e domínio correctos. Inclua o IP da VPS (ou desactive a allowlist para testes).
+
### Obter uma API key
1. Painel Mailgun → domínio → **Domain settings** / **Sending API keys**.
2. Preferir **domain sending key** (menor privilégio) se só precisar de enviar desse domínio; **primary API key** também funciona se tiver permissão de envio.
+Para validar a **primary** no painel ou com `curl`, a listagem de domínios usa **`GET /v4/domains`** (US ou EU). A **domain sending key** não serve para esse endpoint; o envio do runv usa **`POST /v3/<domínio>/messages`** (já implementado em `lib/mailgun_client.py`).
+
## Executar o configurador (predefinido)
```bash
@@ -70,7 +76,7 @@ sudo python3 configure_mailgun.py --test
Em caso de falha, mensagens típicas:
-- **401 / 403** — API key errada, **domain sending key** de outro domínio, ou **conta/região UE** a usar o endpoint US (`api.mailgun.net`); confira no painel Mailgun se o domínio é US ou EU e se a chave corresponde a esse domínio.
+- **401 / 403** — Chave incorrecta (não é API HTTP / não é do domínio), região errada (US vs EU), ou **IP allowlist** no painel a bloquear o servidor; confira também se o domínio na URL coincide com o domínio verificado.
- **400** — payload inválido; From não autorizado no domínio; campos em falta.
- **404** — domínio errado ou URL/região incorreta (US vs EU).
- **Timeout / erro de rede** — DNS, firewall ou TLS.
diff --git a/email/lib/mailgun_client.py b/email/lib/mailgun_client.py
@@ -277,7 +277,11 @@ def format_mailgun_failure(status: int, body_snippet: str) -> str:
"""Mensagem legível para operadores (sem expor segredos)."""
base = f"HTTP {status}"
if status in (401, 403):
- return f"{base}: API key inválida ou sem permissão para este domínio/região."
+ return (
+ f"{base}: API key inválida, domínio/região incorrectos, ou **IP allowlist** no "
+ f"painel Mailgun a bloquear este servidor. Confirme chave HTTP (não password SMTP), "
+ f"domínio na URL, e em Security/API a lista de IPs permitidos."
+ )
if status == 400:
return f"{base}: pedido inválido — verifique domínio, From autorizado e campos obrigatórios. Resposta: {body_snippet[:200]}"
if status == 404: