diff --git a/README.md b/README.md index 58d9f52..7e4191e 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ env vars other optional config parameters * **POSTMOOGLE_PORT** - SMTP port to listen for new emails +* **POSTMOOGLE_PROXIES** - space separated list of IP addresses considered as trusted proxies, thus never banned * **POSTMOOGLE_TLS_PORT** - secure SMTP port to listen for new emails. Requires valid cert and key as well * **POSTMOOGLE_TLS_CERT** - space separated list of paths to the SSL certificates (chain) of your domains, note that position in the cert list must match the position of the cert's key in the key list * **POSTMOOGLE_TLS_KEY** - space separated list of paths to the SSL certificates' private keys of your domains, note that position on the key list must match the position of cert in the cert list diff --git a/bot/access.go b/bot/access.go index 19b6322..0bbd5a5 100644 --- a/bot/access.go +++ b/bot/access.go @@ -109,8 +109,25 @@ func (b *Bot) IsBanned(addr net.Addr) bool { return b.cfg.GetBanlist().Has(addr) } +// IsTrusted checks if address is a trusted (proxy) +func (b *Bot) IsTrusted(addr net.Addr) bool { + ip := utils.AddrIP(addr) + for _, proxy := range b.proxies { + if ip == proxy { + b.log.Debug("address %s is trusted", ip) + return true + } + } + + b.log.Debug("address %s is NOT trusted", ip) + return false +} + // Ban an address func (b *Bot) Ban(addr net.Addr) { + if b.IsTrusted(addr) { + return + } b.log.Debug("attempting to ban %s", addr.String()) banlist := b.cfg.GetBanlist() banlist.Add(addr) diff --git a/bot/bot.go b/bot/bot.go index 81485f7..11a0d72 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -34,6 +34,7 @@ type Bot struct { adminRooms []id.RoomID commands commandList rooms sync.Map + proxies []string sendmail func(string, string, string) error cfg *config.Manager log *logger.Logger @@ -49,6 +50,7 @@ func New( lp *linkpearl.Linkpearl, log *logger.Logger, cfg *config.Manager, + proxies []string, prefix string, domains []string, admins []string, @@ -59,6 +61,7 @@ func New( prefix: prefix, rooms: sync.Map{}, adminRooms: []id.RoomID{}, + proxies: proxies, mbxc: mbxc, cfg: cfg, log: log, diff --git a/bot/config/lists.go b/bot/config/lists.go index 0e28745..3f4c3db 100644 --- a/bot/config/lists.go +++ b/bot/config/lists.go @@ -4,6 +4,8 @@ import ( "net" "sort" "time" + + "gitlab.com/etke.cc/postmoogle/utils" ) // account data keys @@ -26,24 +28,15 @@ func (l List) Slice() []string { return slice } -func (l List) getKey(addr net.Addr) string { - key := addr.String() - host, _, _ := net.SplitHostPort(key) //nolint:errcheck // either way it's ok - if host != "" { - key = host - } - return key -} - // Has addr in ban- or greylist func (l List) Has(addr net.Addr) bool { - _, ok := l[l.getKey(addr)] + _, ok := l[utils.AddrIP(addr)] return ok } // Get when addr was added in ban- or greylist func (l List) Get(addr net.Addr) (time.Time, bool) { - from := l[l.getKey(addr)] + from := l[utils.AddrIP(addr)] if from == "" { return time.Time{}, false } @@ -57,7 +50,7 @@ func (l List) Get(addr net.Addr) (time.Time, bool) { // Add an addr to ban- or greylist func (l List) Add(addr net.Addr) { - key := l.getKey(addr) + key := utils.AddrIP(addr) if _, ok := l[key]; ok { return } @@ -67,7 +60,7 @@ func (l List) Add(addr net.Addr) { // Remove an addr from ban- or greylist func (l List) Remove(addr net.Addr) { - key := l.getKey(addr) + key := utils.AddrIP(addr) if _, ok := l[key]; !ok { return } diff --git a/cmd/cmd.go b/cmd/cmd.go index a9af1be..20f01f8 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -123,7 +123,7 @@ func initMatrix(cfg *config.Config) { mxc = mxconfig.New(lp, cfglog) q = queue.New(lp, mxc, qlog) - mxb, err = bot.New(q, lp, mxlog, mxc, cfg.Prefix, cfg.Domains, cfg.Admins, bot.MBXConfig(cfg.Mailboxes)) + mxb, err = bot.New(q, lp, mxlog, mxc, cfg.Proxies, cfg.Prefix, cfg.Domains, cfg.Admins, bot.MBXConfig(cfg.Mailboxes)) if err != nil { // nolint // Fatal = panic, not os.Exit() log.Fatal("cannot start matrix bot: %v", err) diff --git a/config/config.go b/config/config.go index 3a37ec1..2764e58 100644 --- a/config/config.go +++ b/config/config.go @@ -19,6 +19,7 @@ func New() *Config { Prefix: env.String("prefix", defaultConfig.Prefix), Domains: migrateDomains("domain", "domains"), Port: env.String("port", defaultConfig.Port), + Proxies: env.Slice("proxies"), NoEncryption: env.Bool("noencryption"), DataSecret: env.String("data.secret", defaultConfig.DataSecret), MaxSize: env.Int("maxsize", defaultConfig.MaxSize), diff --git a/config/types.go b/config/types.go index 6efe68d..f39c384 100644 --- a/config/types.go +++ b/config/types.go @@ -14,6 +14,8 @@ type Config struct { Domains []string // Port for SMTP Port string + // Proxies is list of trusted SMTP proxies + Proxies []string // RoomID of the admin room LogLevel string // DataSecret is account data secret key (password) to encrypt all account data values diff --git a/smtp/manager.go b/smtp/manager.go index 2dea6af..62622df 100644 --- a/smtp/manager.go +++ b/smtp/manager.go @@ -43,6 +43,7 @@ type matrixbot interface { AllowAuth(string, string) (id.RoomID, bool) IsGreylisted(net.Addr) bool IsBanned(net.Addr) bool + IsTrusted(net.Addr) bool Ban(net.Addr) GetMapping(string) (id.RoomID, bool) GetIFOptions(id.RoomID) email.IncomingFilteringOptions diff --git a/smtp/server.go b/smtp/server.go index a9c69aa..431e89b 100644 --- a/smtp/server.go +++ b/smtp/server.go @@ -81,6 +81,7 @@ func (m *mailServer) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, receiveEmail: m.ReceiveEmail, ban: m.bot.Ban, greylisted: m.bot.IsGreylisted, + trusted: m.bot.IsTrusted, log: m.log, domains: m.domains, addr: state.RemoteAddr, diff --git a/smtp/session.go b/smtp/session.go index 0e4b799..448dbdc 100644 --- a/smtp/session.go +++ b/smtp/session.go @@ -6,6 +6,7 @@ import ( "errors" "io" "net" + "strconv" "github.com/emersion/go-msgauth/dkim" "github.com/emersion/go-smtp" @@ -26,9 +27,10 @@ type incomingSession struct { getFilters func(id.RoomID) email.IncomingFilteringOptions receiveEmail func(context.Context, *email.Email) error greylisted func(net.Addr) bool + trusted func(net.Addr) bool ban func(net.Addr) domains []string - enforceDKIM bool + roomID id.RoomID ctx context.Context addr net.Addr @@ -64,38 +66,69 @@ func (s *incomingSession) Rcpt(to string) error { return ErrNoUser } - roomID, ok := s.getRoomID(utils.Mailbox(to)) + var ok bool + s.roomID, ok = s.getRoomID(utils.Mailbox(to)) if !ok { s.log.Debug("mapping for %s not found", to) return ErrNoUser } - validations := s.getFilters(roomID) - s.enforceDKIM = validations.SpamcheckDKIM() - if !validateIncoming(s.from, to, s.addr, s.log, validations) { - s.ban(s.addr) - return ErrBanned - } - s.log.Debug("mail to %s", to) return nil } -func (s *incomingSession) Data(r io.Reader) error { - if s.greylisted(s.addr) { - return &smtp.SMTPError{ - Code: 451, - EnhancedCode: smtp.EnhancedCode{4, 5, 1}, - Message: "You have been greylisted, try again a bit later.", - } +// getAddr gets real address of incoming email serder, +// including special case of trusted proxy +func (s *incomingSession) getAddr(envelope *enmime.Envelope) net.Addr { + if !s.trusted(s.addr) { + return s.addr } + + addrHeader := envelope.GetHeader("X-Real-Addr") + if addrHeader == "" { + return s.addr + } + + host, portString, _ := net.SplitHostPort(addrHeader) //nolint:errcheck + if host == "" { + return s.addr + } + + var port int + port, _ = strconv.Atoi(portString) //nolint:errcheck + + realAddr := &net.TCPAddr{IP: net.ParseIP(host), Port: port} + s.log.Info("real address: %s", realAddr.String()) + return realAddr +} + +func (s *incomingSession) Data(r io.Reader) error { data, err := io.ReadAll(r) if err != nil { s.log.Error("cannot read DATA: %v", err) return err } reader := bytes.NewReader(data) - if s.enforceDKIM { + parser := enmime.NewParser() + envelope, err := parser.ReadEnvelope(reader) + if err != nil { + return err + } + addr := s.getAddr(envelope) + reader.Seek(0, io.SeekStart) //nolint:errcheck + validations := s.getFilters(s.roomID) + if !validateIncoming(s.from, s.tos[0], addr, s.log, validations) { + s.ban(addr) + return ErrBanned + } + if s.greylisted(addr) { + return &smtp.SMTPError{ + Code: 451, + EnhancedCode: smtp.EnhancedCode{4, 5, 1}, + Message: "You have been greylisted, try again a bit later.", + } + } + if validations.SpamcheckDKIM() { results, verr := dkim.Verify(reader) if verr != nil { s.log.Error("cannot verify DKIM: %v", verr) @@ -107,12 +140,6 @@ func (s *incomingSession) Data(r io.Reader) error { return result.Err } } - reader.Seek(0, io.SeekStart) //nolint:errcheck - } - parser := enmime.NewParser() - envelope, err := parser.ReadEnvelope(reader) - if err != nil { - return err } eml := email.FromEnvelope(s.tos[0], envelope) @@ -125,6 +152,7 @@ func (s *incomingSession) Data(r io.Reader) error { } return nil } + func (s *incomingSession) Reset() {} func (s *incomingSession) Logout() error { return nil } diff --git a/utils/utils.go b/utils/utils.go index fd7af2d..fa9b486 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,6 +1,7 @@ package utils import ( + "net" "strconv" "strings" @@ -22,6 +23,16 @@ func SetDomains(slice []string) { domains = slice } +// AddrIP returns IP from a network address +func AddrIP(addr net.Addr) string { + key := addr.String() + host, _, _ := net.SplitHostPort(key) //nolint:errcheck // either way it's ok + if host != "" { + key = host + } + return key +} + // SanitizeDomain checks that input domain is available for use func SanitizeDomain(domain string) string { domain = strings.TrimSpace(domain)