diff --git a/README.md b/README.md index 7e201cc..45a26e8 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ so you can use it to send emails from your apps and scripts as well. - [x] SMTP verification - [x] DKIM verification - [x] SPF verification +- [x] RBL verification - [x] MX verification - [x] Spamlist of emails (wildcards supported) - [x] Spamlist of hosts (per server only) @@ -149,6 +150,7 @@ If you want to change them - check available options in the help message (`!pm h * **`!pm spamcheck:mx`** - only accept email from servers which seem prepared to receive it (those having valid MX records) (`true` - enable, `false` - disable) * **`!pm spamcheck:spf`** - only accept email from senders which authorized to send it (those matching SPF records) (`true` - enable, `false` - disable) +* **`!pm spamcheck:rbl`** - reject incoming emails from hosts listed in DNS blocklists (`true` - enable, `false` - disable) * **`!pm spamcheck:dkim`** - only accept correctly authorized emails (without DKIM signature at all or with valid DKIM signature) (`true` - enable, `false` - disable) * **`!pm spamcheck:smtp`** - only accept email from servers which seem prepared to receive it (those listening on an SMTP port) (`true` - enable, `false` - disable) diff --git a/bot/access.go b/bot/access.go index fe51702..14f2d5b 100644 --- a/bot/access.go +++ b/bot/access.go @@ -189,28 +189,6 @@ func (b *Bot) BanAuth(ctx context.Context, addr net.Addr) { } } -// Ban an address listed in DNS Blacklists automatically -func (b *Bot) BanDNSBL(ctx context.Context, addr net.Addr) { - if !b.cfg.GetBot(ctx).BanlistEnabled() { - return - } - - if !b.cfg.GetBot(ctx).BanlistDNSBL() { - return - } - - if b.IsTrusted(addr) { - return - } - b.log.Debug().Str("addr", addr.String()).Msg("attempting to automatically ban") - banlist := b.cfg.GetBanlist(ctx) - banlist.Add(addr) - err := b.cfg.SetBanlist(ctx, banlist) - if err != nil { - b.log.Error().Err(err).Str("addr", addr.String()).Msg("cannot update banlist") - } -} - // Ban an address manually func (b *Bot) BanManually(ctx context.Context, addr net.Addr) { if !b.cfg.GetBot(ctx).BanlistEnabled() { diff --git a/bot/command.go b/bot/command.go index be685a2..84817b7 100644 --- a/bot/command.go +++ b/bot/command.go @@ -35,7 +35,6 @@ const ( commandBanlistTotals = "banlist:totals" commandBanlistAuto = "banlist:auto" commandBanlistAuth = "banlist:auth" - commandBanlistDNSBL = "banlist:dnsbl" commandBanlistAdd = "banlist:add" commandBanlistRemove = "banlist:remove" commandBanlistReset = "banlist:reset" @@ -256,6 +255,12 @@ func (b *Bot) initCommands() commandList { sanitizer: utils.SanitizeBoolString, allowed: b.allowOwner, }, + { + key: config.RoomSpamcheckRBL, + description: "reject incoming emails from hosts listed in DNS blocklists (`true` - enable, `false` - disable)", + sanitizer: utils.SanitizeBoolString, + allowed: b.allowOwner, + }, {allowed: b.allowOwner, description: "mailbox anti-spam"}, // delimiter { key: commandSpamlist, @@ -337,11 +342,6 @@ func (b *Bot) initCommands() commandList { description: "Enable/disable automatic banning of IP addresses when they try to auth with invalid credentials", allowed: b.allowAdmin, }, - { - key: commandBanlistDNSBL, - description: "Enable/disable automatic banning of IP addresses when they are listed in DNS Blacklists", - allowed: b.allowAdmin, - }, { key: commandBanlistAuto, description: "Enable/disable automatic banning of IP addresses when they try to send invalid emails", @@ -439,8 +439,6 @@ func (b *Bot) handle(ctx context.Context) { b.runBanlist(ctx, commandSlice) case commandBanlistAuth: b.runBanlistAuth(ctx, commandSlice) - case commandBanlistDNSBL: - b.runBanlistDNSBL(ctx, commandSlice) case commandBanlistAuto: b.runBanlistAuto(ctx, commandSlice) case commandBanlistTotals: diff --git a/bot/command_admin.go b/bot/command_admin.go index e994244..0c21585 100644 --- a/bot/command_admin.go +++ b/bot/command_admin.go @@ -380,33 +380,6 @@ func (b *Bot) runBanlistAuth(ctx context.Context, commandSlice []string) { //nol b.lp.SendNotice(ctx, evt.RoomID, "auth banning has been updated", linkpearl.RelatesTo(evt.ID)) } -func (b *Bot) runBanlistDNSBL(ctx context.Context, commandSlice []string) { //nolint:dupl // not in that case - evt := eventFromContext(ctx) - cfg := b.cfg.GetBot(ctx) - if len(commandSlice) < 2 { - var msg strings.Builder - msg.WriteString("Currently: `") - msg.WriteString(cfg.Get(config.BotBanlistDNSBL)) - msg.WriteString("`\n\n") - - if !cfg.BanlistDNSBL() { - msg.WriteString("To enable automatic banning by using DNS Blacklists, send `") - msg.WriteString(b.prefix) - msg.WriteString(" banlist:dnsbl true` (banlist itself must be enabled!)\n\n") - } - - b.lp.SendNotice(ctx, evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID)) - return - } - value := utils.SanitizeBoolString(commandSlice[1]) - cfg.Set(config.BotBanlistDNSBL, value) - err := b.cfg.SetBot(ctx, cfg) - if err != nil { - b.Error(ctx, "cannot set bot config: %v", err) - } - b.lp.SendNotice(ctx, evt.RoomID, "dns blacklists banning has been updated", linkpearl.RelatesTo(evt.ID)) -} - func (b *Bot) runBanlistAuto(ctx context.Context, commandSlice []string) { //nolint:dupl // not in that case evt := eventFromContext(ctx) cfg := b.cfg.GetBot(ctx) diff --git a/bot/config/bot.go b/bot/config/bot.go index 25a7344..c0adca0 100644 --- a/bot/config/bot.go +++ b/bot/config/bot.go @@ -23,7 +23,6 @@ const ( BotBanlistEnabled = "banlist:enabled" BotBanlistAuto = "banlist:auto" BotBanlistAuth = "banlist:auth" - BotBanlistDNSBL = "banlist:dnsbl" BotGreylist = "greylist" BotMautrix015Migration = "mautrix015migration" ) @@ -85,11 +84,6 @@ func (s Bot) BanlistAuth() bool { return utils.Bool(s.Get(BotBanlistAuth)) } -// BanlistDNSBL option -func (s Bot) BanlistDNSBL() bool { - return utils.Bool(s.Get(BotBanlistDNSBL)) -} - // Greylist option (duration in minutes) func (s Bot) Greylist() int { return utils.Int(s.Get(BotGreylist)) diff --git a/bot/config/room.go b/bot/config/room.go index cc06bbf..1b31f21 100644 --- a/bot/config/room.go +++ b/bot/config/room.go @@ -39,10 +39,11 @@ const ( RoomNoSubject = "nosubject" RoomNoThreads = "nothreads" + RoomSpamcheckRBL = "spamcheck:rbl" RoomSpamcheckDKIM = "spamcheck:dkim" - RoomSpamcheckMX = "spamcheck:mx" RoomSpamcheckSMTP = "spamcheck:smtp" RoomSpamcheckSPF = "spamcheck:spf" + RoomSpamcheckMX = "spamcheck:mx" RoomSpamlist = "spamlist" ) @@ -147,6 +148,10 @@ func (s Room) NoInlines() bool { return utils.Bool(s.Get(RoomNoInlines)) } +func (s Room) SpamcheckRBL() bool { + return utils.Bool(s.Get(RoomSpamcheckRBL)) +} + func (s Room) SpamcheckDKIM() bool { return utils.Bool(s.Get(RoomSpamcheckDKIM)) } diff --git a/cmd/cmd.go b/cmd/cmd.go index 7d1011e..e563c94 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -108,6 +108,9 @@ func initMatrix(cfg *config.Config) { if err != nil { log.Fatal().Err(err).Msg("cannot initialize SQL database") } + if cfg.DB.Dialect == "sqlite" { + db.SetMaxOpenConns(1) + } lp, err := linkpearl.New(&linkpearl.Config{ Homeserver: cfg.Homeserver, diff --git a/e2e/send b/e2e/send index cc647fd..2aeeab9 100755 --- a/e2e/send +++ b/e2e/send @@ -1,3 +1,3 @@ #!/bin/bash -ssmtp -v aine@gelato.casa < $1 +ssmtp -v test@localhost < $1 diff --git a/email/options.go b/email/options.go index 47232a4..899efd8 100644 --- a/email/options.go +++ b/email/options.go @@ -5,6 +5,7 @@ type IncomingFilteringOptions interface { SpamcheckDKIM() bool SpamcheckSMTP() bool SpamcheckSPF() bool + SpamcheckRBL() bool SpamcheckMX() bool Spamlist() []string } diff --git a/smtp/dnsbl.go b/smtp/dnsbl.go index 081192f..efe7766 100644 --- a/smtp/dnsbl.go +++ b/smtp/dnsbl.go @@ -51,7 +51,7 @@ type DNSBLResult struct { } // CheckDNSBLs checks if the given IP address is listed in any of the DNSBLs, and returns a decision, based on the results -func CheckDNSBLs(ctx context.Context, log *zerolog.Logger, addr net.Addr, optionalTimeout ...time.Duration) bool { +func CheckDNSBLs(ctx context.Context, log *zerolog.Logger, addr net.Addr, optionalTimeout ...time.Duration) (blocked bool, reasons []string) { ttl := DNSBLTimeout if len(optionalTimeout) > 0 { ttl = optionalTimeout[0] @@ -77,6 +77,7 @@ func CheckDNSBLs(ctx context.Context, log *zerolog.Logger, addr net.Addr, option } if r.Listed { listed++ + reasons = append(reasons, r.Reasons) listedRBLs = append(listedRBLs, r.RBL) } else { unlisted++ @@ -95,7 +96,7 @@ func CheckDNSBLs(ctx context.Context, log *zerolog.Logger, addr net.Addr, option Bool("blocked", decision). Msg("DNSBL results") - return decision + return decision, reasons } // check checks if the given IP address is listed in any of the DNSBLs diff --git a/smtp/errors.go b/smtp/errors.go new file mode 100644 index 0000000..0bcbd9d --- /dev/null +++ b/smtp/errors.go @@ -0,0 +1,65 @@ +package smtp + +import ( + "errors" + "strings" + + "github.com/emersion/go-smtp" +) + +const ( + // NoUserCode SMTP code + NoUserCode = 550 + // BannedCode SMTP code + BannedCode = 554 + // GreylistCode SMTP code + GreylistCode = 451 + // RBLCode SMTP code + RBLCode = 450 +) + +var ( + // NoUserEnhancedCode enhanced SMTP code + NoUserEnhancedCode = smtp.EnhancedCode{5, 5, 0} + // BannedEnhancedCode enhanced SMTP code + BannedEnhancedCode = smtp.EnhancedCode{5, 5, 4} + // GreylistEnhancedCode is GraylistCode in enhanced code notation + GreylistEnhancedCode = smtp.EnhancedCode{4, 5, 1} + // RBLEnhancedCode is RBLCode in enhanced code notation + RBLEnhancedCode = smtp.EnhancedCode{4, 5, 0} + // ErrBanned returned to banned hosts + ErrBanned = &smtp.SMTPError{ + Code: BannedCode, + EnhancedCode: BannedEnhancedCode, + Message: "please, don't bother me anymore, kupo.", + } + // ErrNoUser returned when no such mailbox found + ErrNoUser = &smtp.SMTPError{ + Code: NoUserCode, + EnhancedCode: NoUserEnhancedCode, + Message: "no such user here, kupo.", + } + // ErrGreylisted returned when the host is graylisted + ErrGreylisted = &smtp.SMTPError{ + Code: GreylistCode, + EnhancedCode: GreylistEnhancedCode, + Message: "You have been greylisted, try again a bit later.", + } + // ErrRBL returned when the host is blacklisted + ErrRBL = &smtp.SMTPError{ + Code: RBLCode, + EnhancedCode: RBLEnhancedCode, + Message: "You are blacklisted, kupo.", + } + // ErrInvalidEmail for invalid emails :) + ErrInvalidEmail = errors.New("please, provide valid email address") +) + +// extendErrRBL extends the RBL error with reasons returned by the DNSBLs +func extendErrRBL(reasons []string) *smtp.SMTPError { + return &smtp.SMTPError{ + Code: RBLCode, + EnhancedCode: RBLEnhancedCode, + Message: "You are blacklisted, kupo. Details: " + strings.Join(reasons, "; "), + } +} diff --git a/smtp/listener.go b/smtp/listener.go index 61b58a4..26ab274 100644 --- a/smtp/listener.go +++ b/smtp/listener.go @@ -17,14 +17,12 @@ type Listener struct { tlsMu sync.Mutex listener net.Listener isBanned func(context.Context, net.Addr) bool - banDNSBL func(context.Context, net.Addr) } func NewListener( port string, tlsConfig *tls.Config, isBanned func(context.Context, net.Addr) bool, - banDNSBL func(context.Context, net.Addr), log *zerolog.Logger, ) (*Listener, error) { actual, err := net.Listen("tcp", ":"+port) @@ -38,7 +36,6 @@ func NewListener( tls: tlsConfig, listener: actual, isBanned: isBanned, - banDNSBL: banDNSBL, }, nil } @@ -69,15 +66,6 @@ func (l *Listener) Accept() (net.Conn, error) { continue } - log.Info().Msg("checking dns blacklists...") - if CheckDNSBLs(ctx, l.log, conn.RemoteAddr()) { - //nolint:gocritic // TODO - // conn.Close() - // l.banDNSBL(ctx, conn.RemoteAddr()) - log.Info().Msg("should rejected connection (DNS Blacklist); but won't do it for now (for testing purposes)") - continue - } - log.Info().Msg("accepted connection") if l.tls != nil { diff --git a/smtp/manager.go b/smtp/manager.go index 528bd1b..64f81e1 100644 --- a/smtp/manager.go +++ b/smtp/manager.go @@ -67,7 +67,6 @@ type matrixbot interface { IsTrusted(net.Addr) bool BanAuto(context.Context, net.Addr) BanAuth(context.Context, net.Addr) - BanDNSBL(context.Context, net.Addr) GetMapping(context.Context, string) (id.RoomID, bool) GetIFOptions(context.Context, id.RoomID) email.IncomingFilteringOptions IncomingEmail(context.Context, *email.Email) error @@ -174,7 +173,7 @@ func (m *Manager) Stop() { } func (m *Manager) listen(port string, tlsConfig *tls.Config) { - lwrapper, err := NewListener(port, tlsConfig, m.bot.IsBanned, m.bot.BanDNSBL, m.log) + lwrapper, err := NewListener(port, tlsConfig, m.bot.IsBanned, m.log) if err != nil { m.log.Error().Err(err).Str("port", port).Msg("cannot start listener") m.errs <- err diff --git a/smtp/server.go b/smtp/server.go index c2a81c2..a43573b 100644 --- a/smtp/server.go +++ b/smtp/server.go @@ -10,32 +10,6 @@ import ( "gitlab.com/etke.cc/postmoogle/email" ) -const ( - // NoUserCode SMTP code - NoUserCode = 550 - // BannedCode SMTP code - BannedCode = 554 -) - -var ( - // NoUserEnhancedCode enhanced SMTP code - NoUserEnhancedCode = smtp.EnhancedCode{5, 5, 0} - // BannedEnhancedCode enhanced SMTP code - BannedEnhancedCode = smtp.EnhancedCode{5, 5, 4} - // ErrBanned returned to banned hosts - ErrBanned = &smtp.SMTPError{ - Code: BannedCode, - EnhancedCode: BannedEnhancedCode, - Message: "please, don't bother me anymore, kupo.", - } - // ErrNoUser returned when no such mailbox found - ErrNoUser = &smtp.SMTPError{ - Code: NoUserCode, - EnhancedCode: NoUserEnhancedCode, - Message: "no such user here, kupo.", - } -) - type mailServer struct { bot matrixbot log *zerolog.Logger diff --git a/smtp/session.go b/smtp/session.go index d84b0a8..b59a4f5 100644 --- a/smtp/session.go +++ b/smtp/session.go @@ -3,7 +3,6 @@ package smtp import ( "bytes" "context" - "errors" "io" "net" "net/url" @@ -22,22 +21,14 @@ import ( ) const ( - // GraylistCode SMTP code - GraylistCode = 451 // Incoming is the direction of the email Incoming = "incoming" // Outgoing is the direction of the email Outoing = "outgoing" ) -var ( - // ErrInvalidEmail for invalid emails :) - ErrInvalidEmail = errors.New("please, provide valid email address") - // GraylistEnhancedCode is GraylistCode in enhanced code notation - GraylistEnhancedCode = smtp.EnhancedCode{4, 5, 1} - // ensure that session implements smtp.AuthSession - _ smtp.AuthSession = (*session)(nil) -) +// ensure that session implements smtp.AuthSession +var _ smtp.AuthSession = (*session)(nil) type session struct { log *zerolog.Logger @@ -95,27 +86,38 @@ func (s *session) authPlain(_, username, password string) error { func (s *session) Mail(from string, _ *smtp.MailOptions) error { if s.dir == Outoing { - if err := s.validateOutgoingMail(from); err != nil { - return err - } - } else { - if !email.AddressValid(from) { - s.log.Debug().Str("from", from).Msg("address is invalid") - s.bot.BanAuto(s.ctx, s.conn.Conn().RemoteAddr()) - return ErrBanned - } - s.from = email.Address(from) - s.log.Debug().Str("from", from).Msg("incoming mail") + return s.validateOutgoingMail(from) } + + // incoming mail + if !email.AddressValid(from) { + s.log.Debug().Str("from", from).Msg("address is invalid") + s.bot.BanAuto(s.ctx, s.conn.Conn().RemoteAddr()) + return ErrBanned + } + s.from = email.Address(from) + s.log.Debug().Str("from", from).Msg("incoming mail") return nil } func (s *session) Rcpt(to string, _ *smtp.RcptOptions) error { s.tos = append(s.tos, to) s.log.Debug().Str("to", to).Msg("mail") - if s.dir != Outoing { - if err := s.validateIncomingRcpt(to); err != nil { - return err + if s.dir == Outoing { + return nil + } + if err := s.validateIncomingRcpt(to); err != nil { + return err + } + + if s.bot.GetIFOptions(s.ctx, s.roomID).SpamcheckRBL() { + s.log.Info().Msg("checking dns blacklists...") + if listed, reasons := CheckDNSBLs(s.ctx, s.log, s.conn.Conn().RemoteAddr()); listed { + s.log.Info().Strs("reasons", reasons).Msg("rejected incoming email (DNS Blacklist)") + if len(reasons) > 0 { + return extendErrRBL(reasons) + } + return ErrRBL } } @@ -173,11 +175,7 @@ func (s *session) incomingData(r io.Reader) error { return ErrBanned } if s.bot.IsGreylisted(s.ctx, addr) { - return &smtp.SMTPError{ - Code: GraylistCode, - EnhancedCode: GraylistEnhancedCode, - Message: "You have been greylisted, try again a bit later.", - } + return ErrGreylisted } if validations.SpamcheckDKIM() { results, verr := dkim.Verify(reader)