From 400c181a0baa4d33e2b8814e64dc697b333517bf Mon Sep 17 00:00:00 2001 From: Daniel Jagszent Date: Mon, 3 Apr 2023 22:46:35 +0200 Subject: [PATCH] feat: change logging, add MySQL email forward lookup --- Makefile | 2 +- README.md | 15 +++- cache.go | 2 +- cache_test.go | 2 +- cmd/srs-milter/config.go | 18 ++-- cmd/srs-milter/log.go | 24 +++++ cmd/srs-milter/main.go | 102 ++++++++++++++------- config.go | 78 +++++++++++++++- config_test.go | 6 +- filter.go | 142 +++++++++--------------------- filter_test.go | 33 ++++++- go.mod | 9 ++ go.sum | 20 ++++- integration/go.mod | 8 ++ integration/go.sum | 17 +++- integration/tests/forward/test.go | 10 +-- integration/tests/reverse/test.go | 10 +-- packaging/srs-milter.adoc | 4 +- packaging/srs-milter.service | 2 +- packaging/srs-milter.yml | 10 ++- srs.go | 46 ++++++++++ 21 files changed, 386 insertions(+), 174 deletions(-) create mode 100644 cmd/srs-milter/log.go create mode 100644 srs.go diff --git a/Makefile b/Makefile index ae8ed8a..5edd5b2 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ GO_MILTER_INTEGRATION_DIR := $(shell cd integration && go list -f '{{.Dir}}' github.com/d--j/go-milter/integration) integration: - docker build -q --progress=plain -t go-milter-integration "$(GO_MILTER_INTEGRATION_DIR)/docker" && \ + docker build -q -t go-milter-integration "$(GO_MILTER_INTEGRATION_DIR)/docker" && \ docker run --rm --hostname=mx.example.com -w /usr/src/root/integration -v $(PWD):/usr/src/root go-milter-integration \ go run github.com/d--j/go-milter/integration/runner -filter '.*' -mtaFilter '.*' ./tests diff --git a/README.md b/README.md index 7d6d89b..159b840 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,15 @@ localDomains: - 'example.com' ``` +You can also specify an optional MySQL query for email forwarding lookups: + +```yaml +# Optional: You can specify a MySQL connection/query to lookup mail forwarding replacements +dbDriver: 'mysql' +dbDSN: 'user:password@tcp(host:port)/dbname' +dbForwardQuery: "SELECT destination from mail_forwarding WHERE source = ? AND active = 'y' AND server_id = 1;" +``` + If your machine does not have public IP addresses (NATed/firewalled) or you deployed the milter on another machine, you need to specify the IPs that we check against the SPF records. These IPs should be the IPs that get used for outgoing SMTP connections. @@ -75,9 +84,9 @@ You can use command line parameters to change this default: ``` $ srs-milter -help Usage of ./srs-milter: - -addr string + -milterAddr string Bind to address/port or unix domain socket path (default "127.0.0.1:10382") - -proto string + -milterProto unix or tcp Protocol family (unix or tcp) (default "tcp") -forward email email to do forward SRS lookup for. If specified the milter will not be started. @@ -98,6 +107,8 @@ Add this to `/etc/postfix/main.cf` smtpd_milters = inet:127.0.0.1:10382 non_smtpd_milters = inet:127.0.0.1:10382 milter_protocol = 6 +# so that SRS bounces can be accepted - otherwise Postfix will reject the bounces +mydestination = $myhostname, srs.example.com ``` If you already have milters defined (e.g. Rspamd), diff --git a/cache.go b/cache.go index d5496c7..f644070 100644 --- a/cache.go +++ b/cache.go @@ -1,4 +1,4 @@ -package srsMilter +package srsmilter import ( "time" diff --git a/cache_test.go b/cache_test.go index f7ead93..6eb3d7a 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1,4 +1,4 @@ -package srsMilter +package srsmilter import ( "net" diff --git a/cmd/srs-milter/config.go b/cmd/srs-milter/config.go index 522eb52..8124bda 100644 --- a/cmd/srs-milter/config.go +++ b/cmd/srs-milter/config.go @@ -2,12 +2,12 @@ package main import ( "errors" - "log" "net" "reflect" "strings" "github.com/d--j/srs-milter" + _ "github.com/go-sql-driver/mysql" "github.com/mitchellh/mapstructure" "github.com/spf13/viper" "golang.org/x/net/idna" @@ -52,19 +52,17 @@ func determineExternalIPs() ([]net.IP, error) { func ipsToString(ips []net.IP) string { var s strings.Builder - s.WriteString(`"`) for i, ip := range ips { if i > 0 { s.WriteString(",") } s.WriteString(ip.String()) } - s.WriteString(`"`) return s.String() } -func loadViperConfig() (*srsMilter.Configuration, error) { - var conf srsMilter.Configuration +func loadViperConfig() (*srsmilter.Configuration, error) { + var conf srsmilter.Configuration err := viper.Unmarshal(&conf, viper.DecodeHook(mapstructure.StringToIPHookFunc()), viper.DecodeHook(func( f reflect.Type, t reflect.Type, @@ -72,12 +70,12 @@ func loadViperConfig() (*srsMilter.Configuration, error) { if f.Kind() != reflect.String { return data, nil } - if t != reflect.TypeOf(srsMilter.Domain("")) { + if t != reflect.TypeOf(srsmilter.Domain("")) { return data, nil } asciiDomain, err := idna.Lookup.ToASCII(data.(string)) - return srsMilter.Domain(asciiDomain), err + return srsmilter.Domain(asciiDomain), err }), viper.DecodeHook(func( f reflect.Type, t reflect.Type, @@ -106,11 +104,5 @@ func loadViperConfig() (*srsMilter.Configuration, error) { return nil, err } } - if len(conf.LocalDomains) == 0 { - log.Printf("warn=\"local domain list is empty: only relying on SPF lookups\"") - } - if conf.LogLevel > 0 { - log.Printf("info=\"config loaded\" srsDomain=%q numSrsKeys=%d numLocalDomains=%d localIPs=%s", conf.SrsDomain, len(conf.SrsKeys), len(conf.LocalDomains), ipsToString(conf.LocalIps)) - } return &conf, nil } diff --git a/cmd/srs-milter/log.go b/cmd/srs-milter/log.go new file mode 100644 index 0000000..ff9d114 --- /dev/null +++ b/cmd/srs-milter/log.go @@ -0,0 +1,24 @@ +package main + +import ( + "github.com/go-logfmt/logfmt" + "github.com/inconshreveable/log15" +) + +func LogfmtFormatWithTime() log15.Format { + return log15.FormatFunc(func(r *log15.Record) []byte { + common := []interface{}{r.KeyNames.Time, r.Time, r.KeyNames.Lvl, r.Lvl, r.KeyNames.Msg, r.Msg} + b, _ := logfmt.MarshalKeyvals(append(common, r.Ctx...)...) + b = append(b, '\n') + return b + }) +} + +func LogfmtFormatWithoutTime() log15.Format { + return log15.FormatFunc(func(r *log15.Record) []byte { + common := []interface{}{r.KeyNames.Lvl, r.Lvl, r.KeyNames.Msg, r.Msg} + b, _ := logfmt.MarshalKeyvals(append(common, r.Ctx...)...) + b = append(b, '\n') + return b + }) +} diff --git a/cmd/srs-milter/main.go b/cmd/srs-milter/main.go index cf2fd04..bbb2592 100644 --- a/cmd/srs-milter/main.go +++ b/cmd/srs-milter/main.go @@ -3,18 +3,20 @@ package main import ( "context" "flag" - "log" + "os" "sync" "github.com/d--j/go-milter/mailfilter" "github.com/d--j/srs-milter" "github.com/fsnotify/fsnotify" + "github.com/inconshreveable/log15" "github.com/spf13/viper" ) -var RuntimeConfig *srsMilter.Configuration -var RuntimeCache *srsMilter.Cache +var RuntimeConfig *srsmilter.Configuration +var RuntimeCache *srsmilter.Cache var RuntimeConfigMutex sync.RWMutex +var LogHandler log15.Handler var ( version = "dev" @@ -26,13 +28,13 @@ var ( func main() { // parse commandline arguments var systemd bool - var protocol, address, forward, reverse string - flag.StringVar(&protocol, - "proto", + var milterProtocol, milterAddress, forward, reverse string + flag.StringVar(&milterProtocol, + "milterProto", "tcp", - "Protocol family (unix or tcp)") - flag.StringVar(&address, - "addr", + "Protocol family (`unix or tcp`)") + flag.StringVar(&milterAddress, + "milterAddr", "127.0.0.1:10382", "Bind to address/port or unix domain socket path") flag.StringVar(&forward, @@ -48,14 +50,19 @@ func main() { // disable logging date/time when called as systemd service – journald will add those anyway if systemd { - log.Default().SetFlags(0) + LogHandler = log15.StreamHandler(os.Stdout, LogfmtFormatWithoutTime()) + } else { + LogHandler = log15.StreamHandler(os.Stdout, LogfmtFormatWithTime()) } + logger := log15.New() + logger.SetHandler(LogHandler) - log.Printf("info=\"start\" version=%q commit=%q buildDate=%q", version, commit, date) + logger.Info("start", log15.Ctx{"version": version, "commit": commit, "build": date}) // make sure the specified protocol is either unix or tcp - if protocol != "unix" && protocol != "tcp" { - log.Fatal("invalid protocol name") + if milterProtocol != "unix" && milterProtocol != "tcp" { + logger.Crit("invalid protocol name", "protocol", milterProtocol) + os.Exit(1) } var err error @@ -63,22 +70,54 @@ func main() { viper.AddConfigPath("/etc/srs-milter") viper.AddConfigPath(".") if err = viper.ReadInConfig(); err != nil { - log.Fatal(err) + logger.Crit("error reading config file", log15.Ctx{"err": err}) + os.Exit(1) } RuntimeConfig, err = loadViperConfig() if err != nil { - log.Fatal(err) + logger.Crit("error parsing config file", log15.Ctx{"err": err}) + os.Exit(1) } - RuntimeConfig.Setup() - RuntimeCache = srsMilter.NewCache(RuntimeConfig) + err = RuntimeConfig.Setup() + if err != nil { + logger.Crit("error in config file", log15.Ctx{"err": err}) + os.Exit(1) + } + RuntimeCache = srsmilter.NewCache(RuntimeConfig) + configureLogging := func() { + if systemd { + LogHandler = log15.StreamHandler(os.Stdout, LogfmtFormatWithoutTime()) + } else { + LogHandler = log15.StreamHandler(os.Stdout, LogfmtFormatWithTime()) + } + switch RuntimeConfig.LogLevel { + case 0: + LogHandler = log15.LvlFilterHandler(log15.LvlCrit, LogHandler) + case 1: + LogHandler = log15.LvlFilterHandler(log15.LvlError, LogHandler) + case 2: + LogHandler = log15.LvlFilterHandler(log15.LvlWarn, LogHandler) + case 3: + LogHandler = log15.LvlFilterHandler(log15.LvlInfo, LogHandler) + default: + LogHandler = log15.LvlFilterHandler(log15.LvlDebug, LogHandler) + } + logger.SetHandler(LogHandler) + srsmilter.Log.SetHandler(LogHandler) + logger.Info("config loaded", log15.Ctx{"srsDomain": RuntimeConfig.SrsDomain, "localIps": ipsToString(RuntimeConfig.LocalIps), "numKeys": len(RuntimeConfig.SrsKeys), "numLocalDomains": len(RuntimeConfig.LocalDomains)}) + if len(RuntimeConfig.LocalDomains) == 0 { + logger.Warn("local domain list is empty: only relying on SPF lookups") + } + } + configureLogging() if forward != "" { - srsAddress, err := srsMilter.ForwardSrs(forward, RuntimeConfig) - log.Printf("address=<%s> srsAddress=<%s> error=%v", forward, srsAddress, err) + srsAddress, err := srsmilter.ForwardSrs(forward, RuntimeConfig) + logger.Info("forward SRS", log15.Ctx{"ofrom": forward, "from": srsAddress, "err": err}) } if reverse != "" { - address, err := srsMilter.ReverseSrs(reverse, RuntimeConfig) - log.Printf("srsAddress=<%s> address=<%s> error=%v", reverse, address, err) + address, err := srsmilter.ReverseSrs(reverse, RuntimeConfig) + logger.Info("reverse SRS", log15.Ctx{"oto": reverse, "to": address, "err": err}) } if forward != "" || reverse != "" { return @@ -87,30 +126,33 @@ func main() { viper.OnConfigChange(func(_ fsnotify.Event) { newConfig, err := loadViperConfig() if err != nil { - log.Printf("warn=\"could not load new config on change\" error=%q", err) + logger.Error("could not load new config on change", "err", err) } else { - newConfig.Setup() + err = newConfig.Setup() + if err != nil { + logger.Error("could not load new config on change", "err", err) + } RuntimeConfigMutex.Lock() RuntimeConfig = newConfig - RuntimeCache = srsMilter.NewCache(RuntimeConfig) + RuntimeCache = srsmilter.NewCache(RuntimeConfig) + configureLogging() RuntimeConfigMutex.Unlock() } }) viper.WatchConfig() - filter, err := mailfilter.New(protocol, address, func(ctx context.Context, trx mailfilter.Trx) (mailfilter.Decision, error) { + filter, err := mailfilter.New(milterProtocol, milterAddress, func(ctx context.Context, trx mailfilter.Trx) (mailfilter.Decision, error) { RuntimeConfigMutex.RLock() config := RuntimeConfig cache := RuntimeCache RuntimeConfigMutex.RUnlock() - return srsMilter.Filter(ctx, trx, config, cache) + return srsmilter.Filter(ctx, trx, config, cache) }, mailfilter.WithDecisionAt(mailfilter.DecisionAtEndOfHeaders)) if err != nil { - log.Fatal(err) - } - if RuntimeConfig.LogLevel > 0 { - log.Printf("info=\"ready\" network=%q address=%q", filter.Addr().Network(), filter.Addr().String()) + logger.Crit("error creating milter", "err", err) + os.Exit(1) } + logger.Info("milter ready", log15.Ctx{"network": filter.Addr().Network(), "address": filter.Addr().String()}) // quit when milter quits filter.Wait() diff --git a/config.go b/config.go index 17bc66a..5a2eb4d 100644 --- a/config.go +++ b/config.go @@ -1,8 +1,14 @@ -package srsMilter +package srsmilter import ( + "database/sql" + "fmt" "net" + "net/mail" + "strings" + "github.com/d--j/go-milter/mailfilter/addr" + "github.com/inconshreveable/log15" "golang.org/x/net/idna" ) @@ -34,16 +40,80 @@ type Configuration struct { SrsKeys []string LocalIps []net.IP LogLevel uint + DbDriver string + DbDSN string + DbForwardQuery string + db *sql.DB localDomainMap map[string]bool } -func (c *Configuration) Setup() { +func (c *Configuration) Setup() error { c.localDomainMap = make(map[string]bool) for _, d := range c.LocalDomains { c.localDomainMap[d.String()] = true } + if c.DbDriver != "" && c.DbDSN != "" && c.DbForwardQuery != "" { + db, err := sql.Open(c.DbDriver, c.DbDSN) + if err != nil { + return err + } + err = db.Ping() + if err != nil { + return err + } + c.db = db + } + return nil +} + +func (c *Configuration) IsLocalDomain(asciiDomain string) bool { + if c.localDomainMap[asciiDomain] { + return true + } + return false } -func (c *Configuration) HasLocalDomain(asciiDomain string) bool { - return c.localDomainMap[asciiDomain] +func emailWithoutExtension(local string, asciiDomain string) string { + plus := strings.IndexByte(local, '+') + if plus < 1 { + return fmt.Sprintf("%s@%s", local, asciiDomain) + } + return fmt.Sprintf("%s@%s", local[:plus-1], asciiDomain) +} + +func (c *Configuration) ResolveForward(email *addr.RcptTo) (emails []*addr.RcptTo) { + if c.db == nil { + return []*addr.RcptTo{email} + } + rows, err := c.db.Query(c.DbForwardQuery, emailWithoutExtension(email.Local(), email.AsciiDomain())) + if err != nil { + Log.Warn("query error looking up forwards", "email", email.Addr, "err", err) + return []*addr.RcptTo{email} + } + defer rows.Close() + for rows.Next() { + dest := "" + if err = rows.Scan(&dest); err != nil { + Log.Warn("scan error looking up forwards", "email", email.Addr, "err", err) + return []*addr.RcptTo{email} + } + addresses, err := mail.ParseAddressList(dest) + if err != nil { + Log.Warn("parse error looking up forwards", "email", email.Addr, "err", err) + return []*addr.RcptTo{email} + } + for _, a := range addresses { + emails = append(emails, addr.NewRcptTo(a.Address, "", email.Transport())) + } + } + if len(emails) > 0 { + return emails + } + return []*addr.RcptTo{email} +} + +var Log = log15.New() + +func init() { + Log.SetHandler(log15.DiscardHandler()) } diff --git a/config_test.go b/config_test.go index cbd29b5..96d23ee 100644 --- a/config_test.go +++ b/config_test.go @@ -1,4 +1,4 @@ -package srsMilter +package srsmilter import ( "testing" @@ -34,8 +34,8 @@ func TestConfiguration_HasLocalDomain(t *testing.T) { LocalDomains: tt.local, } c.Setup() - if got := c.HasLocalDomain(tt.arg); got != tt.want { - t.Errorf("HasLocalDomain() = %v, want %v", got, tt.want) + if got := c.IsLocalDomain(tt.arg); got != tt.want { + t.Errorf("IsLocalDomain() = %v, want %v", got, tt.want) } }) } diff --git a/filter.go b/filter.go index 071a567..5f83dfe 100644 --- a/filter.go +++ b/filter.go @@ -1,124 +1,71 @@ -package srsMilter +package srsmilter import ( "context" - "errors" - "strings" "time" "github.com/d--j/go-milter/mailfilter" - "github.com/mileusna/srs" + "github.com/inconshreveable/log15" ) -func ForwardSrs(addr string, config *Configuration) (string, error) { - if len(config.SrsKeys) == 0 { - return "", errors.New("no SRS key found") - } - s := srs.SRS{ - Secret: []byte(config.SrsKeys[0]), - Domain: config.SrsDomain.String(), - FirstSeparator: "=", - } - srsAddress, err := s.Forward(addr) - if err != nil { - return "", err - } - return srsAddress, nil -} - -func ReverseSrs(srsAddress string, config *Configuration) (string, error) { - for _, key := range config.SrsKeys { - s := srs.SRS{ - Secret: []byte(key), - Domain: config.SrsDomain.String(), - FirstSeparator: "=", - } - addr, err := s.Reverse(srsAddress) - if err != nil && err.Error() != "Hash invalid in SRS address" { - return "", err - } - if err == nil { - return addr, nil - } - } - return "", errors.New("no SRS key found or all tried keys failed") -} - -func looksLikeSrs(local string) bool { - return strings.HasPrefix(local, "SRS0=") || strings.HasPrefix(local, "SRS1=") -} - func Filter(_ context.Context, trx mailfilter.Trx, config *Configuration, cache *Cache) (mailfilter.Decision, error) { - var startTime time.Time + startTime := time.Now() didSomething := false fromIsSrs := trx.MailFrom().AsciiDomain() == config.SrsDomain.String() && looksLikeSrs(trx.MailFrom().Local()) hasRemoteTo := false + hasDkim := trx.Headers().Value("Dkim-Signature") != "" toReplacements := make(map[string]string) - if config.LogLevel > 0 { - startTime = time.Now() - if config.LogLevel > 1 { - trx.Log("from=<%s> isSRS=%v isSRSDomain=%v isSRSLocal=%v", trx.MailFrom().Addr, fromIsSrs, trx.MailFrom().AsciiDomain() == config.SrsDomain.String(), looksLikeSrs(trx.MailFrom().Local())) + logger := Log.New(log15.Ctx{"qid": trx.QueueId(), "user": trx.MailFrom().AuthenticatedUser()}) + logger.Debug("start", log15.Ctx{"ofrom": trx.MailFrom().Addr}) + + // change any rcpt to that is pointing to our SRS domain back to the real address + for _, to := range trx.RcptTos() { + if to.AsciiDomain() != config.SrsDomain.String() || !looksLikeSrs(to.Local()) { + logger.Debug("to is not one of our SRS addresses", log15.Ctx{"to": to.Addr}) + continue + } + didSomething = true + rewrittenTo, err := ReverseSrs(to.Addr, config) + if err != nil { + logger.Error("error while generating reverse SRS address", log15.Ctx{"oto": to.Addr, "to": rewrittenTo, "err": err}) + } else { + logger.Info("reverse SRS", log15.Ctx{"oto": to.Addr, "to": rewrittenTo}) + toReplacements[to.Addr] = rewrittenTo + trx.AddRcptTo(rewrittenTo, "") + trx.DelRcptTo(to.Addr) } } + // Change the return path when it's not already one of my SRS and the mail goes to another MTA // … but only when there is an SPF record for the return path that prevents me from sending without SRS if !fromIsSrs && trx.MailFrom().Addr != "" { for _, to := range trx.RcptTos() { - if config.LogLevel > 2 { - trx.Log("to=<%s>", to.Addr) - } - if to.Addr != "" && !config.HasLocalDomain(to.AsciiDomain()) { - hasRemoteTo = true - break + for _, t := range config.ResolveForward(to) { + if t.Addr != "" && !config.IsLocalDomain(t.AsciiDomain()) { + hasRemoteTo = true + logger.Info("to is remote", "to", t.Addr, "transport", t.Transport()) + break + } + logger.Debug("to is not remote", "to", t.Addr, "transport", t.Transport()) } } - if config.LogLevel > 1 { - trx.Log("hasRemoteTo=%v", hasRemoteTo) - } if hasRemoteTo && cache.IsLocalNotAllowedToSend(trx.MailFrom().Addr, trx.MailFrom().AsciiDomain()) { didSomething = true srsAddress, err := ForwardSrs(trx.MailFrom().Addr, config) - if config.LogLevel > 2 { - trx.Log("from=<%s> srsAddress=<%s> err=%v", trx.MailFrom().Addr, srsAddress, err) - } if err != nil { - trx.Log("warn=\"error while generating SRS address\" input=<%s> error=%q", trx.MailFrom().Addr, err) + logger.Error("error while generating SRS address", log15.Ctx{"ofrom": trx.MailFrom().Addr, "from": srsAddress, "err": err}) } else { + logger.Info("SRS", log15.Ctx{"ofrom": trx.MailFrom().Addr, "from": srsAddress}) // Sendmail does not like getting ESMTP args, so we always send empty ESMTP args trx.ChangeMailFrom(srsAddress, "") } } } - // change any rcpt to that is pointing to our SRS domain back to the real address - for _, to := range trx.RcptTos() { - if config.LogLevel > 2 { - trx.Log("to=<%s> isSRSDomain=%v isSRSLocal=%v", to.Addr, to.AsciiDomain() == config.SrsDomain.String(), looksLikeSrs(to.Local())) - } - if to.AsciiDomain() != config.SrsDomain.String() || !looksLikeSrs(to.Local()) { - continue - } - didSomething = true - rewrittenTo, err := ReverseSrs(to.Addr, config) - if config.LogLevel > 2 { - trx.Log("to=<%s> rewrittenTo=<%s> err=%v", to.Addr, rewrittenTo, err) - } - if err != nil { - trx.Log("warn=\"error while generating reverse SRS address\" input=<%s> error=%q", to.Addr, err) - } else { - toReplacements[to.Addr] = rewrittenTo - trx.AddRcptTo(rewrittenTo, "") - trx.DelRcptTo(to.Addr) - } - } - // fix up To:, Cc: and Bcc: headers -- but only if there are no DKIM-Signatures that we might break // (we assume that the To header is secured by the DKIM signature – almost all do this) - if config.LogLevel > 2 { - trx.Log("toReplacements=%v dkim=%v", toReplacements, trx.Headers().Value("Dkim-Signature") != "") - } - if len(toReplacements) > 0 && trx.Headers().Value("Dkim-Signature") == "" { + if len(toReplacements) > 0 && !hasDkim { fields := trx.Headers().Fields() for fields.Next() { switch fields.CanonicalKey() { @@ -127,11 +74,8 @@ func Filter(_ context.Context, trx mailfilter.Trx, config *Configuration, cache continue } addresses, err := fields.AddressList() - if config.LogLevel > 2 { - trx.Log("key=%s addresses=%v err=%v", fields.Key(), addresses, err) - } if err != nil { - trx.Log("warn=\"error while rewriting header\" key=%q value=%q error=%q", fields.CanonicalKey(), fields.Value(), err) + logger.Warn("error parsing address list, skipping", log15.Ctx{"key": fields.Key(), "value": fields.Value(), "err": err}) continue } changed := false @@ -145,19 +89,19 @@ func Filter(_ context.Context, trx mailfilter.Trx, config *Configuration, cache } if changed { fields.SetAddressList(addresses) + logger.Info("fixing MIME header", log15.Ctx{"key": fields.Key(), "ovalue": fields.Value(), "addresses": addresses}) + } else { + logger.Debug("nothing to do", log15.Ctx{"key": fields.Key(), "value": fields.Value(), "addresses": addresses}) } } + } else if len(toReplacements) > 0 && hasDkim { + logger.Info("did not touch MIME headers because of DKIM") } - switch config.LogLevel { - case 0: - default: - didSomething = true - fallthrough - case 1: - if didSomething { - trx.Log("from=<%s> isSRS=%v hasRemoteTo=%v toReplacements=%v duration=%s", trx.MailFrom().Addr, fromIsSrs, hasRemoteTo, toReplacements, time.Now().Sub(startTime)) - } + if didSomething { + logger.Info("done", "dur", time.Now().Sub(startTime)) + } else { + logger.Debug("done", "dur", time.Now().Sub(startTime)) } return mailfilter.Accept, nil diff --git a/filter_test.go b/filter_test.go index 58adeef..5d39182 100644 --- a/filter_test.go +++ b/filter_test.go @@ -1,4 +1,4 @@ -package srsMilter +package srsmilter import ( "context" @@ -81,6 +81,12 @@ func TestFilter(t *testing.T) { SetRcptTosList("someone@example.net"), conf, cache, }, mailfilter.Accept, []testtrx.Modification{{Kind: testtrx.ChangeFrom, Addr: "SRS1=TWks=example.net==ABCD=46=example.org=not-local-srs1@srs.example.com"}}, false}, + {"forward-bogus-email", args{ + newTrx(). + SetMailFrom(addr.NewMailFrom("(not-local@example.net", "", "smtp", "", "")). + SetRcptTosList("someone@example.net"), + conf, cache, + }, mailfilter.Accept, nil, false}, {"reverse-local", args{ newTrx(). SetRcptTosList("local@example.com"), @@ -115,6 +121,31 @@ func TestFilter(t *testing.T) { {Kind: testtrx.DelRcptTo, Addr: "SRS0=PNjA=46=example.net=my-srs@srs.example.com"}, {Kind: testtrx.AddRcptTo, Addr: "my-srs@example.net"}, }, false}, + {"reverse-my-srs-err", args{ + newTrx(). + SetRcptTosList("SRS0=XXXX=46=example.net=my-srs@srs.example.com"). + SetHeadersRaw([]byte("From: Someone \nTo: Someone \nSubject: Test\nDate: Fri, 10 Mar 2023 23:29:35 +0000 (UTC)\nMessage-ID: \n\n")), + conf, cache, + }, mailfilter.Accept, nil, false}, + {"reverse-my-srs-header-err", args{ + newTrx(). + SetRcptTosList("SRS0=PNjA=46=example.net=my-srs@srs.example.com"). + SetHeadersRaw([]byte("From: Someone \nTo: Someone ,,(broken\nSubject: Test\nDate: Fri, 10 Mar 2023 23:29:35 +0000 (UTC)\nMessage-ID: \n\n")), + conf, cache, + }, mailfilter.Accept, []testtrx.Modification{ + {Kind: testtrx.DelRcptTo, Addr: "SRS0=PNjA=46=example.net=my-srs@srs.example.com"}, + {Kind: testtrx.AddRcptTo, Addr: "my-srs@example.net"}, + }, false}, + {"reverse-my-srs-cc", args{ + newTrx(). + SetRcptTosList("SRS0=PNjA=46=example.net=my-srs@srs.example.com"). + SetHeadersRaw([]byte("From: Someone \nTo: Someone \nCc: \nSubject: Test\nDate: Fri, 10 Mar 2023 23:29:35 +0000 (UTC)\nMessage-ID: \n\n")), + conf, cache, + }, mailfilter.Accept, []testtrx.Modification{ + {Kind: testtrx.DelRcptTo, Addr: "SRS0=PNjA=46=example.net=my-srs@srs.example.com"}, + {Kind: testtrx.AddRcptTo, Addr: "my-srs@example.net"}, + {Kind: testtrx.ChangeHeader, Index: 1, Name: "To", Value: " \"Someone\" "}, + }, false}, {"reverse-other-srs", args{ newTrx(). SetRcptTosList("SRS0=R9Ph=46=example.net=other-srs@srs.example.net"). diff --git a/go.mod b/go.mod index 122c681..f951abe 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,9 @@ require ( github.com/agiledragon/gomonkey/v2 v2.9.0 github.com/d--j/go-milter v0.7.0 github.com/fsnotify/fsnotify v1.6.0 + github.com/go-logfmt/logfmt v0.6.0 + github.com/go-sql-driver/mysql v1.7.0 + github.com/inconshreveable/log15 v2.16.0+incompatible github.com/jellydator/ttlcache/v3 v3.0.1 github.com/mileusna/srs v0.0.0-20210306010925-501e7d108e91 github.com/mitchellh/mapstructure v1.5.0 @@ -17,8 +20,11 @@ require ( require ( github.com/emersion/go-message v0.16.0 // indirect github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect + github.com/go-stack/stack v1.8.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/magiconair/properties v1.8.6 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.5 // indirect github.com/spf13/afero v1.9.2 // indirect @@ -28,8 +34,11 @@ require ( github.com/subosito/gotenv v1.4.1 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.5.0 // indirect + golang.org/x/term v0.5.0 // indirect golang.org/x/text v0.8.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/mileusna/srs => github.com/d--j/srs v0.0.0-20230317210039-a2adfcc7ffdf diff --git a/go.sum b/go.sum index 88737f1..202acb9 100644 --- a/go.sum +++ b/go.sum @@ -52,6 +52,8 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/d--j/go-milter v0.7.0 h1:cGdgCRbqHptW4ElEA3yrPLyJymglmJ2OF5zsJIz4Ymg= github.com/d--j/go-milter v0.7.0/go.mod h1:Xqk8WnXZ6Sk7r4J0FHZFExi0l0PDwT6EHbN5yMZTVHA= +github.com/d--j/srs v0.0.0-20230317210039-a2adfcc7ffdf h1:DO0XGxkAcr+9jq0+y078Esp8PWKj8CMTsAWldufa9CA= +github.com/d--j/srs v0.0.0-20230317210039-a2adfcc7ffdf/go.mod h1:poYfuh4BE2wUTkmAyELATePMccRV3nj/fAc1UDBbsGI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -71,6 +73,12 @@ github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbS github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= +github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -133,6 +141,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/log15 v2.16.0+incompatible h1:6nvMKxtGcpgm7q0KiGs+Vc+xDvUXaBqsPKHWKsinccw= +github.com/inconshreveable/log15 v2.16.0+incompatible/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o= github.com/jellydator/ttlcache/v3 v3.0.1 h1:cHgCSMS7TdQcoprXnWUptJZzyFsqs18Lt8VVhRuZYVU= github.com/jellydator/ttlcache/v3 v3.0.1/go.mod h1:WwTaEmcXQ3MTjOm4bsZoDFiCu/hMvNWLO1w67RXz6h4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -147,8 +157,10 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mileusna/srs v0.0.0-20210306010925-501e7d108e91 h1:4NA/GiJ7l5ZXpCjAvwBIBmyJRDrfEUi9Bo4tqkC/fSw= -github.com/mileusna/srs v0.0.0-20210306010925-501e7d108e91/go.mod h1:poYfuh4BE2wUTkmAyELATePMccRV3nj/fAc1UDBbsGI= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= @@ -325,10 +337,14 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/integration/go.mod b/integration/go.mod index 62d6de1..5061fdd 100644 --- a/integration/go.mod +++ b/integration/go.mod @@ -15,12 +15,20 @@ require ( github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead // indirect github.com/emersion/go-smtp v0.16.0 // indirect github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect + github.com/go-stack/stack v1.8.1 // indirect + github.com/inconshreveable/log15 v2.16.0+incompatible // indirect github.com/jellydator/ttlcache/v3 v3.0.1 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect github.com/mileusna/srs v0.0.0-20210306010925-501e7d108e91 // indirect golang.org/x/net v0.8.0 // indirect golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/term v0.6.0 // indirect golang.org/x/text v0.8.0 // indirect golang.org/x/tools v0.7.0 // indirect ) replace github.com/d--j/srs-milter => ../ + +replace github.com/mileusna/srs => github.com/d--j/srs v0.0.0-20230317210039-a2adfcc7ffdf diff --git a/integration/go.sum b/integration/go.sum index 82f0259..9999d4f 100644 --- a/integration/go.sum +++ b/integration/go.sum @@ -6,6 +6,8 @@ github.com/d--j/go-milter v0.7.0 h1:cGdgCRbqHptW4ElEA3yrPLyJymglmJ2OF5zsJIz4Ymg= github.com/d--j/go-milter v0.7.0/go.mod h1:Xqk8WnXZ6Sk7r4J0FHZFExi0l0PDwT6EHbN5yMZTVHA= github.com/d--j/go-milter/integration v0.0.0-20230315192140-b1c7d01972da h1:aMS2hrowJanYLAzD5da+3W7apv0r8BJ5xJz8xQ6JfS4= github.com/d--j/go-milter/integration v0.0.0-20230315192140-b1c7d01972da/go.mod h1:5TOa20ELvMw6y/qA7PqrI1pZKtnZ8KPKF2JiUYjfrQc= +github.com/d--j/srs v0.0.0-20230317210039-a2adfcc7ffdf h1:DO0XGxkAcr+9jq0+y078Esp8PWKj8CMTsAWldufa9CA= +github.com/d--j/srs v0.0.0-20230317210039-a2adfcc7ffdf/go.mod h1:poYfuh4BE2wUTkmAyELATePMccRV3nj/fAc1UDBbsGI= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/emersion/go-message v0.16.0 h1:uZLz8ClLv3V5fSFF/fFdW9jXjrZkXIpE1Fn8fKx7pO4= github.com/emersion/go-message v0.16.0/go.mod h1:pDJDgf/xeUIF+eicT6B/hPX/ZbEorKkUMPOxrPVG2eQ= @@ -16,12 +18,18 @@ github.com/emersion/go-smtp v0.16.0 h1:eB9CY9527WdEZSs5sWisTmilDX7gG+Q/2IdRcmubp github.com/emersion/go-smtp v0.16.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= +github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= +github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/inconshreveable/log15 v2.16.0+incompatible h1:6nvMKxtGcpgm7q0KiGs+Vc+xDvUXaBqsPKHWKsinccw= +github.com/inconshreveable/log15 v2.16.0+incompatible/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o= github.com/jellydator/ttlcache/v3 v3.0.1 h1:cHgCSMS7TdQcoprXnWUptJZzyFsqs18Lt8VVhRuZYVU= github.com/jellydator/ttlcache/v3 v3.0.1/go.mod h1:WwTaEmcXQ3MTjOm4bsZoDFiCu/hMvNWLO1w67RXz6h4= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/mileusna/srs v0.0.0-20210306010925-501e7d108e91 h1:4NA/GiJ7l5ZXpCjAvwBIBmyJRDrfEUi9Bo4tqkC/fSw= -github.com/mileusna/srs v0.0.0-20210306010925-501e7d108e91/go.mod h1:poYfuh4BE2wUTkmAyELATePMccRV3nj/fAc1UDBbsGI= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= @@ -36,7 +44,12 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= diff --git a/integration/tests/forward/test.go b/integration/tests/forward/test.go index 1ded57f..8623864 100644 --- a/integration/tests/forward/test.go +++ b/integration/tests/forward/test.go @@ -14,15 +14,15 @@ func main() { integration.Test(func(ctx context.Context, trx mailfilter.Trx) (mailfilter.Decision, error) { p := patches.Apply() defer p.Reset() - config := &srsMilter.Configuration{ - SrsDomain: srsMilter.ToDomain("srs.example.com"), - LocalDomains: []srsMilter.Domain{"example.com"}, + config := &srsmilter.Configuration{ + SrsDomain: srsmilter.ToDomain("srs.example.com"), + LocalDomains: []srsmilter.Domain{"example.com"}, SrsKeys: []string{"secret-key"}, LocalIps: []net.IP{net.ParseIP("10.0.0.1")}, LogLevel: 5, } config.Setup() - cache := srsMilter.NewCache(config) - return srsMilter.Filter(ctx, trx, config, cache) + cache := srsmilter.NewCache(config) + return srsmilter.Filter(ctx, trx, config, cache) }, mailfilter.WithDecisionAt(mailfilter.DecisionAtEndOfHeaders)) } diff --git a/integration/tests/reverse/test.go b/integration/tests/reverse/test.go index 03dc152..0771e8d 100644 --- a/integration/tests/reverse/test.go +++ b/integration/tests/reverse/test.go @@ -14,16 +14,16 @@ func main() { integration.Test(func(ctx context.Context, trx mailfilter.Trx) (mailfilter.Decision, error) { p := patches.Apply() defer p.Reset() - config := &srsMilter.Configuration{ - SrsDomain: srsMilter.ToDomain("srs.example.com"), - LocalDomains: []srsMilter.Domain{"example.com"}, + config := &srsmilter.Configuration{ + SrsDomain: srsmilter.ToDomain("srs.example.com"), + LocalDomains: []srsmilter.Domain{"example.com"}, SrsKeys: []string{"secret-key"}, LocalIps: []net.IP{net.ParseIP("10.0.0.1")}, LogLevel: 5, } config.Setup() - cache := srsMilter.NewCache(config) - d, err := srsMilter.Filter(ctx, trx, config, cache) + cache := srsmilter.NewCache(config) + d, err := srsmilter.Filter(ctx, trx, config, cache) return d, err }, mailfilter.WithDecisionAt(mailfilter.DecisionAtEndOfHeaders)) } diff --git a/packaging/srs-milter.adoc b/packaging/srs-milter.adoc index 6fde9bc..e78bc17 100644 --- a/packaging/srs-milter.adoc +++ b/packaging/srs-milter.adoc @@ -23,10 +23,10 @@ The srs-milter(1) daemon is a Postfix and Sendmail compatible milter that does S *-help*:: Print a help message. -*-addr* _string_:: +*-milterAddr* _string_:: Bind to address/port or unix domain socket path (default "127.0.0.1:10382") -*-proto* _string_:: +*-milterProto* _string_:: Protocol family (unix or tcp) (default "tcp") *-forward* _email_:: diff --git a/packaging/srs-milter.service b/packaging/srs-milter.service index ccdcc40..cbcff4f 100644 --- a/packaging/srs-milter.service +++ b/packaging/srs-milter.service @@ -12,7 +12,7 @@ Restart=always RestartSec=10 #ConfigurationDirectory=srs-milter #ConfigurationDirectoryMode=750 -ProtectProc=invisible +#ProtectProc=invisible PrivateDevices=true ProtectHostname=true ProtectClock=true diff --git a/packaging/srs-milter.yml b/packaging/srs-milter.yml index 7407bd5..4636780 100644 --- a/packaging/srs-milter.yml +++ b/packaging/srs-milter.yml @@ -16,8 +16,9 @@ srsKeys: ['__SRS_KEY__'] # - 'example.net' # - 'example.com' -# Adjust the logging verbosity with logLevel. A logLevel of 0 only logs errors/warnings. -logLevel: 1 +# Adjust the logging verbosity with logLevel. A logLevel of 0 (the default) only logs critical errors. +# `1` also logs normal errors. `2` also warnings. `3` informational messages and `4` also includes debug messages. +logLevel: 3 # Public IPv4 and IPv6 addresses of the MTA # If the MTA is on another host or does not have public IPs (e.g. it is firewalled) you need @@ -25,3 +26,8 @@ logLevel: 1 # If you leave this list empty we will try to determine the public IPs automatically. #localIps: # - '8.8.8.8' + +# Optional: You can specify a MySQL connection/query to lookup mail forwarding replacements +#dbDriver: 'mysql' +#dbDSN: 'user:password@tcp(host:port)/dbname' +#dbForwardQuery: "SELECT destination from mail_forwarding WHERE source = ? AND active = 'y' AND server_id = 1;" diff --git a/srs.go b/srs.go new file mode 100644 index 0000000..38b209d --- /dev/null +++ b/srs.go @@ -0,0 +1,46 @@ +package srsmilter + +import ( + "errors" + "strings" + + "github.com/mileusna/srs" +) + +func ForwardSrs(addr string, config *Configuration) (string, error) { + if len(config.SrsKeys) == 0 { + return "", errors.New("no SRS key found") + } + s := srs.SRS{ + Secret: []byte(config.SrsKeys[0]), + Domain: config.SrsDomain.String(), + FirstSeparator: "=", + } + srsAddress, err := s.Forward(addr) + if err != nil { + return "", err + } + return srsAddress, nil +} + +func ReverseSrs(srsAddress string, config *Configuration) (string, error) { + for _, key := range config.SrsKeys { + s := srs.SRS{ + Secret: []byte(key), + Domain: config.SrsDomain.String(), + FirstSeparator: "=", + } + addr, err := s.Reverse(srsAddress) + if err != nil && err != srs.ErrHashInvalid { + return "", err + } + if err == nil { + return addr, nil + } + } + return "", errors.New("no SRS key found or all tried keys failed") +} + +func looksLikeSrs(local string) bool { + return strings.HasPrefix(local, "SRS0=") || strings.HasPrefix(local, "SRS1=") || strings.HasPrefix(local, "srs0=") || strings.HasPrefix(local, "srs1=") +}