feat(gerp/mail): 支持用户注册多个企业邮箱,上限 5 个

- MailApply 上限由 1 改 5,超出提示"邮箱数量已达上限"

- MyMailbox 返回 MailboxList 数组(按创建时间倒序),替代单 Mailbox

- ChangePassword/Login 支持可选 MailboxName 指定目标邮箱,缺省取第一个
This commit is contained in:
TermiNexus committed 2026-08-13 01:00:37 +08:00
1 parent 0302e3a2b2
commit 838b0a18e7
1 file changed
+26 -24
+26 -24
View File
@@ -41,6 +41,8 @@ _MAIL_TOKEN_TTL = 24 * 3600 # token 有效期 24 小时
_MAIL_TOKEN_STORE = {} # token -> {"email": str, "password": str, "exp": float}
_MAIL_TOKEN_LOCK = threading.Lock()
_MAILBOX_LIMIT = 5 # 每个用户最多申请的企业邮箱数
def _PurgeMailTokens():
"""清理已过期 token。"""
@@ -218,9 +220,9 @@ class MailApplyView(ELTAuthenticatedAPIView):
if not user_bytes:
return Response({'code': 1, 'msg': '无法识别当前用户'}, status=401)
# 每个用户最多一个邮箱
if UserMailbox.objects.filter(UserUUID=request.user).exists():
return Response({'code': 1, 'msg': '您已拥有邮箱,一个用户只能申请一个'}, status=400)
# 每个用户最多申请 _MAILBOX_LIMIT 个邮箱
if UserMailbox.objects.filter(UserUUID=request.user).count() >= _MAILBOX_LIMIT:
return Response({'code': 1, 'msg': f'邮箱数量已达上限(最多 {_MAILBOX_LIMIT} 个)'}, status=400)
mailbox_name = (request.data.get('MailboxName') or '').strip()
password = request.data.get('Password') or ''
@@ -291,9 +293,11 @@ class MailChangePasswordView(ELTAuthenticatedAPIView):
permission_classes = [IsAuthenticated]
def post(self, request):
mailbox = UserMailbox.objects.filter(UserUUID=request.user, Status=1).first()
mailbox_name = (request.data.get('MailboxName') or '').strip()
queryset = UserMailbox.objects.filter(UserUUID=request.user, Status=1)
mailbox = queryset.filter(MailboxName=mailbox_name).first() if mailbox_name else queryset.first()
if mailbox is None:
return Response({'code': 1, 'msg': '您尚未申请邮箱'}, status=404)
return Response({'code': 1, 'msg': '未找到该邮箱' if mailbox_name else '您尚未申请邮箱'}, status=404)
# 安全:改密必须先验证当前邮箱密码,防止已登录会话被劫持后直接改密
old_password = request.data.get('OldPassword') or ''
if not old_password:
@@ -315,27 +319,23 @@ class MailChangePasswordView(ELTAuthenticatedAPIView):
class MailMyMailboxView(ELTAuthenticatedAPIView):
"""我的邮箱信息 + JMAP 连接配置(供浏览器直连)。"""
"""我的邮箱列表 + JMAP 连接配置(供浏览器直连)。"""
permission_classes = [IsAuthenticated]
def get(self, request):
mailbox = UserMailbox.objects.filter(UserUUID=request.user).first()
session = StalwartMailManager.SessionConfig()
if mailbox is None:
return Response({'code': 0, 'data': {'Mailbox': None, 'Session': session}})
return Response({
'code': 0,
'data': {
'Mailbox': {
'MailboxName': mailbox.MailboxName,
'QuotaMB': mailbox.QuotaMB,
'Status': mailbox.Status,
'CreateTime': mailbox.CreateTime.isoformat() if mailbox.CreateTime else None,
},
'Session': session,
},
})
rows = list(UserMailbox.objects.filter(UserUUID=request.user).order_by('-CreateTime'))
mailbox_list = [
{
'MailboxName': m.MailboxName,
'QuotaMB': m.QuotaMB,
'Status': m.Status,
'CreateTime': m.CreateTime.isoformat() if m.CreateTime else None,
}
for m in rows
]
return Response({'code': 0, 'data': {'MailboxList': mailbox_list, 'Session': session}})
class MailAdminListView(ELTAuthenticatedAPIView):
@@ -403,16 +403,18 @@ class MailLoginView(ELTAuthenticatedAPIView):
密码仅在本次请求明文传输一次;后端校验通过后把凭据存入进程内存,
返回 token,后续 JMAP 调用只携带 token(见 MailJmapView)。
请求体:Password
请求体:Password, MailboxName(可选,多邮箱时指定登录哪个;缺省取第一个)
响应:{code: 0, data: {Token, MailboxName}}
"""
permission_classes = [IsAuthenticated]
def post(self, request):
mailbox = UserMailbox.objects.filter(UserUUID=request.user, Status=1).first()
mailbox_name = (request.data.get('MailboxName') or '').strip()
queryset = UserMailbox.objects.filter(UserUUID=request.user, Status=1)
mailbox = queryset.filter(MailboxName=mailbox_name).first() if mailbox_name else queryset.first()
if mailbox is None:
return Response({'code': 1, 'msg': '您尚未申请邮箱'}, status=404)
return Response({'code': 1, 'msg': '未找到该邮箱' if mailbox_name else '您尚未申请邮箱'}, status=404)
password = request.data.get('Password') or ''
if not password:
return Response({'code': 1, 'msg': '请输入邮箱密码'}, status=400)