feat(mail): 增加签名管理与客户端配置

- 新增邮件签名的数据表、接口和前端管理页,支持全局/邮箱默认签名的创建、编辑、删除与查询。
- 写信时自动带入当前邮箱的默认签名,并优化手动输入内容时的覆盖行为。
- 补充第三方邮件客户端配置页,展示 IMAP/POP3/SMTP 连接信息及公共主机名。
- 扩展部署配置,开放 POP3S/SMTPS 端口并启用 Dovecot POP3 服务。
This commit is contained in:
LanQin
2026-06-16 23:15:35 +08:00
parent 9a0e039767
commit 045132146b
18 changed files with 628 additions and 22 deletions
+51
View File
@@ -692,6 +692,57 @@ func TestProfileAndPasswordUpdate(t *testing.T) {
}
}
func TestUserMailSignaturesDefaultResolution(t *testing.T) {
a := newTestApp(t)
ts := httptest.NewServer(a.Router())
defer ts.Close()
admin := &testClient{t: t, server: ts}
var login map[string]any
if code := admin.do("POST", "/api/auth/login", map[string]string{"email": "admin@lanqin.local", "password": "ChangeMe123!"}, &login); code != http.StatusOK {
t.Fatalf("admin login code=%d body=%v", code, login)
}
domainID := mustDefaultDomainID(t, a)
mb1 := createTestMailbox(t, admin, domainID, "signer", "Signer", "Password123!", nil)
mb2 := createTestMailbox(t, admin, domainID, "second", "Second", "Password123!", map[string]any{"ownerEmail": mb1.Address})
user := &testClient{t: t, server: ts}
if code := user.do("POST", "/api/auth/login", map[string]string{"email": mb1.Address, "password": "Password123!"}, &login); code != http.StatusOK {
t.Fatalf("user login code=%d", code)
}
var global MailSignature
if code := user.do("POST", "/api/me/signatures", map[string]any{"name": "全局签名", "content": "Global Sig", "isDefault": true}, &global); code != http.StatusCreated || !global.IsDefault || global.MailboxID != "" {
t.Fatalf("create global signature code=%d sig=%+v", code, global)
}
var bound MailSignature
if code := user.do("POST", "/api/me/signatures", map[string]any{"mailboxId": mb1.ID, "name": "邮箱签名", "content": "Mailbox Sig", "isDefault": true}, &bound); code != http.StatusCreated || !bound.IsDefault || bound.MailboxID != mb1.ID {
t.Fatalf("create bound signature code=%d sig=%+v", code, bound)
}
var defaultResp struct {
Signature *MailSignature `json:"signature"`
}
if code := user.do("GET", "/api/me/signatures/default?mailboxId="+mb1.ID, nil, &defaultResp); code != http.StatusOK || defaultResp.Signature == nil || defaultResp.Signature.ID != bound.ID {
t.Fatalf("bound default code=%d resp=%+v", code, defaultResp)
}
if code := user.do("GET", "/api/me/signatures/default?mailboxId="+mb2.ID, nil, &defaultResp); code != http.StatusOK || defaultResp.Signature == nil || defaultResp.Signature.ID != global.ID {
t.Fatalf("global fallback code=%d resp=%+v", code, defaultResp)
}
var updated MailSignature
if code := user.do("POST", "/api/me/signatures/"+bound.ID, map[string]any{"mailboxId": mb1.ID, "name": "更新签名", "content": "Updated Sig", "isDefault": false}, &updated); code != http.StatusOK || updated.IsDefault || updated.Content != "Updated Sig" {
t.Fatalf("update signature code=%d sig=%+v", code, updated)
}
if code := user.do("GET", "/api/me/signatures/default?mailboxId="+mb1.ID, nil, &defaultResp); code != http.StatusOK || defaultResp.Signature == nil || defaultResp.Signature.ID != global.ID {
t.Fatalf("fallback after update code=%d resp=%+v", code, defaultResp)
}
var ok map[string]any
if code := user.do("DELETE", "/api/me/signatures/"+global.ID, nil, &ok); code != http.StatusOK {
t.Fatalf("delete signature code=%d body=%v", code, ok)
}
if code := user.do("GET", "/api/me/signatures/default?mailboxId="+mb2.ID, nil, &defaultResp); code != http.StatusOK || defaultResp.Signature != nil {
t.Fatalf("empty default code=%d resp=%+v", code, defaultResp)
}
}
func TestUserTwoFactorSetupAndLogin(t *testing.T) {
a := newTestApp(t)
a.cfg.TwoFactorEnabled = true