diff --git a/default.yaml b/default.yaml index 44b170e4..e934ef26 100644 --- a/default.yaml +++ b/default.yaml @@ -418,8 +418,13 @@ accounts: # username: "admin" # password: "hunter2" # implicit-tls: false # TLS from the first byte, typically on port 465 - blacklist-regexes: - # - ".*@mailinator.com" + # addresses that are not accepted for registration: + address-blacklist: + # - "*@mailinator.com" + address-blacklist-syntax: "glob" # change to "regex" for regular expressions + # file of newline-delimited address blacklist entries in the above syntax; + # supersedes address-blacklist if set: + # address-blacklist-file: "/path/to/address-blacklist-file" timeout: 60s # email-based password reset: password-reset: diff --git a/irc/email/email.go b/irc/email/email.go index 4b0834b7..07c7bc52 100644 --- a/irc/email/email.go +++ b/irc/email/email.go @@ -4,10 +4,13 @@ package email import ( + "bufio" "bytes" "errors" "fmt" + "io" "net" + "os" "regexp" "strings" "time" @@ -23,6 +26,38 @@ var ( ErrNoMXRecord = errors.New("Couldn't resolve MX record") ) +type BlacklistSyntax uint + +const ( + BlacklistSyntaxGlob BlacklistSyntax = iota + BlacklistSyntaxRegexp +) + +func blacklistSyntaxFromString(status string) (BlacklistSyntax, error) { + switch strings.ToLower(status) { + case "glob", "": + return BlacklistSyntaxGlob, nil + case "re", "regex", "regexp": + return BlacklistSyntaxRegexp, nil + default: + return BlacklistSyntaxRegexp, fmt.Errorf("Unknown blacklist syntax type `%s`", status) + } +} + +func (bs *BlacklistSyntax) UnmarshalYAML(unmarshal func(interface{}) error) error { + var orig string + var err error + if err = unmarshal(&orig); err != nil { + return err + } + if result, err := blacklistSyntaxFromString(orig); err == nil { + *bs = result + return nil + } else { + return err + } +} + type MTAConfig struct { Server string Port int @@ -35,24 +70,64 @@ type MailtoConfig struct { // legacy config format assumed the use of an MTA/smarthost, // so server, port, etc. appear directly at top level // XXX: see https://github.com/go-yaml/yaml/issues/63 - MTAConfig `yaml:",inline"` - Enabled bool - Sender string - HeloDomain string `yaml:"helo-domain"` - RequireTLS bool `yaml:"require-tls"` - VerifyMessageSubject string `yaml:"verify-message-subject"` - DKIM DKIMConfig - MTAReal MTAConfig `yaml:"mta"` - BlacklistRegexes []string `yaml:"blacklist-regexes"` - blacklistRegexes []*regexp.Regexp - Timeout time.Duration - PasswordReset struct { + MTAConfig `yaml:",inline"` + Enabled bool + Sender string + HeloDomain string `yaml:"helo-domain"` + RequireTLS bool `yaml:"require-tls"` + VerifyMessageSubject string `yaml:"verify-message-subject"` + DKIM DKIMConfig + MTAReal MTAConfig `yaml:"mta"` + AddressBlacklist []string `yaml:"address-blacklist"` + AddressBlacklistSyntax BlacklistSyntax `yaml:"address-blacklist-syntax"` + AddressBlacklistFile string `yaml:"address-blacklist-file"` + blacklistRegexes []*regexp.Regexp + Timeout time.Duration + PasswordReset struct { Enabled bool Cooldown custime.Duration Timeout custime.Duration } `yaml:"password-reset"` } +func (config *MailtoConfig) compileBlacklistEntry(source string) (re *regexp.Regexp, err error) { + if config.AddressBlacklistSyntax == BlacklistSyntaxGlob { + return utils.CompileGlob(source, false) + } else { + return regexp.Compile(fmt.Sprintf("^%s$", source)) + } +} + +func (config *MailtoConfig) processBlacklistFile(filename string) (result []*regexp.Regexp, err error) { + f, err := os.Open(filename) + if err != nil { + return + } + defer f.Close() + reader := bufio.NewReader(f) + lineNo := 0 + for { + line, err := reader.ReadString('\n') + lineNo++ + line = strings.TrimSpace(line) + if line != "" && line[0] != '#' { + if compiled, compileErr := config.compileBlacklistEntry(line); compileErr == nil { + result = append(result, compiled) + } else { + return result, fmt.Errorf("Failed to compile line %d of blacklist-regex-file `%s`: %w", lineNo, line, compileErr) + } + } + switch err { + case io.EOF: + return result, nil + case nil: + continue + default: + return result, err + } + } +} + func (config *MailtoConfig) Postprocess(heloDomain string) (err error) { if config.Sender == "" { return errors.New("Invalid mailto sender address") @@ -68,12 +143,20 @@ func (config *MailtoConfig) Postprocess(heloDomain string) (err error) { config.HeloDomain = heloDomain } - for _, reg := range config.BlacklistRegexes { - compiled, err := regexp.Compile(fmt.Sprintf("^%s$", reg)) + if config.AddressBlacklistFile != "" { + config.blacklistRegexes, err = config.processBlacklistFile(config.AddressBlacklistFile) if err != nil { return err } - config.blacklistRegexes = append(config.blacklistRegexes, compiled) + } else if len(config.AddressBlacklist) != 0 { + config.blacklistRegexes = make([]*regexp.Regexp, 0, len(config.AddressBlacklist)) + for _, reg := range config.AddressBlacklist { + compiled, err := config.compileBlacklistEntry(reg) + if err != nil { + return err + } + config.blacklistRegexes = append(config.blacklistRegexes, compiled) + } } if config.MTAConfig.Server != "" { @@ -118,8 +201,9 @@ func ComposeMail(config MailtoConfig, recipient, subject string) (message bytes. } func SendMail(config MailtoConfig, recipient string, msg []byte) (err error) { + recipientLower := strings.ToLower(recipient) for _, reg := range config.blacklistRegexes { - if reg.MatchString(recipient) { + if reg.MatchString(recipientLower) { return ErrBlacklistedAddress } } diff --git a/traditional.yaml b/traditional.yaml index 811a1e7d..7ffbdd84 100644 --- a/traditional.yaml +++ b/traditional.yaml @@ -391,8 +391,13 @@ accounts: # username: "admin" # password: "hunter2" # implicit-tls: false # TLS from the first byte, typically on port 465 - blacklist-regexes: - # - ".*@mailinator.com" + # addresses that are not accepted for registration: + address-blacklist: + # - "*@mailinator.com" + address-blacklist-syntax: "glob" # change to "regex" for regular expressions + # file of newline-delimited address blacklist entries in the above syntax; + # supersedes address-blacklist if set: + # address-blacklist-file: "/path/to/address-blacklist-file" timeout: 60s # email-based password reset: password-reset: