diff --git a/server/config/runtime_access.go b/server/config/runtime_access.go index 921c70a..2a6e1b1 100644 --- a/server/config/runtime_access.go +++ b/server/config/runtime_access.go @@ -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 diff --git a/server/config/runtime_access_test.go b/server/config/runtime_access_test.go index 3c53313..8cff24b 100644 --- a/server/config/runtime_access_test.go +++ b/server/config/runtime_access_test.go @@ -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) diff --git a/server/dto/parsemail/dkim.go b/server/dto/parsemail/dkim.go index 8380474..8d82700 100644 --- a/server/dto/parsemail/dkim.go +++ b/server/dto/parsemail/dkim.go @@ -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, } diff --git a/server/listen/pop3_server/pop3server.go b/server/listen/pop3_server/pop3server.go index 3ca34c3..849777d 100644 --- a/server/listen/pop3_server/pop3server.go +++ b/server/listen/pop3_server/pop3server.go @@ -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) diff --git a/server/listen/smtp_server/read_content.go b/server/listen/smtp_server/read_content.go index 5955268..11d0a39 100644 --- a/server/listen/smtp_server/read_content.go +++ b/server/listen/smtp_server/read_content.go @@ -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) } diff --git a/server/listen/smtp_server/smtp.go b/server/listen/smtp_server/smtp.go index a1b1e65..04be260 100644 --- a/server/listen/smtp_server/smtp.go +++ b/server/listen/smtp_server/smtp.go @@ -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 diff --git a/server/utils/send/send.go b/server/utils/send/send.go index 5146764..864272d 100644 --- a/server/utils/send/send.go +++ b/server/utils/send/send.go @@ -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 { diff --git a/server/utils/smtp/smtp.go b/server/utils/smtp/smtp.go index f5506b8..432f00f 100644 --- a/server/utils/smtp/smtp.go +++ b/server/utils/smtp/smtp.go @@ -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}