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())