Files
hr-portal/backend/app/services/mailserver_service.py
Porsche Chen 360533393f feat: HR Portal - Complete Multi-Tenant System with Redis Session Storage
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>
2026-02-23 20:12:43 +08:00

246 lines
7.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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