refactor(mail): 统一邮件认证策略请求与返回格式。
- 将 `auth-policy` 调整为读取 `login`、`remote` 等字段,并返回数值型 `status`。 - 支持按邮箱定位用户与邮箱箱体,补充 IMAP/POP3 认证策略校验。 - 更新 Dovecot 与 Postfix 配置,移除原有的发件人 BCC 依赖。 - 补充相关测试并同步部署说明。
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
*.sh text eol=lf
|
||||
deploy/**/entrypoint.sh text eol=lf
|
||||
deploy/**/sync-dkim.sh text eol=lf
|
||||
@@ -814,6 +814,29 @@ func TestAdminSMTPTestEndpoint(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthPolicyDovecotResponseFormat(t *testing.T) {
|
||||
a := newTestApp(t)
|
||||
ts := httptest.NewServer(a.Router())
|
||||
defer ts.Close()
|
||||
client := &testClient{t: t, server: ts}
|
||||
|
||||
var allowed map[string]any
|
||||
if code := client.do("POST", "/auth-policy?command=allow", map[string]string{"login": "admin@lanqin.local", "protocol": "smtp"}, &allowed); code != http.StatusOK {
|
||||
t.Fatalf("auth policy allow code=%d body=%v", code, allowed)
|
||||
}
|
||||
if allowed["status"] != float64(0) {
|
||||
t.Fatalf("expected numeric allow status 0, got %#v", allowed["status"])
|
||||
}
|
||||
|
||||
var denied map[string]any
|
||||
if code := client.do("POST", "/auth-policy?command=allow", map[string]string{"login": "missing@lanqin.local", "protocol": "imap"}, &denied); code != http.StatusOK {
|
||||
t.Fatalf("auth policy deny code=%d body=%v", code, denied)
|
||||
}
|
||||
if denied["status"] != float64(-1) {
|
||||
t.Fatalf("expected numeric deny status -1, got %#v", denied["status"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileAndPasswordUpdate(t *testing.T) {
|
||||
a := newTestApp(t)
|
||||
ts := httptest.NewServer(a.Router())
|
||||
|
||||
@@ -599,48 +599,67 @@ func (a *App) checkAndRecordProtocolRate(ctx context.Context, user *User, mb *Ma
|
||||
|
||||
func (a *App) handleAuthPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Login string `json:"login"`
|
||||
Protocol string `json:"protocol"`
|
||||
Username string `json:"username"`
|
||||
IP string `json:"ip"`
|
||||
Remote string `json:"remote"`
|
||||
Success *bool `json:"success"`
|
||||
PolicyReject *bool `json:"policy_reject"`
|
||||
}
|
||||
if err := decodeJSON(r, &req); err != nil {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
respondJSON(w, http.StatusCreated, map[string]string{"status": "allow"})
|
||||
respondJSON(w, http.StatusOK, map[string]int{"status": 0})
|
||||
return
|
||||
}
|
||||
var user *User
|
||||
if req.Username != "" {
|
||||
var mailbox *Mailbox
|
||||
login := normalizeEmail(req.Login)
|
||||
if login == "" {
|
||||
login = normalizeEmail(req.Username)
|
||||
}
|
||||
if login != "" {
|
||||
if mb, err := a.mailboxByAddress(r.Context(), login); err == nil {
|
||||
mailbox = mb
|
||||
user, _ = a.userByID(r.Context(), mb.UserID)
|
||||
}
|
||||
if user == nil {
|
||||
var passHash string
|
||||
user, passHash, _ = a.userByEmail(r.Context(), req.Username)
|
||||
user, passHash, _ = a.userByEmail(r.Context(), login)
|
||||
_ = passHash
|
||||
}
|
||||
}
|
||||
if user == nil || user.Disabled {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
respondJSON(w, http.StatusCreated, map[string]string{"status": "deny", "reason": "user not found or disabled"})
|
||||
respondJSON(w, http.StatusOK, map[string]any{"status": -1, "msg": "user not found or disabled"})
|
||||
return
|
||||
}
|
||||
if user.Role == "admin" {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
respondJSON(w, http.StatusCreated, map[string]string{"status": "allow"})
|
||||
respondJSON(w, http.StatusOK, map[string]int{"status": 0})
|
||||
return
|
||||
}
|
||||
limits := user.Limits
|
||||
var err error
|
||||
switch req.Protocol {
|
||||
case "imap", "IMAP":
|
||||
switch strings.ToLower(req.Protocol) {
|
||||
case "imap":
|
||||
if limits.IMAPMinuteLimit > 0 {
|
||||
err = a.checkAndRecordProtocolRate(r.Context(), user, nil, "imap_events", 0, limits.IMAPMinuteLimit)
|
||||
}
|
||||
case "pop3", "POP3":
|
||||
if limits.POP3MinuteLimit > 0 {
|
||||
err = a.checkAndRecordProtocolRate(r.Context(), user, nil, "pop3_events", 0, limits.POP3MinuteLimit)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err != nil {
|
||||
respondJSON(w, http.StatusCreated, map[string]any{"status": "deny", "reason": err.Error()})
|
||||
if mailbox == nil {
|
||||
err = errors.New("mailbox not found")
|
||||
} else {
|
||||
respondJSON(w, http.StatusCreated, map[string]any{"status": "allow"})
|
||||
err = a.checkAndRecordProtocolRate(r.Context(), user, mailbox, "imap_events", 0, limits.IMAPMinuteLimit)
|
||||
}
|
||||
}
|
||||
case "pop3":
|
||||
if limits.POP3MinuteLimit > 0 {
|
||||
if mailbox == nil {
|
||||
err = errors.New("mailbox not found")
|
||||
} else {
|
||||
err = a.checkAndRecordProtocolRate(r.Context(), user, mailbox, "pop3_events", 0, limits.POP3MinuteLimit)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
respondJSON(w, http.StatusOK, map[string]any{"status": -1, "msg": err.Error()})
|
||||
} else {
|
||||
respondJSON(w, http.StatusOK, map[string]int{"status": 0})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -128,7 +128,7 @@ docker compose -f docker-compose.stack.yml -f docker-compose.stack.build.yml up
|
||||
- Rspamd 会周期性从 SQLite 导出域名 DKIM 私钥到容器内 `/var/lib/rspamd/dkim`。
|
||||
- Go API 是 Webmail 和管理后台入口;浏览器不直接连接 SMTP/IMAP/POP3。
|
||||
- Go API 会读取 `LANQIN_MAILDIR_ROOT=/var/mail/vhosts`,周期扫描 Maildir,把 Postfix/Dovecot 入站邮件同步成 Webmail 索引。
|
||||
- 第三方客户端通过 SMTP `465/587` 发信时,Postfix 会把已认证发件人的邮件自动 BCC 到 `发件人+Sent@域名`,Dovecot LMTP 会保存到该邮箱的 `Sent` 文件夹,Webmail 扫描后会显示在“已发送”。
|
||||
- 第三方客户端可通过 SMTP `465/587` 发信;Webmail 内的“已发送”由 Webmail API 发信流程写入。
|
||||
|
||||
## 邮件客户端 TLS 证书
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ auth_policy_server_api_header = Content-Type: application/json
|
||||
auth_policy_hash_mech = sha256
|
||||
auth_policy_hash_truncate = 12
|
||||
auth_policy_hash_nonce = __LANQIN_AUTH_POLICY_HASH_NONCE__
|
||||
auth_policy_request_attributes = protocol=imap username=user
|
||||
auth_policy_request_attributes = login=%{requested_username} remote=%{rip} protocol=%s
|
||||
|
||||
namespace inbox {
|
||||
inbox = yes
|
||||
|
||||
@@ -4,13 +4,11 @@ submission inet n - n - - smtpd
|
||||
-o smtpd_tls_security_level=may
|
||||
-o smtpd_sasl_auth_enable=yes
|
||||
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
|
||||
-o sender_bcc_maps=sqlite:/etc/postfix/sqlite-sender-bcc.cf
|
||||
smtps inet n - n - - smtpd
|
||||
-o syslog_name=postfix/smtps
|
||||
-o smtpd_tls_wrappermode=yes
|
||||
-o smtpd_sasl_auth_enable=yes
|
||||
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
|
||||
-o sender_bcc_maps=sqlite:/etc/postfix/sqlite-sender-bcc.cf
|
||||
pickup unix n - n 60 1 pickup
|
||||
cleanup unix n - n - 0 cleanup
|
||||
qmgr unix n - n 300 1 qmgr
|
||||
|
||||
Reference in New Issue
Block a user