From 8ebe80bc4f21e0187b87de08cdf74bff574e8c05 Mon Sep 17 00:00:00 2001 From: Aine Date: Wed, 16 Nov 2022 18:47:24 +0200 Subject: [PATCH] add automatic greylisting --- README.md | 4 +- bot/access.go | 23 +++++++++ bot/command.go | 7 +++ bot/command_admin.go | 49 +++++++++++++++++- bot/settings_banlist.go | 76 ---------------------------- bot/settings_bot.go | 6 +++ bot/settings_lists.go | 109 ++++++++++++++++++++++++++++++++++++++++ smtp/manager.go | 1 + smtp/server.go | 2 + smtp/session.go | 8 +++ 10 files changed, 207 insertions(+), 78 deletions(-) delete mode 100644 bot/settings_banlist.go create mode 100644 bot/settings_lists.go diff --git a/README.md b/README.md index feb0136..4e66550 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ so you can use it to send emails from your apps and scripts as well. - [x] Catch-all mailbox - [x] Map email threads to matrix threads - [x] Multi-domain support +- [x] automatic banlist +- [x] automatic greylisting #### deep dive @@ -28,7 +30,6 @@ so you can use it to send emails from your apps and scripts as well. - [ ] DKIM verification - [ ] SPF verification - [ ] DMARC verification -- [ ] Blocklists ### Send @@ -124,6 +125,7 @@ If you want to change them - check available options in the help message (`!pm h --- +* **!pm greylist** - Set automatic greylisting duration in minutes (0 - disabled) * **!pm banlist** - Enable/disable banlist and show current values * **!pm banlist:add** - Ban an IP * **!pm banlist:remove** - Unban an IP diff --git a/bot/access.go b/bot/access.go index 8ec070a..f20f002 100644 --- a/bot/access.go +++ b/bot/access.go @@ -5,6 +5,7 @@ import ( "net" "regexp" "strings" + "time" "github.com/getsentry/sentry-go" "github.com/raja/argon2pw" @@ -72,6 +73,28 @@ func (b *Bot) allowSend(actorID id.UserID, targetRoomID id.RoomID) bool { return !cfg.NoSend() } +// IsGreylisted checks if host is in greylist +func (b *Bot) IsGreylisted(addr net.Addr) bool { + if b.getBotSettings().Greylist() == 0 { + return false + } + + greylist := b.getGreylist() + greylistedAt, ok := greylist.Get(addr) + if !ok { + b.log.Debug("greylisting %s", addr.String()) + greylist.Add(addr) + err := b.setGreylist(greylist) + if err != nil { + b.log.Error("cannot update greylist with %s: %v", addr.String(), err) + } + return true + } + duration := time.Duration(b.getBotSettings().Greylist()) * time.Minute + + return greylistedAt.Add(duration).After(time.Now().UTC()) +} + // IsBanned checks if address is banned func (b *Bot) IsBanned(addr net.Addr) bool { if !b.getBotSettings().BanlistEnabled() { diff --git a/bot/command.go b/bot/command.go index b4674cb..ed4bc2f 100644 --- a/bot/command.go +++ b/bot/command.go @@ -216,6 +216,11 @@ func (b *Bot) initCommands() commandList { allowed: b.allowAdmin, }, {allowed: b.allowAdmin}, // delimiter + { + key: botOptionGreylist, + description: "Set automatic greylisting duration in minutes (0 - disabled)", + allowed: b.allowAdmin, + }, { key: commandBanlist, description: "Enable/disable banlist and show current values", @@ -270,6 +275,8 @@ func (b *Bot) handleCommand(ctx context.Context, evt *event.Event, commandSlice b.runCatchAll(ctx, commandSlice) case commandDelete: b.runDelete(ctx, commandSlice) + case botOptionGreylist: + b.runGreylist(ctx, commandSlice) case commandBanlist: b.runBanlist(ctx, commandSlice) case commandBanlistAdd: diff --git a/bot/command_admin.go b/bot/command_admin.go index 77f3c1b..cb906fb 100644 --- a/bot/command_admin.go +++ b/bot/command_admin.go @@ -208,6 +208,53 @@ func (b *Bot) runCatchAll(ctx context.Context, commandSlice []string) { b.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Catch-all is set to: `%s` (%s).", mailbox, utils.EmailsList(mailbox, ""))) } +func (b *Bot) printGreylist(ctx context.Context, roomID id.RoomID) { + cfg := b.getBotSettings() + greylist := b.getGreylist() + var msg strings.Builder + size := len(greylist) + duration := cfg.Greylist() + msg.WriteString("Currently: `") + if duration == 0 { + msg.WriteString("disabled") + } else { + msg.WriteString(cfg.Get(botOptionGreylist)) + msg.WriteString("min") + } + msg.WriteString("`") + if size > 0 { + msg.WriteString(", total known: ") + msg.WriteString(strconv.Itoa(size)) + msg.WriteString(" hosts (`") + msg.WriteString(strings.Join(greylist.Slice(), "`, `")) + msg.WriteString("`)\n\n") + } + if duration == 0 { + msg.WriteString("\n\nTo enable greylist: `") + msg.WriteString(b.prefix) + msg.WriteString(" greylist MIN`") + msg.WriteString("where `MIN` is duration in minutes for automatic greylisting\n") + } + + b.SendNotice(ctx, roomID, msg.String()) +} + +func (b *Bot) runGreylist(ctx context.Context, commandSlice []string) { + evt := eventFromContext(ctx) + if len(commandSlice) < 2 { + b.printGreylist(ctx, evt.RoomID) + return + } + cfg := b.getBotSettings() + value := utils.SanitizeIntString(commandSlice[1]) + cfg.Set(botOptionGreylist, value) + err := b.setBotSettings(cfg) + if err != nil { + b.Error(ctx, evt.RoomID, "cannot set bot config: %v", err) + } + b.SendNotice(ctx, evt.RoomID, "greylist duration has been updated") +} + func (b *Bot) runBanlist(ctx context.Context, commandSlice []string) { evt := eventFromContext(ctx) cfg := b.getBotSettings() @@ -303,7 +350,7 @@ func (b *Bot) runBanlistRemove(ctx context.Context, commandSlice []string) { func (b *Bot) runBanlistReset(ctx context.Context) { evt := eventFromContext(ctx) - err := b.setBanlist(banList{}) + err := b.setBanlist(bglist{}) if err != nil { b.Error(ctx, evt.RoomID, "cannot set banlist: %v", err) return diff --git a/bot/settings_banlist.go b/bot/settings_banlist.go deleted file mode 100644 index bcd6086..0000000 --- a/bot/settings_banlist.go +++ /dev/null @@ -1,76 +0,0 @@ -package bot - -import ( - "net" - "sort" - "time" - - "gitlab.com/etke.cc/postmoogle/utils" -) - -// account data key -const acBanlistKey = "cc.etke.postmoogle.banlist" - -type banList map[string]string - -// Slice returns slice of banlist items -func (b banList) Slice() []string { - slice := make([]string, 0, len(b)) - for item := range b { - slice = append(slice, item) - } - sort.Strings(slice) - - return slice -} - -func (b banList) 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 banlist -func (b banList) Has(addr net.Addr) bool { - _, ok := b[b.getKey(addr)] - return ok -} - -// Add an addr to banlist -func (b banList) Add(addr net.Addr) { - key := b.getKey(addr) - if _, ok := b[key]; ok { - return - } - - b[key] = time.Now().UTC().Format(time.RFC1123Z) -} - -// Remove an addr from banlist -func (b banList) Remove(addr net.Addr) { - key := b.getKey(addr) - if _, ok := b[key]; !ok { - return - } - - delete(b, key) -} - -func (b *Bot) getBanlist() banList { - config, err := b.lp.GetAccountData(acBanlistKey) - if err != nil { - b.log.Error("cannot get banlist: %v", utils.UnwrapError(err)) - } - if config == nil { - config = map[string]string{} - } - - return config -} - -func (b *Bot) setBanlist(cfg banList) error { - return utils.UnwrapError(b.lp.SetAccountData(acBanlistKey, cfg)) -} diff --git a/bot/settings_bot.go b/bot/settings_bot.go index f91c57c..c55a1a7 100644 --- a/bot/settings_bot.go +++ b/bot/settings_bot.go @@ -18,6 +18,7 @@ const ( botOptionQueueBatch = "queue:batch" botOptionQueueRetries = "queue:retries" botOptionBanlistEnabled = "banlist:enabled" + botOptionGreylist = "greylist" ) type botSettings map[string]string @@ -56,6 +57,11 @@ func (s botSettings) BanlistEnabled() bool { return utils.Bool(s.Get(botOptionBanlistEnabled)) } +// Greylist option (duration in minutes) +func (s botSettings) Greylist() int { + return utils.Int(s.Get(botOptionGreylist)) +} + // DKIMSignature (DNS TXT record) func (s botSettings) DKIMSignature() string { return s.Get(botOptionDKIMSignature) diff --git a/bot/settings_lists.go b/bot/settings_lists.go new file mode 100644 index 0000000..2295fc4 --- /dev/null +++ b/bot/settings_lists.go @@ -0,0 +1,109 @@ +package bot + +import ( + "net" + "sort" + "time" + + "gitlab.com/etke.cc/postmoogle/utils" +) + +// account data keys +const ( + acBanlistKey = "cc.etke.postmoogle.banlist" + acGreylistKey = "cc.etke.postmoogle.greylist" +) + +type bglist map[string]string + +// Slice returns slice of ban- or greylist items +func (b bglist) Slice() []string { + slice := make([]string, 0, len(b)) + for item := range b { + slice = append(slice, item) + } + sort.Strings(slice) + + return slice +} + +func (b bglist) 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 (b bglist) Has(addr net.Addr) bool { + _, ok := b[b.getKey(addr)] + return ok +} + +// Get when addr was added in ban- or greylist +func (b bglist) Get(addr net.Addr) (time.Time, bool) { + from := b[b.getKey(addr)] + if from == "" { + return time.Time{}, false + } + t, err := time.Parse(time.RFC1123Z, from) + if err != nil { + return time.Time{}, false + } + + return t, true +} + +// Add an addr to ban- or greylist +func (b bglist) Add(addr net.Addr) { + key := b.getKey(addr) + if _, ok := b[key]; ok { + return + } + + b[key] = time.Now().UTC().Format(time.RFC1123Z) +} + +// Remove an addr from ban- or greylist +func (b bglist) Remove(addr net.Addr) { + key := b.getKey(addr) + if _, ok := b[key]; !ok { + return + } + + delete(b, key) +} + +func (b *Bot) getBanlist() bglist { + config, err := b.lp.GetAccountData(acBanlistKey) + if err != nil { + b.log.Error("cannot get banlist: %v", utils.UnwrapError(err)) + } + if config == nil { + config = map[string]string{} + } + + return config +} + +func (b *Bot) setBanlist(cfg bglist) error { + return utils.UnwrapError(b.lp.SetAccountData(acBanlistKey, cfg)) +} + +func (b *Bot) getGreylist() bglist { + config, err := b.lp.GetAccountData(acGreylistKey) + if err != nil { + b.log.Error("cannot get banlist: %v", utils.UnwrapError(err)) + } + if config == nil { + config = map[string]string{} + } + + return config +} + +func (b *Bot) setGreylist(cfg bglist) error { + return utils.UnwrapError(b.lp.SetAccountData(acGreylistKey, cfg)) +} diff --git a/smtp/manager.go b/smtp/manager.go index ffcf6c0..b1b69f3 100644 --- a/smtp/manager.go +++ b/smtp/manager.go @@ -40,6 +40,7 @@ type Manager struct { type matrixbot interface { AllowAuth(string, string) bool + IsGreylisted(net.Addr) bool IsBanned(net.Addr) bool Ban(net.Addr) GetMapping(string) (id.RoomID, bool) diff --git a/smtp/server.go b/smtp/server.go index f9b0c18..51fd685 100644 --- a/smtp/server.go +++ b/smtp/server.go @@ -59,6 +59,8 @@ func (m *mailServer) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, getRoomID: m.bot.GetMapping, getFilters: m.bot.GetIFOptions, receiveEmail: m.ReceiveEmail, + ban: m.bot.Ban, + greylisted: m.bot.IsGreylisted, log: m.log, domains: m.domains, addr: state.RemoteAddr, diff --git a/smtp/session.go b/smtp/session.go index 792506d..3d656d4 100644 --- a/smtp/session.go +++ b/smtp/session.go @@ -22,6 +22,7 @@ type incomingSession struct { getRoomID func(string) (id.RoomID, bool) getFilters func(id.RoomID) utils.IncomingFilteringOptions receiveEmail func(context.Context, *utils.Email) error + greylisted func(net.Addr) bool ban func(net.Addr) domains []string @@ -76,6 +77,13 @@ func (s *incomingSession) Rcpt(to string) error { } 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.", + } + } parser := enmime.NewParser() eml, err := parser.ReadEnvelope(r) if err != nil {