add automatic greylisting
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
109
bot/settings_lists.go
Normal 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))
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user