feat(external-imap): 新增外部 IMAP 全局开关与后台配置

- 支持在系统设置中启用/禁用外部 IMAP,并持久化相关密钥与 OAuth 配置。
- 在公共设置和前端页面中联动开关状态,未启用时隐藏外部 IMAP 相关功能并拦截后端接口。
- 补充外部 IMAP 默认关闭、配置说明与回调地址文档。
This commit is contained in:
LanQin
2026-06-25 22:44:32 +08:00
parent cc75382869
commit 707f11687d
12 changed files with 394 additions and 180 deletions
+168 -96
View File
@@ -10,63 +10,80 @@ import (
)
type SystemSettings struct {
PublicHostname string `json:"publicHostname"`
PublicBaseURL string `json:"publicBaseUrl"`
SMTPHost string `json:"smtpHost"`
SMTPPort string `json:"smtpPort"`
SMTPUsername string `json:"smtpUsername"`
SMTPPasswordSet bool `json:"smtpPasswordSet"`
SMTPRequireTLS bool `json:"smtpRequireTls"`
MaildirRoot string `json:"maildirRoot"`
MaildirScanSeconds int `json:"maildirScanSeconds"`
SessionTTLHours int `json:"sessionTtlHours"`
AllowInsecureHTTP bool `json:"allowInsecureHttp"`
OpenRegistration bool `json:"openRegistration"`
TwoFactorEnabled bool `json:"twoFactorEnabled"`
TurnstileEnabled bool `json:"turnstileEnabled"`
TurnstileSiteKey string `json:"turnstileSiteKey"`
TurnstileSecretSet bool `json:"turnstileSecretSet"`
CatchAllEnabled bool `json:"catchAllEnabled"`
MailAutoRefresh bool `json:"mailAutoRefresh"`
MailRefreshSeconds int `json:"mailRefreshSeconds"`
UserMailboxApplyEnabled bool `json:"userMailboxApplyEnabled"`
UserMailboxDomainIDs []string `json:"userMailboxDomainIds"`
ReservedMailboxPrefixes string `json:"reservedMailboxPrefixes"`
PublicHostname string `json:"publicHostname"`
PublicBaseURL string `json:"publicBaseUrl"`
SMTPHost string `json:"smtpHost"`
SMTPPort string `json:"smtpPort"`
SMTPUsername string `json:"smtpUsername"`
SMTPPasswordSet bool `json:"smtpPasswordSet"`
SMTPRequireTLS bool `json:"smtpRequireTls"`
MaildirRoot string `json:"maildirRoot"`
MaildirScanSeconds int `json:"maildirScanSeconds"`
SessionTTLHours int `json:"sessionTtlHours"`
AllowInsecureHTTP bool `json:"allowInsecureHttp"`
OpenRegistration bool `json:"openRegistration"`
TwoFactorEnabled bool `json:"twoFactorEnabled"`
TurnstileEnabled bool `json:"turnstileEnabled"`
TurnstileSiteKey string `json:"turnstileSiteKey"`
TurnstileSecretSet bool `json:"turnstileSecretSet"`
CatchAllEnabled bool `json:"catchAllEnabled"`
MailAutoRefresh bool `json:"mailAutoRefresh"`
MailRefreshSeconds int `json:"mailRefreshSeconds"`
UserMailboxApplyEnabled bool `json:"userMailboxApplyEnabled"`
UserMailboxDomainIDs []string `json:"userMailboxDomainIds"`
ReservedMailboxPrefixes string `json:"reservedMailboxPrefixes"`
ExternalIMAPEnabled bool `json:"externalImapEnabled"`
ExternalIMAPSecretSet bool `json:"externalImapSecretSet"`
ExternalIMAPSyncSeconds int `json:"externalImapSyncSeconds"`
ExternalIMAPAllowPrivateHosts bool `json:"externalImapAllowPrivateHosts"`
ExternalIMAPGmailClientID string `json:"externalImapGmailClientId"`
ExternalIMAPGmailClientSecretSet bool `json:"externalImapGmailClientSecretSet"`
ExternalIMAPOutlookClientID string `json:"externalImapOutlookClientId"`
ExternalIMAPOutlookClientSecretSet bool `json:"externalImapOutlookClientSecretSet"`
}
type systemSettingsUpdate struct {
PublicHostname string `json:"publicHostname"`
PublicBaseURL string `json:"publicBaseUrl"`
SMTPHost string `json:"smtpHost"`
SMTPPort string `json:"smtpPort"`
SMTPUsername string `json:"smtpUsername"`
SMTPPassword string `json:"smtpPassword"`
SMTPRequireTLS bool `json:"smtpRequireTls"`
MaildirRoot string `json:"maildirRoot"`
MaildirScanSeconds int `json:"maildirScanSeconds"`
SessionTTLHours int `json:"sessionTtlHours"`
AllowInsecureHTTP bool `json:"allowInsecureHttp"`
OpenRegistration bool `json:"openRegistration"`
TwoFactorEnabled bool `json:"twoFactorEnabled"`
TurnstileEnabled bool `json:"turnstileEnabled"`
TurnstileSiteKey string `json:"turnstileSiteKey"`
TurnstileSecretKey string `json:"turnstileSecretKey"`
CatchAllEnabled bool `json:"catchAllEnabled"`
MailAutoRefresh bool `json:"mailAutoRefresh"`
MailRefreshSeconds int `json:"mailRefreshSeconds"`
UserMailboxApplyEnabled bool `json:"userMailboxApplyEnabled"`
UserMailboxDomainIDs []string `json:"userMailboxDomainIds"`
ReservedMailboxPrefixes string `json:"reservedMailboxPrefixes"`
PublicHostname string `json:"publicHostname"`
PublicBaseURL string `json:"publicBaseUrl"`
SMTPHost string `json:"smtpHost"`
SMTPPort string `json:"smtpPort"`
SMTPUsername string `json:"smtpUsername"`
SMTPPassword string `json:"smtpPassword"`
SMTPRequireTLS bool `json:"smtpRequireTls"`
MaildirRoot string `json:"maildirRoot"`
MaildirScanSeconds int `json:"maildirScanSeconds"`
SessionTTLHours int `json:"sessionTtlHours"`
AllowInsecureHTTP bool `json:"allowInsecureHttp"`
OpenRegistration bool `json:"openRegistration"`
TwoFactorEnabled bool `json:"twoFactorEnabled"`
TurnstileEnabled bool `json:"turnstileEnabled"`
TurnstileSiteKey string `json:"turnstileSiteKey"`
TurnstileSecretKey string `json:"turnstileSecretKey"`
CatchAllEnabled bool `json:"catchAllEnabled"`
MailAutoRefresh bool `json:"mailAutoRefresh"`
MailRefreshSeconds int `json:"mailRefreshSeconds"`
UserMailboxApplyEnabled bool `json:"userMailboxApplyEnabled"`
UserMailboxDomainIDs []string `json:"userMailboxDomainIds"`
ReservedMailboxPrefixes string `json:"reservedMailboxPrefixes"`
ExternalIMAPEnabled bool `json:"externalImapEnabled"`
ExternalIMAPSecretKey string `json:"externalImapSecretKey"`
ExternalIMAPSyncSeconds int `json:"externalImapSyncSeconds"`
ExternalIMAPAllowPrivateHosts bool `json:"externalImapAllowPrivateHosts"`
ExternalIMAPGmailClientID string `json:"externalImapGmailClientId"`
ExternalIMAPGmailClientSecret string `json:"externalImapGmailClientSecret"`
ExternalIMAPOutlookClientID string `json:"externalImapOutlookClientId"`
ExternalIMAPOutlookClientSecret string `json:"externalImapOutlookClientSecret"`
}
type PublicSettings struct {
OpenRegistration bool `json:"openRegistration"`
TurnstileEnabled bool `json:"turnstileEnabled"`
TurnstileSiteKey string `json:"turnstileSiteKey"`
PublicHostname string `json:"publicHostname"`
MailAutoRefresh bool `json:"mailAutoRefresh"`
MailRefreshMs int `json:"mailRefreshMs"`
MailboxDomains []PublicDomain `json:"mailboxDomains,omitempty"`
OpenRegistration bool `json:"openRegistration"`
TurnstileEnabled bool `json:"turnstileEnabled"`
TurnstileSiteKey string `json:"turnstileSiteKey"`
PublicHostname string `json:"publicHostname"`
MailAutoRefresh bool `json:"mailAutoRefresh"`
MailRefreshMs int `json:"mailRefreshMs"`
ExternalIMAPEnabled bool `json:"externalImapEnabled"`
MailboxDomains []PublicDomain `json:"mailboxDomains,omitempty"`
}
type PublicDomain struct {
@@ -88,7 +105,7 @@ func (a *App) handlePublicSettings(w http.ResponseWriter, r *http.Request) {
if refreshSeconds <= 0 {
refreshSeconds = 30
}
settings := PublicSettings{OpenRegistration: a.cfg.OpenRegistration, TurnstileEnabled: enabled, TurnstileSiteKey: a.cfg.TurnstileSiteKey, PublicHostname: a.cfg.PublicHostname, MailAutoRefresh: a.cfg.MailAutoRefresh, MailRefreshMs: refreshSeconds * 1000}
settings := PublicSettings{OpenRegistration: a.cfg.OpenRegistration, TurnstileEnabled: enabled, TurnstileSiteKey: a.cfg.TurnstileSiteKey, PublicHostname: a.cfg.PublicHostname, MailAutoRefresh: a.cfg.MailAutoRefresh, MailRefreshMs: refreshSeconds * 1000, ExternalIMAPEnabled: a.cfg.ExternalIMAPEnabled}
// Include available domains for mailbox creation during registration
if a.cfg.OpenRegistration {
@@ -165,6 +182,27 @@ func (a *App) handleUpdateSystemSettings(w http.ResponseWriter, r *http.Request)
next.UserMailboxApplyEnabled = req.UserMailboxApplyEnabled
next.UserMailboxDomainIDs = strings.Join(cleanIDList(req.UserMailboxDomainIDs), ",")
next.ReservedMailboxPrefixes = strings.Join(parseReservedPrefixes(req.ReservedMailboxPrefixes), ",")
next.ExternalIMAPEnabled = req.ExternalIMAPEnabled
if strings.TrimSpace(req.ExternalIMAPSecretKey) != "" {
next.ExternalIMAPSecretKey = strings.TrimSpace(req.ExternalIMAPSecretKey)
}
if req.ExternalIMAPSyncSeconds <= 0 {
req.ExternalIMAPSyncSeconds = 300
}
next.ExternalIMAPSyncSeconds = req.ExternalIMAPSyncSeconds
next.ExternalIMAPAllowPrivateHosts = req.ExternalIMAPAllowPrivateHosts
next.ExternalIMAPGmailClientID = strings.TrimSpace(req.ExternalIMAPGmailClientID)
if strings.TrimSpace(req.ExternalIMAPGmailClientSecret) != "" {
next.ExternalIMAPGmailClientSecret = strings.TrimSpace(req.ExternalIMAPGmailClientSecret)
}
next.ExternalIMAPOutlookClientID = strings.TrimSpace(req.ExternalIMAPOutlookClientID)
if strings.TrimSpace(req.ExternalIMAPOutlookClientSecret) != "" {
next.ExternalIMAPOutlookClientSecret = strings.TrimSpace(req.ExternalIMAPOutlookClientSecret)
}
if next.ExternalIMAPEnabled && strings.TrimSpace(next.ExternalIMAPSecretKey) == "" {
badRequest(w, errors.New("外部 IMAP 加密密钥未设置"))
return
}
if err := a.saveSystemSettings(r.Context(), next); err != nil {
respondError(w, http.StatusInternalServerError, "failed to save settings")
@@ -248,28 +286,36 @@ func (a *App) handleTestSMTP(w http.ResponseWriter, r *http.Request) {
func (a *App) systemSettingsSnapshot() SystemSettings {
return SystemSettings{
PublicHostname: a.cfg.PublicHostname,
PublicBaseURL: a.cfg.PublicBaseURL,
SMTPHost: a.cfg.SMTPHost,
SMTPPort: a.cfg.SMTPPort,
SMTPUsername: a.cfg.SMTPUsername,
SMTPPasswordSet: strings.TrimSpace(a.cfg.SMTPPassword) != "",
SMTPRequireTLS: a.cfg.SMTPRequireTLS,
MaildirRoot: a.cfg.MaildirRoot,
MaildirScanSeconds: a.cfg.MaildirScanSeconds,
SessionTTLHours: a.cfg.SessionTTLHours,
AllowInsecureHTTP: a.cfg.AllowInsecureHTTP,
OpenRegistration: a.cfg.OpenRegistration,
TwoFactorEnabled: a.cfg.TwoFactorEnabled,
TurnstileEnabled: a.cfg.TurnstileEnabled,
TurnstileSiteKey: a.cfg.TurnstileSiteKey,
TurnstileSecretSet: strings.TrimSpace(a.cfg.TurnstileSecretKey) != "",
CatchAllEnabled: a.cfg.CatchAllEnabled,
MailAutoRefresh: a.cfg.MailAutoRefresh,
MailRefreshSeconds: a.cfg.MailRefreshSeconds,
UserMailboxApplyEnabled: a.cfg.UserMailboxApplyEnabled,
UserMailboxDomainIDs: cleanIDList(strings.Split(a.cfg.UserMailboxDomainIDs, ",")),
ReservedMailboxPrefixes: strings.Join(parseReservedPrefixes(a.cfg.ReservedMailboxPrefixes), "\n"),
PublicHostname: a.cfg.PublicHostname,
PublicBaseURL: a.cfg.PublicBaseURL,
SMTPHost: a.cfg.SMTPHost,
SMTPPort: a.cfg.SMTPPort,
SMTPUsername: a.cfg.SMTPUsername,
SMTPPasswordSet: strings.TrimSpace(a.cfg.SMTPPassword) != "",
SMTPRequireTLS: a.cfg.SMTPRequireTLS,
MaildirRoot: a.cfg.MaildirRoot,
MaildirScanSeconds: a.cfg.MaildirScanSeconds,
SessionTTLHours: a.cfg.SessionTTLHours,
AllowInsecureHTTP: a.cfg.AllowInsecureHTTP,
OpenRegistration: a.cfg.OpenRegistration,
TwoFactorEnabled: a.cfg.TwoFactorEnabled,
TurnstileEnabled: a.cfg.TurnstileEnabled,
TurnstileSiteKey: a.cfg.TurnstileSiteKey,
TurnstileSecretSet: strings.TrimSpace(a.cfg.TurnstileSecretKey) != "",
CatchAllEnabled: a.cfg.CatchAllEnabled,
MailAutoRefresh: a.cfg.MailAutoRefresh,
MailRefreshSeconds: a.cfg.MailRefreshSeconds,
UserMailboxApplyEnabled: a.cfg.UserMailboxApplyEnabled,
UserMailboxDomainIDs: cleanIDList(strings.Split(a.cfg.UserMailboxDomainIDs, ",")),
ReservedMailboxPrefixes: strings.Join(parseReservedPrefixes(a.cfg.ReservedMailboxPrefixes), "\n"),
ExternalIMAPEnabled: a.cfg.ExternalIMAPEnabled,
ExternalIMAPSecretSet: strings.TrimSpace(a.cfg.ExternalIMAPSecretKey) != "",
ExternalIMAPSyncSeconds: a.cfg.ExternalIMAPSyncSeconds,
ExternalIMAPAllowPrivateHosts: a.cfg.ExternalIMAPAllowPrivateHosts,
ExternalIMAPGmailClientID: a.cfg.ExternalIMAPGmailClientID,
ExternalIMAPGmailClientSecretSet: strings.TrimSpace(a.cfg.ExternalIMAPGmailClientSecret) != "",
ExternalIMAPOutlookClientID: a.cfg.ExternalIMAPOutlookClientID,
ExternalIMAPOutlookClientSecretSet: strings.TrimSpace(a.cfg.ExternalIMAPOutlookClientSecret) != "",
}
}
@@ -335,6 +381,24 @@ func (a *App) loadPersistedSystemSettings(ctx context.Context) error {
a.cfg.UserMailboxDomainIDs = value
case "reservedMailboxPrefixes":
a.cfg.ReservedMailboxPrefixes = value
case "externalImapEnabled":
a.cfg.ExternalIMAPEnabled = value == "true"
case "externalImapSecretKey":
a.cfg.ExternalIMAPSecretKey = value
case "externalImapSyncSeconds":
if n, err := strconv.Atoi(value); err == nil && n > 0 {
a.cfg.ExternalIMAPSyncSeconds = n
}
case "externalImapAllowPrivateHosts":
a.cfg.ExternalIMAPAllowPrivateHosts = value == "true"
case "externalImapGmailClientId":
a.cfg.ExternalIMAPGmailClientID = value
case "externalImapGmailClientSecret":
a.cfg.ExternalIMAPGmailClientSecret = value
case "externalImapOutlookClientId":
a.cfg.ExternalIMAPOutlookClientID = value
case "externalImapOutlookClientSecret":
a.cfg.ExternalIMAPOutlookClientSecret = value
}
}
return rows.Err()
@@ -342,28 +406,36 @@ func (a *App) loadPersistedSystemSettings(ctx context.Context) error {
func (a *App) saveSystemSettings(ctx context.Context, cfg Config) error {
values := map[string]string{
"publicHostname": cfg.PublicHostname,
"publicBaseUrl": cfg.PublicBaseURL,
"smtpHost": cfg.SMTPHost,
"smtpPort": cfg.SMTPPort,
"smtpUsername": cfg.SMTPUsername,
"smtpPassword": cfg.SMTPPassword,
"smtpRequireTls": strconv.FormatBool(cfg.SMTPRequireTLS),
"maildirRoot": cfg.MaildirRoot,
"maildirScanSeconds": strconv.Itoa(cfg.MaildirScanSeconds),
"sessionTtlHours": strconv.Itoa(cfg.SessionTTLHours),
"allowInsecureHttp": strconv.FormatBool(cfg.AllowInsecureHTTP),
"openRegistration": strconv.FormatBool(cfg.OpenRegistration),
"twoFactorEnabled": strconv.FormatBool(cfg.TwoFactorEnabled),
"turnstileEnabled": strconv.FormatBool(cfg.TurnstileEnabled),
"turnstileSiteKey": cfg.TurnstileSiteKey,
"turnstileSecretKey": cfg.TurnstileSecretKey,
"catchAllEnabled": strconv.FormatBool(cfg.CatchAllEnabled),
"mailAutoRefresh": strconv.FormatBool(cfg.MailAutoRefresh),
"mailRefreshSeconds": strconv.Itoa(cfg.MailRefreshSeconds),
"userMailboxApplyEnabled": strconv.FormatBool(cfg.UserMailboxApplyEnabled),
"userMailboxDomainIds": strings.Join(cleanIDList(strings.Split(cfg.UserMailboxDomainIDs, ",")), ","),
"reservedMailboxPrefixes": strings.Join(parseReservedPrefixes(cfg.ReservedMailboxPrefixes), ","),
"publicHostname": cfg.PublicHostname,
"publicBaseUrl": cfg.PublicBaseURL,
"smtpHost": cfg.SMTPHost,
"smtpPort": cfg.SMTPPort,
"smtpUsername": cfg.SMTPUsername,
"smtpPassword": cfg.SMTPPassword,
"smtpRequireTls": strconv.FormatBool(cfg.SMTPRequireTLS),
"maildirRoot": cfg.MaildirRoot,
"maildirScanSeconds": strconv.Itoa(cfg.MaildirScanSeconds),
"sessionTtlHours": strconv.Itoa(cfg.SessionTTLHours),
"allowInsecureHttp": strconv.FormatBool(cfg.AllowInsecureHTTP),
"openRegistration": strconv.FormatBool(cfg.OpenRegistration),
"twoFactorEnabled": strconv.FormatBool(cfg.TwoFactorEnabled),
"turnstileEnabled": strconv.FormatBool(cfg.TurnstileEnabled),
"turnstileSiteKey": cfg.TurnstileSiteKey,
"turnstileSecretKey": cfg.TurnstileSecretKey,
"catchAllEnabled": strconv.FormatBool(cfg.CatchAllEnabled),
"mailAutoRefresh": strconv.FormatBool(cfg.MailAutoRefresh),
"mailRefreshSeconds": strconv.Itoa(cfg.MailRefreshSeconds),
"userMailboxApplyEnabled": strconv.FormatBool(cfg.UserMailboxApplyEnabled),
"userMailboxDomainIds": strings.Join(cleanIDList(strings.Split(cfg.UserMailboxDomainIDs, ",")), ","),
"reservedMailboxPrefixes": strings.Join(parseReservedPrefixes(cfg.ReservedMailboxPrefixes), ","),
"externalImapEnabled": strconv.FormatBool(cfg.ExternalIMAPEnabled),
"externalImapSecretKey": cfg.ExternalIMAPSecretKey,
"externalImapSyncSeconds": strconv.Itoa(cfg.ExternalIMAPSyncSeconds),
"externalImapAllowPrivateHosts": strconv.FormatBool(cfg.ExternalIMAPAllowPrivateHosts),
"externalImapGmailClientId": cfg.ExternalIMAPGmailClientID,
"externalImapGmailClientSecret": cfg.ExternalIMAPGmailClientSecret,
"externalImapOutlookClientId": cfg.ExternalIMAPOutlookClientID,
"externalImapOutlookClientSecret": cfg.ExternalIMAPOutlookClientSecret,
}
now := a.now().UTC().Format(time.RFC3339Nano)
tx, err := a.db.BeginTx(ctx, nil)