consider Return-Path in SPF checks

This commit is contained in:
Aine 2024-07-24 12:45:39 +03:00
parent a6f52fe1ee
commit 952ff99fc7
No known key found for this signature in database
GPG key ID: 34969C908CCA2804
19 changed files with 567 additions and 308 deletions

2
go.mod
View file

@ -27,7 +27,7 @@ require (
gitlab.com/etke.cc/go/mxidwc v1.0.0
gitlab.com/etke.cc/go/psd v1.1.2
gitlab.com/etke.cc/go/secgen v1.2.0
gitlab.com/etke.cc/go/validator v1.0.7
gitlab.com/etke.cc/go/validator/v2 v2.2.0
gitlab.com/etke.cc/linkpearl v0.0.0-20240716084747-f2a547f02d54
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
maunium.net/go/mautrix v0.19.0

4
go.sum
View file

@ -116,8 +116,8 @@ gitlab.com/etke.cc/go/secgen v1.2.0 h1:qpV7rUn5Rs6eWxAmbGG/idPCOgsN4HggGmSZ+1R/L
gitlab.com/etke.cc/go/secgen v1.2.0/go.mod h1:v5L07AIXtNpC/miYiK0TMIn+ZKbiYrTRiXTw6qTL6pw=
gitlab.com/etke.cc/go/trysmtp v1.1.3 h1:e2EHond77onMaecqCg6mWumffTSEf+ycgj88nbeefDI=
gitlab.com/etke.cc/go/trysmtp v1.1.3/go.mod h1:lOO7tTdAE0a3ETV3wN3GJ7I1Tqewu7YTpPWaOmTteV0=
gitlab.com/etke.cc/go/validator v1.0.7 h1:4BGDTa9x68vJhbyn7m8W2yX+2Nb5im9+JLRrgoLUlF4=
gitlab.com/etke.cc/go/validator v1.0.7/go.mod h1:Id0SxRj0J3IPhiKlj0w1plxVLZfHlkwipn7HfRZsDts=
gitlab.com/etke.cc/go/validator/v2 v2.2.0 h1:XgB8XikOc3WzzFyimpZOt3p+kbHny98sGkXtp2tSwzQ=
gitlab.com/etke.cc/go/validator/v2 v2.2.0/go.mod h1:NBY0v4ZqR1GQpuojf369EW2oS/4Ll1uIs9YTgijRbaY=
gitlab.com/etke.cc/linkpearl v0.0.0-20240716084747-f2a547f02d54 h1:+ihVGkVBBWlRyugJ3dvmA1rwqC9jFXrUZSslkYFN0Gw=
gitlab.com/etke.cc/linkpearl v0.0.0-20240716084747-f2a547f02d54/go.mod h1:R/uxbOoajQVxm0qCbWYfox6B9uqnM5R0/KMJcLSr6zM=
go.mau.fi/util v0.6.0 h1:W6SyB3Bm/GjenQ5iq8Z8WWdN85Gy2xS6L0wmnR7SVjg=

View file

@ -2,23 +2,8 @@ package smtp
import (
"strings"
"github.com/rs/zerolog"
)
// validatorLoggerWrapper is a wrapper around zerolog.Logger to implement validator.Logger interface
type validatorLoggerWrapper struct {
log *zerolog.Logger
}
func (l validatorLoggerWrapper) Info(msg string, args ...any) {
l.log.Info().Msgf(msg, args...)
}
func (l validatorLoggerWrapper) Error(msg string, args ...any) {
l.log.Error().Msgf(msg, args...)
}
// loggerWrapper is a wrapper around any logger to implement smtp.Logger interface
type loggerWrapper struct {
log func(string, ...any)

View file

@ -15,7 +15,7 @@ import (
"github.com/emersion/go-smtp"
"github.com/jhillyerd/enmime"
"github.com/rs/zerolog"
"gitlab.com/etke.cc/go/validator"
"gitlab.com/etke.cc/go/validator/v2"
"gitlab.com/etke.cc/postmoogle/email"
"gitlab.com/etke.cc/postmoogle/utils"
"maunium.net/go/mautrix/id"
@ -171,7 +171,7 @@ func (s *session) incomingData(r io.Reader) error {
addr := s.getAddr(envelope)
reader.Seek(0, io.SeekStart) //nolint:errcheck // becase we're sure that's ok
validations := s.bot.GetIFOptions(s.ctx, s.roomID)
if !validateIncoming(s.from, s.tos[0], addr, s.log, validations) {
if !validateIncoming(s.from, envelope.GetHeader("Return-Path"), addr, s.log, validations) {
s.bot.BanAuth(s.ctx, addr)
return ErrBanned
}
@ -288,7 +288,7 @@ func (s *session) getAddr(envelope *enmime.Envelope) net.Addr {
return realAddr
}
func validateIncoming(from, to string, senderAddr net.Addr, log *zerolog.Logger, options email.IncomingFilteringOptions) bool {
func validateIncoming(from, returnPath string, senderAddr net.Addr, log *zerolog.Logger, options email.IncomingFilteringOptions) bool {
var sender net.IP
switch netaddr := senderAddr.(type) {
case *net.TCPAddr:
@ -298,13 +298,18 @@ func validateIncoming(from, to string, senderAddr net.Addr, log *zerolog.Logger,
sender = net.ParseIP(host)
}
enforce := validator.Enforce{
Email: true,
vcfg := &validator.Config{
Log: log.Warn().Msgf,
Email: validator.Email{
Enforce: true,
Spamlist: options.Spamlist(),
MX: options.SpamcheckMX(),
SPF: options.SpamcheckSPF(),
SMTP: options.SpamcheckSMTP(),
From: from,
},
}
v := validator.New(options.Spamlist(), enforce, to, &validatorLoggerWrapper{log: log})
v := validator.New(vcfg)
return v.Email(from, sender)
return v.Email(from, returnPath, sender)
}

View file

@ -1,11 +0,0 @@
lint:
image: registry.gitlab.com/etke.cc/base
script:
- golangci-lint run ./...
unit:
image: registry.gitlab.com/etke.cc/base
script:
- go test -coverprofile=cover.out ./...
- go tool cover -func=cover.out
- rm -f cover.out

View file

@ -1,142 +0,0 @@
package validator
import (
"fmt"
"net"
"net/mail"
"strings"
"time"
"blitiri.com.ar/go/spf"
"gitlab.com/etke.cc/go/trysmtp"
"golang.org/x/net/context"
)
// Email checks if email is valid
func (v *V) Email(email string, optionalSenderIP ...net.IP) bool {
// edge case: email may be optional
if email == "" {
return !v.enforce.Email
}
address, err := mail.ParseAddress(email)
if err != nil {
v.log.Info("email %s invalid, reason: %v", email, err)
return false
}
email = address.Address
return v.emailChecks(email, optionalSenderIP...)
}
func (v *V) emailChecks(email string, optionalSenderIP ...net.IP) bool {
maxChecks := 4
var senderIP net.IP
if len(optionalSenderIP) > 0 {
senderIP = optionalSenderIP[0]
}
errchan := make(chan error, maxChecks)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
go v.emailSpamlist(ctx, email, errchan)
go v.emailNoMX(ctx, email, errchan)
go v.emailNoSPF(ctx, email, senderIP, errchan)
go v.emailNoSMTP(ctx, email, errchan)
var checks int
for {
checks++
err := <-errchan
if err != nil {
v.log.Info("email %q is invalid, reason: %v", email, err)
return false
}
if checks >= maxChecks {
return true
}
}
}
func (v *V) emailSpamlist(ctx context.Context, email string, errchan chan error) {
select {
case <-ctx.Done():
return
default:
emailb := []byte(email)
for _, spamregex := range v.spamlist {
if spamregex.Match(emailb) {
errchan <- fmt.Errorf("spamlist")
return
}
}
errchan <- nil
}
}
func (v *V) emailNoMX(ctx context.Context, email string, errchan chan error) {
select {
case <-ctx.Done():
return
default:
if !v.enforce.MX {
errchan <- nil
return
}
at := strings.LastIndex(email, "@")
domain := email[at+1:]
if !v.MX(domain) {
v.log.Info("email %s domain %s invalid, reason: no MX", email, domain)
errchan <- fmt.Errorf("no MX")
return
}
errchan <- nil
}
}
func (v *V) emailNoSMTP(ctx context.Context, email string, errchan chan error) {
select {
case <-ctx.Done():
return
default:
if !v.enforce.SMTP {
errchan <- nil
return
}
client, err := trysmtp.Connect(v.from, email)
if err != nil {
if strings.HasPrefix(err.Error(), "45") {
v.log.Info("email %s may be invalid, reason: SMTP check (%v)", email, err)
errchan <- nil
return
}
v.log.Info("email %s invalid, reason: SMTP check (%v)", email, err)
errchan <- fmt.Errorf("SMTP")
return
}
client.Close()
errchan <- nil
}
}
func (v *V) emailNoSPF(ctx context.Context, email string, senderIP net.IP, errchan chan error) {
select {
case <-ctx.Done():
return
default:
if !v.enforce.SPF {
errchan <- nil
return
}
result, _ := spf.CheckHostWithSender(senderIP, "", email, spf.WithTraceFunc(v.log.Info)) //nolint:errcheck // not a error
if result == spf.Fail {
errchan <- fmt.Errorf("SPF")
return
}
errchan <- nil
}
}

View file

@ -1,45 +0,0 @@
package validator
import "net"
// A checks if host has at least one A record
func (v *V) A(host string) bool {
if host == "" {
return false
}
ips, err := net.LookupIP(host)
if err != nil {
v.log.Error("cannot get A records of %s: %v", host, err)
return false
}
return len(ips) > 0
}
// CNAME checks if host has at least one CNAME record
func (v *V) CNAME(host string) bool {
if host == "" {
return false
}
cname, err := net.LookupCNAME(host)
if err != nil {
v.log.Error("cannot get CNAME records of %s: %v", host, err)
return false
}
return cname != ""
}
// MX checks if host has at least one MX record
func (v *V) MX(host string) bool {
if host == "" {
return false
}
mxs, err := net.LookupMX(host)
if err != nil {
v.log.Error("cannot get MX records of %s: %v", host, err)
return false
}
return len(mxs) > 0
}

View file

@ -0,0 +1,11 @@
lint:
stage: test
image: registry.gitlab.com/etke.cc/base/build
script:
- just lint
unit:
stage: test
image: registry.gitlab.com/etke.cc/base/build
script:
- just test

142
vendor/gitlab.com/etke.cc/go/validator/v2/.golangci.yml generated vendored Normal file
View file

@ -0,0 +1,142 @@
run:
concurrency: 4
timeout: 30m
issues-exit-code: 1
tests: true
build-tags: []
skip-dirs-use-default: true
skip-files: []
modules-download-mode: readonly
output:
formats:
- format: colored-line-number
print-issued-lines: true
print-linter-name: true
sort-results: true
linters-settings:
decorder:
dec-order:
- const
- var
- type
- func
dogsled:
max-blank-identifiers: 3
errcheck:
check-type-assertions: true
check-blank: true
errchkjson:
report-no-exported: true
exhaustive:
check:
- switch
- map
default-signifies-exhaustive: true
gocognit:
min-complexity: 15
nestif:
min-complexity: 5
gocritic:
enabled-tags:
- diagnostic
- style
- performance
gofmt:
simplify: true
rewrite-rules:
- pattern: 'interface{}'
replacement: 'any'
- pattern: 'a[b:len(a)]'
replacement: 'a[b:]'
gofumpt:
extra-rules: true
grouper:
const-require-single-const: true
import-require-single-import: true
var-require-single-var: true
misspell:
locale: US
usestdlibvars:
time-month: true
time-layout: true
crypto-hash: true
default-rpc-path: true
sql-isolation-level: true
tls-signature-scheme: true
constant-kind: true
unparam:
check-exported: true
linters:
disable-all: false
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- containedctx
- decorder
- dogsled
- dupl
- dupword
- durationcheck
- errcheck
- errchkjson
- errname
- errorlint
- exhaustive
- exportloopref
- forcetypeassert
- gocognit
- gocritic
- gocyclo
- gofmt
- gofumpt
- goimports
- gosec
- gosimple
- gosmopolitan
- govet
- ineffassign
- makezero
- mirror
- misspell
- nestif
- nolintlint
- prealloc
- predeclared
- revive
- sqlclosecheck
- staticcheck
- unconvert
- unparam
- unused
- usestdlibvars
- wastedassign
fast: false
issues:
exclude-dirs:
- mocks
exclude-rules:
- path: _test\.go
linters:
- gocyclo
- gocognit
- errcheck
- dupl
- gosec
- linters:
- staticcheck
text: "SA9003:"
- linters:
- lll
source: "^//go:generate "
- linters:
- revive
text: "returns unexported type"
max-issues-per-linter: 0
max-same-issues: 0
new: false

35
vendor/gitlab.com/etke.cc/go/validator/v2/config.go generated vendored Normal file
View file

@ -0,0 +1,35 @@
package validator
import "regexp"
// Config of the validator
type Config struct {
Email Email // Email config
Domain Domain // Domain config
Log func(msg string, args ...any) // Log func
}
// Email checks configuration
type Email struct {
// Enforce enforces email check and rejects empty emails
Enforce bool
// SMTP enforces SMTP check (email actually exists on mail server) and rejects non-existing emails
SMTP bool
// SPF enforces SPF record check (sender allowed to use that email and send emails) and rejects unathorized emails
SPF bool
// MX enforces MX records check on email's mail server
MX bool
// From is a valid email address that will be used for SMTP checks
From string
// Spamlist is a list of spam emails with wildcards
Spamlist []string
spamlist []*regexp.Regexp
}
// Domain checks configuration
type Domain struct {
// Enforce enforces domain check and rejects empty domains
Enforce bool
// PrivateSuffixes considers subdomains with the following suffixes as domains
PrivateSuffixes []string
}

View file

@ -9,12 +9,11 @@ import (
// based on W3C email regex, ref: https://www.w3.org/TR/2016/REC-html51-20161101/sec-forms.html#email-state-typeemail
var domainRegex = regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$`)
var privateSuffix = []string{".etke.host", ".onmatrix.chat"}
// Domain checks if domain is valid
func (v *V) Domain(domain string) bool {
if domain == "" {
return !v.enforce.Domain
return !v.cfg.Domain.Enforce
}
if !v.DomainString(domain) {
@ -27,12 +26,12 @@ func (v *V) Domain(domain string) bool {
// DomainString checks if domain string / value is valid using string checks like length and regexp
func (v *V) DomainString(domain string) bool {
if len(domain) < 4 || len(domain) > 77 {
v.log.Info("domain %s invalid, reason: length", domain)
v.cfg.Log("domain %s invalid, reason: length", domain)
return false
}
if !domainRegex.MatchString(domain) {
v.log.Info("domain %s invalid, reason: regexp", domain)
v.cfg.Log("domain %s invalid, reason: regexp", domain)
return false
}
@ -61,7 +60,7 @@ func (v *V) GetBase(domain string) string {
// hasSuffix checks if domain has a suffix from public suffix list or from predefined suffix list
func (v *V) hasSuffix(domain string) bool {
for _, suffix := range privateSuffix {
for _, suffix := range v.cfg.Domain.PrivateSuffixes {
if strings.HasSuffix(domain, suffix) {
return true
}

197
vendor/gitlab.com/etke.cc/go/validator/v2/emails.go generated vendored Normal file
View file

@ -0,0 +1,197 @@
package validator
import (
"fmt"
"net"
"net/mail"
"strings"
"time"
"blitiri.com.ar/go/spf"
"gitlab.com/etke.cc/go/trysmtp"
"golang.org/x/net/context"
)
var (
ErrSpamlist = fmt.Errorf("spamlist")
ErrNoMX = fmt.Errorf("no MX")
ErrSMTP = fmt.Errorf("SMTP")
ErrSPF = fmt.Errorf("SPF")
)
// Email checks if email is valid
// returnPath and optionalSenderIP are optional fields
func (v *V) Email(email, returnPath string, optionalSenderIP ...net.IP) bool {
// edge case: email may be optional
if email == "" {
return !v.cfg.Email.Enforce
}
address, err := mail.ParseAddress(email)
if err != nil {
v.cfg.Log("email %s invalid, reason: %v", email, err)
return false
}
if returnPath != "" {
rpAddress, err := mail.ParseAddress(returnPath)
if err != nil {
v.cfg.Log("return path %s invalid, reason: %v", returnPath, err)
}
if rpAddress != nil {
returnPath = rpAddress.Address
}
}
email = address.Address
return v.emailChecks(email, returnPath, optionalSenderIP...)
}
func (v *V) emailChecks(email, returnPath string, optionalSenderIP ...net.IP) bool {
maxChecks := 4
var senderIP net.IP
if len(optionalSenderIP) > 0 {
senderIP = optionalSenderIP[0]
}
errchan := make(chan error, maxChecks)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
go v.emailSpamlist(ctx, email, errchan)
go v.emailNoMX(ctx, email, errchan)
go v.emailNoSPF(ctx, email, returnPath, senderIP, errchan)
go v.emailNoSMTP(ctx, email, errchan)
var checks int
for {
checks++
err := <-errchan
if err != nil {
v.cfg.Log("email %q is invalid, reason: %v", email, err)
return false
}
if checks >= maxChecks {
return true
}
}
}
func (v *V) emailSpamlist(ctx context.Context, email string, errchan chan error) {
select {
case <-ctx.Done():
return
default:
emailb := []byte(email)
for _, spamregex := range v.cfg.Email.spamlist {
if spamregex.Match(emailb) {
errchan <- ErrSpamlist
return
}
}
errchan <- nil
}
}
func (v *V) emailNoMX(ctx context.Context, email string, errchan chan error) {
select {
case <-ctx.Done():
return
default:
if !v.cfg.Email.MX {
errchan <- nil
return
}
at := strings.LastIndex(email, "@")
domain := email[at+1:]
if !v.MX(domain) {
v.cfg.Log("email %s domain %s invalid, reason: no MX", email, domain)
errchan <- ErrNoMX
return
}
errchan <- nil
}
}
func (v *V) emailNoSMTP(ctx context.Context, email string, errchan chan error) {
select {
case <-ctx.Done():
return
default:
if !v.cfg.Email.SMTP {
errchan <- nil
return
}
client, err := trysmtp.Connect(v.cfg.Email.From, email)
if err != nil {
if strings.HasPrefix(err.Error(), "45") {
v.cfg.Log("email %s may be invalid, reason: SMTP check (%v)", email, err)
errchan <- nil
return
}
v.cfg.Log("email %s invalid, reason: SMTP check (%v)", email, err)
errchan <- ErrSMTP
return
}
client.Close()
errchan <- nil
}
}
// shouldValidateReturnPath checks if returnPath should be validated in SPF check
func (v *V) shouldValidateReturnPath(email, returnPath string) bool {
if returnPath == "" {
return false
}
if email == returnPath {
return false
}
var emailDomain, returnPathDomain string
emailParts := strings.Split(email, "@")
if len(emailParts) < 2 {
return false
}
emailDomain = v.GetBase(emailParts[1])
if returnPath != "" {
returnPathParts := strings.Split(returnPath, "@")
if len(returnPathParts) < 2 {
return false
}
returnPathDomain = v.GetBase(returnPathParts[1])
}
return emailDomain == returnPathDomain
}
func (v *V) emailNoSPF(ctx context.Context, email, returnPath string, senderIP net.IP, errchan chan error) {
select {
case <-ctx.Done():
return
default:
if !v.cfg.Email.SPF {
errchan <- nil
return
}
opts := []spf.Option{
spf.WithContext(ctx),
spf.WithTraceFunc(v.cfg.Log),
}
resultEmail, _ := spf.CheckHostWithSender(senderIP, "", email, opts...) //nolint:errcheck // not a error
if resultEmail == spf.Fail {
if v.shouldValidateReturnPath(email, returnPath) {
resultReturnPath, _ := spf.CheckHostWithSender(senderIP, "", returnPath, opts...) //nolint:errcheck // not a error
if resultReturnPath == spf.Fail {
errchan <- ErrSPF
return
}
}
errchan <- ErrSPF
return
}
errchan <- nil
}
}

22
vendor/gitlab.com/etke.cc/go/validator/v2/justfile generated vendored Normal file
View file

@ -0,0 +1,22 @@
# show help by default
default:
@just --list --justfile {{ justfile() }}
# update go deps
update *flags:
go get {{flags}} .
go mod tidy
# run linter
lint:
golangci-lint run ./...
# automatically fix liter issues
lintfix:
golangci-lint run --fix ./...
# run unit tests
test:
@go test -cover -coverprofile=cover.out -coverpkg=./... -covermode=set ./...
@go tool cover -func=cover.out
-@rm -f cover.out

80
vendor/gitlab.com/etke.cc/go/validator/v2/lookups.go generated vendored Normal file
View file

@ -0,0 +1,80 @@
package validator
import (
"net"
"strings"
)
// A checks if host has at least one A record
func (v *V) A(host string) bool {
if host == "" {
return false
}
ips, err := net.LookupIP(host)
if err != nil {
v.cfg.Log("cannot get A records of %s: %v", host, err)
return false
}
return len(ips) > 0
}
// CNAME checks if host has at least one CNAME record
func (v *V) CNAME(host string) bool {
if host == "" {
return false
}
cname, err := net.LookupCNAME(host)
if err != nil {
v.cfg.Log("cannot get CNAME records of %s: %v", host, err)
return false
}
return cname != ""
}
// MX checks if host has at least one MX record
func (v *V) MX(host string) bool {
if host == "" {
return false
}
mxs, err := net.LookupMX(host)
if err != nil {
v.cfg.Log("cannot get MX records of %s: %v", host, err)
return false
}
return len(mxs) > 0
}
// NS checks if host has at least one NS record,
// and optionally checks if the NS record contains the given string
func (v *V) NS(host string, contains ...string) bool {
if host == "" {
return false
}
nss, err := net.LookupNS(host)
if err != nil {
v.cfg.Log("cannot get NS records of %s: %v", host, err)
return false
}
if len(nss) == 0 {
v.cfg.Log("%s doesn't have NS records", host)
return false
}
if len(contains) == 0 {
return true
}
if len(contains) > 0 {
for _, ns := range nss {
for _, c := range contains {
if strings.Contains(ns.Host, c) {
return true
}
}
}
}
return false
}

56
vendor/gitlab.com/etke.cc/go/validator/v2/validator.go generated vendored Normal file
View file

@ -0,0 +1,56 @@
package validator
import (
"fmt"
"regexp"
"strings"
)
// V is a validator implementation
type V struct {
cfg *Config
}
var defaultLogfunc = func(msg string, args ...any) {
fmt.Printf(msg+"\n", args...)
}
// New validator
func New(cfg *Config) *V {
if cfg.Log == nil {
cfg.Log = defaultLogfunc
}
spamregexes, err := parseSpamlist(cfg.Email.Spamlist)
if err != nil {
cfg.Log("cannot parse spamlist: %v", err)
}
cfg.Email.spamlist = spamregexes
return &V{cfg}
}
func parseSpamlist(patterns []string) ([]*regexp.Regexp, error) {
regexes := []*regexp.Regexp{}
for _, pattern := range patterns {
rule, err := regexp.Compile("^" + parsePattern(pattern) + "$")
if err != nil {
return regexes, err
}
regexes = append(regexes, rule)
}
return regexes, nil
}
func parsePattern(pattern string) string {
var regexpattern strings.Builder
for _, runeItem := range pattern {
if runeItem == '*' {
regexpattern.WriteString("(.*)")
continue
}
regexpattern.WriteString(regexp.QuoteMeta(string(runeItem)))
}
return regexpattern.String()
}

View file

@ -1,75 +0,0 @@
package validator
import (
"regexp"
"strings"
)
// V is a validator implementation
type V struct {
spamlist []*regexp.Regexp
enforce Enforce
from string
log Logger
}
// Enforce checks
type Enforce struct {
// Email enforces email check and rejects empty emails
Email bool
// Domain enforces domain check and rejects empty domains
Domain bool
// SMTP enforces SMTP check (email actually exists on mail server) and rejects non-existing emails
SMTP bool
// SPF enforces SPF record check (sender allowed to use that email and send emails) and rejects unathorized emails
SPF bool
// MX enforces MX records check on email's mail server
MX bool
}
type Logger interface {
Info(string, ...interface{})
Error(string, ...interface{})
}
// New Validator, accepts spamlist with wildcards
func New(spamlist []string, enforce Enforce, smtpFrom string, log Logger) *V {
spamregexes, err := parseSpamlist(spamlist)
if err != nil {
log.Error("cannot parse spamlist: %v", err)
}
return &V{
spamlist: spamregexes,
enforce: enforce,
from: smtpFrom,
log: log,
}
}
func parseSpamlist(patterns []string) ([]*regexp.Regexp, error) {
regexes := []*regexp.Regexp{}
for _, pattern := range patterns {
rule, err := regexp.Compile("^" + parsePattern(pattern) + "$")
if err != nil {
return regexes, err
}
regexes = append(regexes, rule)
}
return regexes, nil
}
func parsePattern(pattern string) string {
var regexpattern strings.Builder
for _, runeItem := range pattern {
if runeItem == '*' {
regexpattern.WriteString("(.*)")
continue
}
regexpattern.WriteString(regexp.QuoteMeta(string(runeItem)))
}
return regexpattern.String()
}

4
vendor/modules.txt vendored
View file

@ -159,9 +159,9 @@ gitlab.com/etke.cc/go/secgen
# gitlab.com/etke.cc/go/trysmtp v1.1.3
## explicit; go 1.17
gitlab.com/etke.cc/go/trysmtp
# gitlab.com/etke.cc/go/validator v1.0.7
# gitlab.com/etke.cc/go/validator/v2 v2.2.0
## explicit; go 1.18
gitlab.com/etke.cc/go/validator
gitlab.com/etke.cc/go/validator/v2
# gitlab.com/etke.cc/linkpearl v0.0.0-20240716084747-f2a547f02d54
## explicit; go 1.21
gitlab.com/etke.cc/linkpearl