move dnsbl command to per-mailbox spamcheck:rbl; refactor smtp a bit
This commit is contained in:
parent
8246e2c074
commit
b30e013172
15 changed files with 116 additions and 137 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
2
e2e/send
2
e2e/send
|
|
@ -1,3 +1,3 @@
|
|||
#!/bin/bash
|
||||
|
||||
ssmtp -v aine@gelato.casa < $1
|
||||
ssmtp -v test@localhost < $1
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ type IncomingFilteringOptions interface {
|
|||
SpamcheckDKIM() bool
|
||||
SpamcheckSMTP() bool
|
||||
SpamcheckSPF() bool
|
||||
SpamcheckRBL() bool
|
||||
SpamcheckMX() bool
|
||||
Spamlist() []string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
65
smtp/errors.go
Normal 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, "; "),
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue