commit 6da121f22f0c41023ccbe5b2f46565287127c8b6
parent 2959442a903f67337d71af32efa9a22b8fffeadc
Author: Pablo Murad <pablo@pablomurad.com>
Date: Sat, 21 Mar 2026 20:16:36 -0300
fixed usr script
Diffstat:
4 files changed, 126 insertions(+), 9 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -13,4 +13,12 @@ site/public/news/data/news.json
site/public/news/feed.rss
# Wiki: bytecode do gerador local
-site/wiki/__pycache__/
-\ No newline at end of file
+site/wiki/__pycache__/
+
+# Diagnóstico local Mailgun (pode conter chaves / não subir)
+email/testmail.py
+email/testmail*.py
+
+# Cópias locais de segredos de email (nunca versionar)
+runv-email.secrets.json
+**/runv-email.secrets.json
+\ No newline at end of file
diff --git a/email/docs/INTEGRATION.md b/email/docs/INTEGRATION.md
@@ -46,11 +46,11 @@ Com **Mailgun**, `sendmail` é ignorado para o transporte (usa API). Com **legad
| Evento | Template(s) | Onde disparar |
|--------|-------------|----------------|
-| Novo pedido na fila `entre` | `admin_new_request` → admin; opcional `user_request_received` → visitante | Após `save_request_json` em [`terminal/entre_core.py`](../../terminal/entre_core.py) / [`entre_app.py`](../../terminal/entre_app.py). **Hoje** email admin via `sendmail_notify` + `admin_mail.txt` — com Mailgun, `sendmail_notify` tenta **primeiro** a API se o estado global o indicar. |
-| Pedido aprovado (manual) | `user_approved` | Processo admin. |
+| Novo pedido na fila `entre` | Corpo do email: [`terminal/templates/admin_mail.txt`](../../terminal/templates/admin_mail.txt) (não `email/templates/admin_new_request.txt`). Opcional: `user_request_received` existe em `email/templates/` mas **não** está ligado ao `entre`. | Após `save_request_json` em [`terminal/entre_core.py`](../../terminal/entre_core.py) / [`entre_app.py`](../../terminal/entre_app.py). Email admin via `sendmail_notify`; com Mailgun, tenta **primeiro** `lib.mailer.send_mail` se `/etc/runv-email.json` e `email_package_root` / `RUNV_EMAIL_ROOT` forem válidos. |
+| Pedido aprovado (manual) | `user_approved` | Processo admin (manual / futuro). |
| Pedido rejeitado | `user_rejected` (+ `reason`) | Idem. |
-| Conta criada | `admin_user_created`, `user_account_created` | [`scripts/admin/create_runv_user.py`](../../scripts/admin/create_runv_user.py). |
-| Conta removida | `admin_user_deleted`, `user_account_removed` | [`scripts/admin/del-user.py`](../../scripts/admin/del-user.py). |
+| Conta criada | `admin_user_created` → admin; `user_account_created` → utilizador | [`scripts/admin/create_runv_user.py`](../../scripts/admin/create_runv_user.py): `--no-welcome-email` / `--no-admin-create-email` para desactivar cada ramo. |
+| Conta removida | `admin_user_deleted`, `user_account_removed` | Templates em `email/templates/`; [`scripts/admin/del-user.py`](../../scripts/admin/del-user.py) **ainda não** envia estes emails (processo manual ou extensão futura). |
| Erro operacional | `admin_error` | Scripts admin / cron. |
| Quota | `user_quota_warning` | Monitorização / quotas. |
| Teste | `system_test` | `configure_mailgun.py --test` (API) ou legado. |
@@ -71,11 +71,14 @@ Recomenda-se o **mesmo** `admin_email` e remetente coerente com o Mailgun/domín
## `create_runv_user.py` / `del-user.py`
-O **`create_runv_user.py`** envia por omissão um email de **boas-vindas** ao utilizador (`user_account_created`), com instruções para aceder por SSH com a **chave privada** correspondente à chave pública registada. Requer `/etc/runv-email.json` e módulo `email/` acessível; `--no-welcome-email` para desactivar; `--welcome-ssh-host` ou `RUNV_WELCOME_SSH_HOST` para um comando `ssh` explícito.
+O **`create_runv_user.py`** envia por omissão:
-Obtenha `admin_email` / `default_from` de `/etc/runv-email.json` — **não** hardcodar.
+1. **Boas-vindas** ao utilizador (`user_account_created`), com instruções SSH; `--no-welcome-email` desactiva.
+2. **Aviso ao admin** (`admin_user_created` para `admin_email` no JSON); `--no-admin-create-email` desactiva.
+
+Requer `/etc/runv-email.json` (com `default_from`, `admin_email` para o ramo admin), segredos Mailgun se aplicável, e pasta `email/` acessível (`email_package_root` ou `RUNV_EMAIL_ROOT`). Para o texto de boas-vindas, `--welcome-ssh-host` ou `RUNV_WELCOME_SSH_HOST` define o hostname SSH sugerido.
-Ver exemplos na versão anterior deste documento para `send_admin_notice` / `send_user_notice` adicionais.
+Obtenha `admin_email` / `default_from` de `/etc/runv-email.json` — **não** hardcodar.
## Checklist de integração
@@ -83,3 +86,5 @@ Ver exemplos na versão anterior deste documento para `send_admin_notice` / `sen
- [ ] `sudo python3 configure_mailgun.py --test` (ou legado) com sucesso.
- [ ] Templates revistos (português, placeholders).
- [ ] Nenhum segredo em logs ou `print()` (API key só em ficheiro 0600 ou env).
+
+Roteiro passo a passo no servidor: [VERIFICATION_CHECKLIST.md](VERIFICATION_CHECKLIST.md).
diff --git a/email/lib/mailer.py b/email/lib/mailer.py
@@ -181,6 +181,7 @@ def send_admin_notice(
from_addr: str,
sendmail: str | None = None,
html_body: str | None = None,
+ _state: dict | None = None,
**kwargs: object,
) -> None:
"""Renderiza template administrativo e envia para admin_email."""
@@ -192,6 +193,7 @@ def send_admin_notice(
from_addr=from_addr,
sendmail=sendmail,
html=html_body,
+ _state=_state,
)
@@ -203,6 +205,7 @@ def send_user_notice(
from_addr: str,
sendmail: str | None = None,
html_body: str | None = None,
+ _state: dict | None = None,
**kwargs: object,
) -> None:
"""Renderiza template para utilizador e envia para user_email."""
@@ -214,6 +217,7 @@ def send_user_notice(
from_addr=from_addr,
sendmail=sendmail,
html=html_body,
+ _state=_state,
)
diff --git a/scripts/admin/create_runv_user.py b/scripts/admin/create_runv_user.py
@@ -1330,6 +1330,7 @@ def try_send_welcome_email(
user_email,
subject="[runv.club] Bem-vindo(a) — a sua conta foi criada",
from_addr=from_addr,
+ _state=state,
username=username,
email=user_email,
fingerprint=fingerprint,
@@ -1342,6 +1343,91 @@ def try_send_welcome_email(
log.warning("email de boas-vindas falhou (conta já criada): %s", e)
+def try_send_admin_user_created_email(
+ *,
+ username: str,
+ user_email: str,
+ operator_info: str,
+ timestamp: str,
+ no_admin_create_email: bool,
+ dry_run: bool,
+ log: logging.Logger,
+) -> None:
+ """
+ Envia ``admin_user_created`` para ``admin_email`` em ``/etc/runv-email.json``.
+ Falhas só em log — a conta já foi criada.
+ """
+ if no_admin_create_email:
+ log.info("email admin (conta criada): omitido (--no-admin-create-email)")
+ return
+ if dry_run:
+ log.info("email admin (conta criada): omitido (--dry-run)")
+ return
+
+ state_file = Path("/etc/runv-email.json")
+ if not state_file.is_file():
+ log.info(
+ "email admin (conta criada): %s ausente — omitido",
+ state_file,
+ )
+ return
+ try:
+ state = json.loads(state_file.read_text(encoding="utf-8"))
+ except (OSError, json.JSONDecodeError) as e:
+ log.warning("email admin (conta criada): estado inválido (%s): %s", state_file, e)
+ return
+
+ admin = str(state.get("admin_email", "")).strip()
+ if not admin:
+ log.info(
+ "email admin (conta criada): admin_email vazio em %s — omitido",
+ state_file,
+ )
+ return
+
+ email_root = _resolve_email_package_root(state)
+ if email_root is None:
+ log.warning(
+ "email admin (conta criada): pasta email/ não encontrada "
+ "(RUNV_EMAIL_ROOT, email_package_root no JSON ou repositório em %s)",
+ _REPO_ROOT / "email",
+ )
+ return
+
+ root_s = str(email_root.resolve())
+ if root_s not in sys.path:
+ sys.path.insert(0, root_s)
+
+ try:
+ from lib.mailer import send_admin_notice
+ from lib.templates import ADMIN_USER_CREATED
+ except ImportError as e:
+ log.warning("email admin (conta criada): import lib.mailer falhou: %s", e)
+ return
+
+ from_addr = str(state.get("default_from", "")).strip()
+ if not from_addr:
+ log.warning("email admin (conta criada): default_from ausente em %s", state_file)
+ return
+
+ try:
+ send_admin_notice(
+ ADMIN_USER_CREATED,
+ admin,
+ subject=f"[runv.club] Conta criada — {username}",
+ from_addr=from_addr,
+ _state=state,
+ username=username,
+ email=user_email,
+ operator_info=operator_info,
+ timestamp=timestamp,
+ )
+ log.info("email admin (conta criada) enviado para %s", admin)
+ print(f" admin (conta): email enviado para {admin}")
+ except Exception as e:
+ log.warning("email admin (conta criada) falhou (conta já criada): %s", e)
+
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
p = argparse.ArgumentParser(
description=(
@@ -1495,6 +1581,11 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
help="não enviar email de boas-vindas ao utilizador após criar a conta",
)
p.add_argument(
+ "--no-admin-create-email",
+ action="store_true",
+ help="não enviar email ao admin (template admin_user_created) após criar a conta",
+ )
+ p.add_argument(
"--welcome-ssh-host",
default=None,
metavar="HOST",
@@ -1804,6 +1895,15 @@ def main(argv: list[str] | None = None) -> int:
dry_run=bool(args.dry_run),
log=log,
)
+ try_send_admin_user_created_email(
+ username=user,
+ user_email=email,
+ operator_info=record.created_by,
+ timestamp=record.created_at,
+ no_admin_create_email=bool(args.no_admin_create_email),
+ dry_run=bool(args.dry_run),
+ log=log,
+ )
if not args.no_quota and qr.status in ("failed", "not_configured"):
print(