add automatic greylisting

This commit is contained in:
Aine
2022-11-16 18:47:24 +02:00
parent 15b90e9e4c
commit 8ebe80bc4f
10 changed files with 207 additions and 78 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

109
bot/settings_lists.go Normal file
View File

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

View File

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

View File

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

View File

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