Files
colio/mail/views.py
2026-01-13 09:38:11 +09:00

184 lines
6.0 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
import os
import re
import time
from django.conf import settings
from django.shortcuts import render
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny, IsAdminUser
from rest_framework.response import Response
from rest_framework import status
from rest_framework.exceptions import NotFound, APIException
try:
import docker
except ImportError:
docker = None
LOCAL_RE = re.compile(r"^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$")
ANSI_RE = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]")
EMAIL_RE = re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}")
# ----------------------------
# Feature toggle
# ----------------------------
def _require_enabled():
if not getattr(settings, "MAIL_SIGNUP_ENABLED", False):
raise NotFound("Not Found") # 숨김(404)
# ----------------------------
# Docker-mailserver exec helpers
# ----------------------------
def _docker_client():
"""
Uses docker socket mounted into this container:
/var/run/docker.sock
"""
if docker is None:
raise APIException("Python package 'docker' is not installed in this server image.")
sock = getattr(settings, "MAIL_DOCKER_SOCK", None) or os.getenv("MAIL_DOCKER_SOCK", "unix://var/run/docker.sock")
try:
client = docker.DockerClient(base_url=sock)
# ping to ensure socket is usable
client.ping()
return client
except Exception as e:
raise APIException(f"Docker socket is not available. Check /var/run/docker.sock mount. ({e})")
def _dms_container_name() -> str:
return getattr(settings, "MAIL_DMS_CONTAINER", None) or os.getenv("MAIL_DMS_CONTAINER", "dms-mailserver")
def _dms_exec(setup_args):
"""
Runs docker-mailserver's internal `setup` command:
docker exec dms-mailserver setup <args...>
Example:
_dms_exec(["email", "list"])
_dms_exec(["email", "add", "user@domain", "password"])
"""
client = _docker_client()
name = _dms_container_name()
try:
c = client.containers.get(name)
except Exception as e:
raise APIException(f"Cannot find mailserver container '{name}'. ({e})")
# Try to execute: setup <args...>
try:
res = c.exec_run(["setup", *setup_args], demux=True)
out, err = res.output
stdout = (out or b"").decode("utf-8", errors="ignore").strip()
stderr = (err or b"").decode("utf-8", errors="ignore").strip()
if res.exit_code != 0:
# show meaningful error to API consumer
raise ValueError(stderr or stdout or "docker-mailserver setup failed")
return stdout
except ValueError:
raise
except Exception as e:
raise APIException(f"Failed to exec into '{name}'. ({e})")
def _wait_until_exists(address: str, tries: int = 3, delay: float = 0.3) -> bool:
addr = address.strip().lower()
for _ in range(tries):
if addr in _email_list():
return True
time.sleep(delay)
return False
def _email_list():
raw = _dms_exec(["email", "list"])
raw = ANSI_RE.sub("", raw) # 색상코드 제거
# 어떤 형식이 와도 이메일만 뽑아냄
emails = set(m.group(0).strip().lower() for m in EMAIL_RE.finditer(raw))
return sorted(emails)
# ----------------------------
# Views
# ----------------------------
def signup_page(request):
_require_enabled()
return render(request, "mail/signup.html")
@api_view(["POST"])
@permission_classes([AllowAny])
def request_account(request):
_require_enabled()
invite_expected = getattr(settings, "MAIL_INVITE_CODE", None) or os.getenv("MAIL_INVITE_CODE", "")
dms_domain = getattr(settings, "MAIL_DMS_DOMAIN", None) or os.getenv("MAIL_DMS_DOMAIN", "")
local = (request.data.get("local") or "").strip().lower()
password = request.data.get("password") or ""
invite = (request.data.get("invite") or "").strip()
if not invite_expected or invite != invite_expected:
return Response({"detail": "초대코드가 올바르지 않습니다."}, status=status.HTTP_403_FORBIDDEN)
if not dms_domain:
return Response(
{"detail": "서버 설정(MAIL_DMS_DOMAIN)이 비어있습니다."},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
if not LOCAL_RE.match(local):
return Response(
{"detail": "아이디 형식이 올바르지 않습니다. (영문소문자/숫자/._- 허용)"},
status=status.HTTP_400_BAD_REQUEST,
)
if len(password) < 5:
return Response(
{"detail": "비밀번호는 5자 이상으로 설정하세요."},
status=status.HTTP_400_BAD_REQUEST,
)
address = f"{local}@{dms_domain}"
try:
emails = _email_list()
if address in emails:
return Response({"message": f" {address} 는 이미 존재합니다. (email list에서 확인됨)"})
# Create account inside dms-mailserver
_dms_exec(["email", "add", address, password])
if _wait_until_exists(address):
return Response({"message": f"{address} 계정이 잘 만들어졌습니다! (email list에서 확인됨)"})
return Response({"detail": "계정 생성 후 list에서 확인이 안 됩니다. 다시 제출 버튼을 눌러서 확인해보시거나 관리자에게 문의해주세요."}, status=500)
except ValueError as e:
msg = str(e)
if "already exists" in msg.lower():
return Response({"message": f" {address} 는 이미 존재합니다."})
return Response({"detail": msg}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
return Response({"detail": f"서버 오류: {e}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(["GET"])
@permission_classes([IsAdminUser])
def admin_list(request):
_require_enabled()
try:
emails = _email_list()
return Response({"count": len(emails), "emails": emails})
except Exception as e:
return Response({"detail": f"서버 오류: {e}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)