Major Features: - ✅ Multi-tenant architecture (tenant isolation) - ✅ Employee CRUD with lifecycle management (onboarding/offboarding) - ✅ Department tree structure with email domain management - ✅ Company info management (single-record editing) - ✅ System functions CRUD (permission management) - ✅ Email account management (multi-account per employee) - ✅ Keycloak SSO integration (auth.lab.taipei) - ✅ Redis session storage (10.1.0.254:6379) - Solves Cookie 4KB limitation - Cross-system session sharing - Sliding expiration (8 hours) - Automatic token refresh Technical Stack: Backend: - FastAPI + SQLAlchemy - PostgreSQL 16 (10.1.0.20:5433) - Keycloak Admin API integration - Docker Mailserver integration (SSH) - Alembic migrations Frontend: - Next.js 14 (App Router) - NextAuth 4 with Keycloak Provider - Redis session storage (ioredis) - Tailwind CSS Infrastructure: - Redis 7 (10.1.0.254:6379) - Session + Cache - Keycloak 26.1.0 (auth.lab.taipei) - Docker Mailserver (10.1.0.254) Architecture Highlights: - Session管理由 Keycloak + Redis 統一控制 - 支援多系統 (HR/WebMail/Calendar/Drive/Office) 共享 session - Token 自動刷新,異質服務整合 - 未來可無縫遷移到雲端 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
246 lines
7.8 KiB
Python
246 lines
7.8 KiB
Python
"""
|
||
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 <email> <password>
|
||
docker exec mailserver setup quota set <email> <quota>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 <email> <quota>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 <email>
|
||
|
||
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
|