统一邮件协议域名读取运行时快照
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-08-31 00:25:12 +08:00
parent dfd27a34b6
commit d4c3994102
8 changed files with 123 additions and 28 deletions

View File

@ -127,6 +127,40 @@ type MailPorts struct {
POP3 int
}
type MailDomains struct {
Primary string
Accepted []string
}
func CurrentMailDomains() MailDomains {
manager := CurrentManager()
if manager != nil {
runtime := manager.Snapshot()
if runtime.Generation != 0 {
return MailDomains{
Primary: runtime.Root.Domains.Primary,
Accepted: append([]string(nil), runtime.Root.Domains.Accepted...),
}
}
}
cfg := CurrentConfig()
if cfg == nil {
return MailDomains{}
}
domains := MailDomains{
Primary: cfg.Domain,
Accepted: append([]string(nil), cfg.Domains...),
}
if domains.Primary == "" && len(domains.Accepted) > 0 {
domains.Primary = domains.Accepted[0]
}
if len(domains.Accepted) == 0 && domains.Primary != "" {
domains.Accepted = []string{domains.Primary}
}
return domains
}
type SMTPRuntimeSettings struct {
Relay SMTPConfig
Password string

View File

@ -129,6 +129,47 @@ func TestCurrentMailPortsUsesDefaultsForUnavailableSnapshot(t *testing.T) {
}
}
func TestCurrentMailDomainsReadsPartitionSnapshot(t *testing.T) {
previousManager := CurrentManager()
defer SetRuntimeManager(previousManager)
runtime := validRuntimeConfig()
runtime.Root.Domains = DomainConfig{
Primary: "partition.example.com",
Accepted: []string{"partition.example.com", "alias.example.com"},
}
SetRuntimeManager(&ConfigManager{snapshots: runtimeAccessSnapshotStore{runtime: runtime}})
got := CurrentMailDomains()
if got.Primary != "partition.example.com" || len(got.Accepted) != 2 || got.Accepted[1] != "alias.example.com" {
t.Fatalf("mail domains = %+v, want partition snapshot", got)
}
got.Accepted[0] = "changed.example.com"
if runtime.Root.Domains.Accepted[0] != "partition.example.com" {
t.Fatal("mail domain snapshot leaked mutable accepted domains")
}
}
func TestCurrentMailDomainsFallsBackToLegacyConfig(t *testing.T) {
previousManager := CurrentManager()
previousInstance := Instance
defer func() {
SetRuntimeManager(previousManager)
Instance = previousInstance
PublishConfigSnapshot(previousInstance)
}()
SetRuntimeManager(nil)
Instance = &Config{Domain: "legacy.example.com"}
PublishConfigSnapshot(Instance)
got := CurrentMailDomains()
if got.Primary != "legacy.example.com" || len(got.Accepted) != 1 || got.Accepted[0] != got.Primary {
t.Fatalf("legacy mail domains = %+v, want normalized fallback", got)
}
}
func TestCurrentSMTPRuntimeSettingsReadsPartitionSnapshot(t *testing.T) {
previousManager := CurrentManager()
defer SetRuntimeManager(previousManager)

View File

@ -100,8 +100,8 @@ func (p *Dkim) Sign(msgData string) ([]byte, error) {
if p == nil || p.privateKey == nil {
return nil, fmt.Errorf("DKIM signer is not initialized")
}
cfg := config.CurrentConfig()
if cfg == nil || strings.TrimSpace(cfg.Domain) == "" {
domains := config.CurrentMailDomains()
if strings.TrimSpace(domains.Primary) == "" {
return nil, fmt.Errorf("DKIM domain is not configured")
}
@ -109,7 +109,7 @@ func (p *Dkim) Sign(msgData string) ([]byte, error) {
r := strings.NewReader(msgData)
options := &dkim.SignOptions{
Domain: cfg.Domain,
Domain: domains.Primary,
Selector: "default",
Signer: p.privateKey,
}

View File

@ -18,6 +18,10 @@ func StartWithTls() error {
if cfg == nil {
return fmt.Errorf("runtime configuration is unavailable")
}
domains := config.CurrentMailDomains()
if domains.Primary == "" {
return fmt.Errorf("mail domain is not configured")
}
crt, err := config.LoadRuntimeTLSCertificate(cfg)
if err != nil {
return fmt.Errorf("load POP3 TLS certificate: %w", err)
@ -26,7 +30,7 @@ func StartWithTls() error {
tlsConfig.Certificates = []tls.Certificate{crt}
tlsConfig.Time = time.Now
tlsConfig.Rand = rand.Reader
instanceTls = gopop.NewPop3Server(995, "pop."+cfg.Domain, true, tlsConfig, action{})
instanceTls = gopop.NewPop3Server(995, "pop."+domains.Primary, true, tlsConfig, action{})
instanceTls.ConnectAliveTime = 5 * time.Minute
log.Infof("POP3 With TLS Server Start On Port :995")
@ -43,6 +47,10 @@ func Start() error {
if cfg == nil {
return fmt.Errorf("runtime configuration is unavailable")
}
domains := config.CurrentMailDomains()
if domains.Primary == "" {
return fmt.Errorf("mail domain is not configured")
}
crt, err := config.LoadRuntimeTLSCertificate(cfg)
if err != nil {
return fmt.Errorf("load POP3 certificate: %w", err)
@ -52,7 +60,7 @@ func Start() error {
tlsConfig.Time = time.Now
tlsConfig.Rand = rand.Reader
port := config.CurrentMailPorts().POP3
instance = gopop.NewPop3Server(port, "pop."+cfg.Domain, false, tlsConfig, action{})
instance = gopop.NewPop3Server(port, "pop."+domains.Primary, false, tlsConfig, action{})
instance.ConnectAliveTime = 5 * time.Minute
log.Infof("POP3 Server Start On Port :%d", port)

View File

@ -123,8 +123,8 @@ func (s *Session) Data(r io.Reader) error {
SPFStatus = spfCheck(s.RemoteAddress.String(), email.Sender, email.Sender.EmailAddress)
_, formDomain := email.From.GetDomainAccount()
cfg := config.CurrentConfig()
spoofed := cfg != nil && array.InArray(formDomain, cfg.Domains) && SPFStatus == false
domains := config.CurrentMailDomains()
spoofed := array.InArray(formDomain, domains.Accepted) && SPFStatus == false
if spoofed {
dkimStatus = false
}
@ -227,17 +227,17 @@ func validateOutgoingEmail(email *parsemail.Email) error {
}
func authorizedSubmissionEmail(email *parsemail.Email, envelopeFrom string, ctx *context.Context) bool {
cfg := config.CurrentConfig()
if email == nil || email.From == nil || email.Sender == nil || ctx == nil || cfg == nil {
domains := config.CurrentMailDomains()
if email == nil || email.From == nil || email.Sender == nil || ctx == nil || len(domains.Accepted) == 0 {
return false
}
if !validation.IsAuthorizedSender(email.From.EmailAddress, ctx.UserAccount, ctx.IsAdmin, cfg.Domains) {
if !validation.IsAuthorizedSender(email.From.EmailAddress, ctx.UserAccount, ctx.IsAdmin, domains.Accepted) {
return false
}
if !validation.IsAuthorizedSender(email.Sender.EmailAddress, ctx.UserAccount, ctx.IsAdmin, cfg.Domains) {
if !validation.IsAuthorizedSender(email.Sender.EmailAddress, ctx.UserAccount, ctx.IsAdmin, domains.Accepted) {
return false
}
return envelopeFrom == "" || validation.IsAuthorizedSender(envelopeFrom, ctx.UserAccount, ctx.IsAdmin, cfg.Domains)
return envelopeFrom == "" || validation.IsAuthorizedSender(envelopeFrom, ctx.UserAccount, ctx.IsAdmin, domains.Accepted)
}
func saveEmail(ctx *context.Context, size int, email *parsemail.Email, sendUserID int, emailType int, reallyTo []string, SPFStatus, dkimStatus bool) ([]*models.User, *models.Email, error) {
@ -259,11 +259,11 @@ func saveEmail(ctx *context.Context, size int, email *parsemail.Email, sendUserI
msgID := email.MsgID
if msgID == "" {
cfg := config.CurrentConfig()
if cfg == nil || cfg.Domain == "" {
domains := config.CurrentMailDomains()
if domains.Primary == "" {
return nil, nil, fmt.Errorf("runtime configuration is unavailable")
}
msgID, err = parsemail.GenerateMsgID(cfg.Domain)
msgID, err = parsemail.GenerateMsgID(domains.Primary)
if err != nil {
return users, nil, fmt.Errorf("generate message ID: %w", err)
}

View File

@ -26,12 +26,16 @@ func StartWithTLSNew() error {
if cfg == nil {
return fmt.Errorf("runtime configuration is unavailable")
}
domains := config.CurrentMailDomains()
if domains.Primary == "" {
return fmt.Errorf("mail domain is not configured")
}
be := &Backend{}
instanceTlsNew = smtp.NewServer(be)
instanceTlsNew.Addr = ":587"
instanceTlsNew.Domain = cfg.Domain
instanceTlsNew.Domain = domains.Primary
instanceTlsNew.ReadTimeout = 10 * time.Second
instanceTlsNew.WriteTimeout = 10 * time.Second
instanceTlsNew.MaxMessageBytes = maxSMTPMessageBytes
@ -59,12 +63,16 @@ func StartWithTLS() error {
if cfg == nil {
return fmt.Errorf("runtime configuration is unavailable")
}
domains := config.CurrentMailDomains()
if domains.Primary == "" {
return fmt.Errorf("mail domain is not configured")
}
be := &Backend{}
instanceTls = smtp.NewServer(be)
instanceTls.Addr = ":465"
instanceTls.Domain = cfg.Domain
instanceTls.Domain = domains.Primary
instanceTls.ReadTimeout = 10 * time.Second
instanceTls.WriteTimeout = 10 * time.Second
instanceTls.MaxMessageBytes = maxSMTPMessageBytes
@ -91,12 +99,16 @@ func Start() error {
if cfg == nil {
return fmt.Errorf("runtime configuration is unavailable")
}
domains := config.CurrentMailDomains()
if domains.Primary == "" {
return fmt.Errorf("mail domain is not configured")
}
be := &Backend{}
instance = smtp.NewServer(be)
instance.Addr = fmt.Sprintf(":%d", config.CurrentMailPorts().SMTP)
instance.Domain = cfg.Domain
instance.Domain = domains.Primary
instance.ReadTimeout = 10 * time.Second
instance.WriteTimeout = 10 * time.Second
instance.MaxMessageBytes = maxSMTPMessageBytes

View File

@ -51,23 +51,23 @@ func Forward(ctx *context.Context, e *parsemail.Email, forwardAddress string, us
log.WithContext(ctx).Debug("forward message prepared")
cfg := config.CurrentConfig()
if cfg == nil || len(cfg.Domains) == 0 {
domains := config.CurrentMailDomains()
if len(domains.Accepted) == 0 {
return errors.New("runtime configuration is unavailable")
}
from := user.Account + "@" + cfg.Domains[0]
return forwardData(ctx, cfg.Domains[0], b, forwardAddress, from)
from := user.Account + "@" + domains.Primary
return forwardData(ctx, domains.Primary, b, forwardAddress, from)
}
func ForwardRaw(ctx *context.Context, e *parsemail.Email, rawEmailData []byte, forwardAddress string, user *models.User) error {
log.WithContext(ctx).Debugf("开始原始邮件转发")
cfg := config.CurrentConfig()
if cfg == nil || len(cfg.Domains) == 0 {
domains := config.CurrentMailDomains()
if len(domains.Accepted) == 0 {
return errors.New("runtime configuration is unavailable")
}
from := user.Account + "@" + cfg.Domains[0]
return forwardData(ctx, cfg.Domains[0], rawEmailData, forwardAddress, from)
from := user.Account + "@" + domains.Primary
return forwardData(ctx, domains.Primary, rawEmailData, forwardAddress, from)
}
func forwardData(ctx *context.Context, fromDomain string, data []byte, forwardAddress string, from string) error {

View File

@ -221,8 +221,8 @@ func NewClient(conn net.Conn, host, fromDomain string) (*Client, error) {
if fromDomain != "" {
localName = fromDomain
} else if cfg := config.CurrentConfig(); cfg != nil && cfg.Domain != "" {
localName = cfg.Domain
} else if domains := config.CurrentMailDomains(); domains.Primary != "" {
localName = domains.Primary
}
c := &Client{Text: text, conn: conn, serverName: host, localName: localName}