""" Docker Mailserver Service 透過 SSH + docker exec 管理 Docker Mailserver 郵件帳號 部署架構: HR Portal (10.1.0.245 或正式環境) --SSH--> Ubuntu Server (10.1.0.254) Ubuntu Server --> docker exec mailserver setup ... 整合方式: paramiko SSH → docker exec mailserver setup email add/del/quota set 失敗處理原則: - SSH 連線失敗以 warning 記錄,回傳包含 error 的結果字典 - 不拋出例外,不影響 Keycloak 等其他 onboarding 流程 """ import logging from typing import Dict, Any, Optional logger = logging.getLogger(__name__) # 郵件配額設定 (MB),依職級對應 MAIL_QUOTA_BY_JOB_LEVEL = { "Junior": 2048, # 2 GB "Mid": 3072, # 3 GB "Senior": 5120, # 5 GB "Manager": 10240, # 10 GB } def get_mail_quota_by_job_level(job_level: str) -> int: """根據職級取得郵件配額 (MB)""" return MAIL_QUOTA_BY_JOB_LEVEL.get(job_level, MAIL_QUOTA_BY_JOB_LEVEL["Junior"]) class MailserverService: """ Docker Mailserver 管理 Service 透過 SSH 連線到 Ubuntu Server (10.1.0.254), 再執行 docker exec mailserver setup 指令管理郵件帳號。 支援操作: - 建立郵件帳號 - 設定配額 - 停用帳號 (停止收信,保留資料) - 設定轉寄 - 查詢帳號狀態 """ def __init__(self, ssh_host: str, ssh_port: int, ssh_user: str, ssh_password: str, container_name: str = "mailserver", timeout: int = 30): self.ssh_host = ssh_host self.ssh_port = ssh_port self.ssh_user = ssh_user self.ssh_password = ssh_password self.container_name = container_name self.timeout = timeout def _exec_docker_command(self, *setup_args) -> tuple[bool, str, str]: """ 透過 SSH 執行 docker exec mailserver setup 指令 Args: *setup_args: setup 子指令參數 例如: "email", "add", "user@domain.com", "password" Returns: (success: bool, stdout: str, stderr: str) """ try: import paramiko except ImportError: logger.error("缺少 paramiko 套件,請執行: pip install paramiko") return False, "", "缺少 paramiko 套件" cmd = f"docker exec {self.container_name} setup " + " ".join( f'"{arg}"' if " " in str(arg) else str(arg) for arg in setup_args ) logger.debug(f"執行 Mailserver 指令: docker exec {self.container_name} setup {' '.join(str(a) for a in setup_args[:2])} ...") ssh = None try: ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.connect( hostname=self.ssh_host, port=self.ssh_port, username=self.ssh_user, password=self.ssh_password, timeout=self.timeout, ) stdin, stdout, stderr = ssh.exec_command(cmd, timeout=self.timeout) out = stdout.read().decode("utf-8", errors="replace").strip() err = stderr.read().decode("utf-8", errors="replace").strip() rc = stdout.channel.recv_exit_status() success = (rc == 0) if not success: logger.warning(f"Mailserver 指令失敗 (rc={rc}): {err or out}") return success, out, err except Exception as e: logger.warning(f"SSH 連線到 {self.ssh_host} 失敗: {e}") return False, "", str(e) finally: if ssh: try: ssh.close() except Exception: pass def create_email_account( self, email: str, password: str, quota_mb: int, ) -> Dict[str, Any]: """ 建立郵件帳號 執行: docker exec mailserver setup email add docker exec mailserver setup quota set M Args: email: 郵件地址 (例: user@porscheworld.tw) password: 初始密碼 (建議後續透過 Keycloak SSO 管理) quota_mb: 配額 (MB) Returns: {"created": bool, "email": str, "quota_mb": int, "message": str, "error": str|None} """ # 1. 建立帳號 success, out, err = self._exec_docker_command( "email", "add", email, password ) if not success: return { "created": False, "email": email, "quota_mb": quota_mb, "message": "建立郵件帳號失敗", "error": err or out, } logger.info(f"Mailserver: 郵件帳號建立成功 {email}") # 2. 設定配額 self.set_quota(email, quota_mb) return { "created": True, "email": email, "quota_mb": quota_mb, "message": f"郵件帳號建立成功 ({quota_mb}MB)", "error": None, } def set_quota(self, email: str, quota_mb: int) -> Dict[str, Any]: """ 設定郵件配額 執行: docker exec mailserver setup quota set M Returns: {"updated": bool, "email": str, "quota_mb": int, "error": str|None} """ success, out, err = self._exec_docker_command( "quota", "set", email, f"{quota_mb}M" ) if success: logger.info(f"Mailserver: 配額設定成功 {email} → {quota_mb}MB") else: logger.warning(f"Mailserver: 配額設定失敗 {email}: {err}") return { "updated": success, "email": email, "quota_mb": quota_mb, "error": None if success else (err or out), } def delete_email_account(self, email: str) -> Dict[str, Any]: """ 刪除郵件帳號 執行: docker exec mailserver setup email del Returns: {"deleted": bool, "email": str, "error": str|None} """ success, out, err = self._exec_docker_command( "email", "del", email ) if success: logger.info(f"Mailserver: 郵件帳號刪除成功 {email}") else: logger.warning(f"Mailserver: 郵件帳號刪除失敗 {email}: {err}") return { "deleted": success, "email": email, "error": None if success else (err or out), } def list_accounts(self) -> Dict[str, Any]: """ 列出所有郵件帳號 執行: docker exec mailserver setup email list Returns: {"accounts": list[str], "error": str|None} """ success, out, err = self._exec_docker_command("email", "list") if success: accounts = [line.strip() for line in out.splitlines() if line.strip()] return {"accounts": accounts, "error": None} return {"accounts": [], "error": err or out} # ============================================================ # 延遲初始化單例 # ============================================================ _mailserver_service: Optional[MailserverService] = None def get_mailserver_service() -> MailserverService: """取得 MailserverService 單例 (延遲初始化)""" global _mailserver_service if _mailserver_service is None: from app.core.config import settings _mailserver_service = MailserverService( ssh_host=settings.MAILSERVER_SSH_HOST, ssh_port=settings.MAILSERVER_SSH_PORT, ssh_user=settings.MAILSERVER_SSH_USER, ssh_password=settings.MAILSERVER_SSH_PASSWORD, container_name=settings.MAILSERVER_CONTAINER_NAME, timeout=settings.MAILSERVER_SSH_TIMEOUT, ) return _mailserver_service