runv-server

server tooling for runv.club
Log | Files | Refs | README

runv-admin-email-alias (6066B)


      1 #!/usr/bin/env python3
      2 """Administração de pedidos e aliases de email runv.club."""
      3 
      4 from __future__ import annotations
      5 
      6 import argparse
      7 import sys
      8 from pathlib import Path
      9 
     10 sys.tracebacklimit = 0
     11 
     12 
     13 def _bootstrap() -> None:
     14     installed = Path("/usr/local/share/runv/lib")
     15     candidates = [installed]
     16     script = Path(__file__).resolve()
     17     if script.parent.name == "bin":
     18         candidates.insert(0, script.parent.parent / "lib")
     19     for c in candidates:
     20         if (c / "runv_email_aliases.py").is_file() and str(c) not in sys.path:
     21             sys.path.insert(0, str(c))
     22             return
     23 
     24 
     25 _bootstrap()
     26 import runv_email_aliases as ea  # noqa: E402
     27 
     28 try:
     29     import runv_mail_sync as mail_sync  # noqa: E402
     30 except ImportError:
     31     mail_sync = None  # type: ignore[assignment]
     32 
     33 
     34 def cmd_pending() -> int:
     35     pending = ea.iter_pending_requests()
     36     if not pending:
     37         print("Nenhum pedido pendente.")
     38         return 0
     39     print("Pedidos pendentes de alias\n")
     40     for req in pending:
     41         user = req.get("username", "?")
     42         print(user)
     43         print(f"  alias: {req.get('alias', '?')}")
     44         print(f"  destino: {req.get('destination', '?')}")
     45         print(f"  criado: {req.get('created_at', '?')}")
     46         print()
     47     return 0
     48 
     49 
     50 def cmd_list() -> int:
     51     rows = ea.list_active_aliases()
     52     if not rows:
     53         print("Nenhum alias ativo.")
     54         return 0
     55     print("Aliases ativos\n")
     56     for _user, alias, dest in rows:
     57         print(f"{alias} -> {dest}")
     58     return 0
     59 
     60 
     61 def cmd_approve(username: str, *, sync_mail: bool) -> int:
     62     user = ea.validate_alias_username(username)
     63     operator = ea.admin_operator()
     64     entry = ea.approve_pending(user, operator)
     65     print("Alias aprovado localmente.\n")
     66     print("Alias:")
     67     print(f"  {entry.get('alias')}\n")
     68     print("Destino:")
     69     print(f"  {entry.get('destination')}\n")
     70     if sync_mail and mail_sync is not None:
     71         try:
     72             mail_sync.sync_mail()
     73             print("Encaminhamento aplicado no Postfix (sync OK).\n")
     74         except SystemExit:
     75             print(
     76                 "Aviso: sync Postfix falhou; registo JSON está activo.\n"
     77                 "  Corrija /etc/runv-member-mail.json ou execute:\n"
     78                 "    sudo runv-admin-email-alias sync\n",
     79                 file=sys.stderr,
     80             )
     81     elif mail_sync is not None and mail_sync.is_sync_enabled():
     82         print(
     83             "Próximo passo MTA:\n"
     84             "  sudo runv-admin-email-alias sync\n"
     85             "  (ou active auto_sync_on_approve em /etc/runv-member-mail.json)\n"
     86         )
     87     else:
     88         print("Próximo passo manual (MTA):")
     89         print(
     90             "  configurar encaminhamento no Postfix/Dovecot ou\n"
     91             "  copiar email/config/runv-member-mail.example.json para\n"
     92             "  /etc/runv-member-mail.json e executar: runv-admin-email-alias sync\n"
     93         )
     94         print(
     95             f"  destino: {entry.get('alias')} -> {entry.get('destination')}\n"
     96         )
     97     return 0
     98 
     99 
    100 def cmd_sync() -> int:
    101     if mail_sync is None:
    102         ea.rc.friendly_exit("módulo runv_mail_sync não encontrado.")
    103     mail_sync.sync_mail()
    104     print("Sync Postfix concluído.")
    105     return 0
    106 
    107 
    108 def cmd_reject(username: str, reason: str) -> int:
    109     if not reason.strip():
    110         ea.rc.friendly_exit("motivo da rejeição obrigatório (--reason).")
    111     user = ea.validate_alias_username(username)
    112     operator = ea.admin_operator()
    113     ea.reject_pending(user, operator, reason.strip())
    114     print("Pedido rejeitado.\n")
    115     print("Usuário:")
    116     print(f"  {user}\n")
    117     print("Motivo:")
    118     print(f"  {reason}")
    119     return 0
    120 
    121 
    122 def build_parser() -> argparse.ArgumentParser:
    123     p = argparse.ArgumentParser(
    124         prog="runv-admin-email-alias",
    125         description="Aprovar ou rejeitar pedidos de alias de email (requer root).",
    126     )
    127     sub = p.add_subparsers(dest="command", required=True)
    128     sub.add_parser("pending", help="listar pedidos pendentes")
    129     sub.add_parser("list", help="listar aliases activos")
    130     appr = sub.add_parser("approve", help="aprovar pedido pendente mais recente")
    131     appr.add_argument("user", metavar="USER", help="nome de utilizador Unix")
    132     appr.add_argument(
    133         "--sync-mail",
    134         action="store_true",
    135         help="aplicar imediatamente no Postfix (requer /etc/runv-member-mail.json enabled)",
    136     )
    137     appr.add_argument(
    138         "--no-sync-mail",
    139         action="store_true",
    140         help="não tentar sync Postfix mesmo com auto_sync_on_approve",
    141     )
    142     sub.add_parser(
    143         "sync",
    144         help="sincronizar aliases activos com Postfix (hash runv-member-aliases)",
    145     )
    146     rej = sub.add_parser("reject", help="rejeitar pedido pendente")
    147     rej.add_argument("user", metavar="USER", help="nome de utilizador Unix")
    148     rej.add_argument(
    149         "--reason",
    150         required=True,
    151         help='motivo da rejeição (obrigatório)',
    152     )
    153     return p
    154 
    155 
    156 def main(argv: list[str] | None = None) -> int:
    157     try:
    158         args = build_parser().parse_args(argv)
    159         ea.require_root()
    160         if args.command == "pending":
    161             return cmd_pending()
    162         if args.command == "list":
    163             return cmd_list()
    164         if args.command == "approve":
    165             do_sync = bool(args.sync_mail)
    166             if (
    167                 not args.no_sync_mail
    168                 and not do_sync
    169                 and mail_sync is not None
    170                 and mail_sync.is_sync_enabled()
    171             ):
    172                 cfg = mail_sync.load_config()
    173                 do_sync = bool(cfg.get("auto_sync_on_approve"))
    174             return cmd_approve(args.user, sync_mail=do_sync)
    175         if args.command == "sync":
    176             return cmd_sync()
    177         if args.command == "reject":
    178             return cmd_reject(args.user, args.reason)
    179         return 1
    180     except KeyboardInterrupt:
    181         print("\nInterrompido.", file=sys.stderr)
    182         return 130
    183     except SystemExit:
    184         raise
    185     except Exception as e:
    186         ea.rc.friendly_exit(f"erro: {e}")
    187 
    188 
    189 if __name__ == "__main__":
    190     raise SystemExit(main())