move dnsbl command to per-mailbox spamcheck:rbl; refactor smtp a bit

This commit is contained in:
Aine 2024-07-25 11:22:11 +03:00
parent 8246e2c074
commit b30e013172
No known key found for this signature in database
GPG key ID: 34969C908CCA2804
15 changed files with 116 additions and 137 deletions

View file

@ -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)

View file

@ -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() {

View file

@ -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:

View file

@ -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)

View file

@ -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))

View file

@ -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))
}

View file

@ -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,

View file

@ -1,3 +1,3 @@
#!/bin/bash
ssmtp -v aine@gelato.casa < $1
ssmtp -v test@localhost < $1

View file

@ -5,6 +5,7 @@ type IncomingFilteringOptions interface {
SpamcheckDKIM() bool
SpamcheckSMTP() bool
SpamcheckSPF() bool
SpamcheckRBL() bool
SpamcheckMX() bool
Spamlist() []string
}

View file

@ -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

65
smtp/errors.go Normal file
View file

@ -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, "; "),
}
}

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -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)