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] Catch-all mailbox
|
||||||
- [x] Map email threads to matrix threads
|
- [x] Map email threads to matrix threads
|
||||||
- [x] Multi-domain support
|
- [x] Multi-domain support
|
||||||
|
- [x] automatic banlist
|
||||||
|
- [x] automatic greylisting
|
||||||
|
|
||||||
#### deep dive
|
#### deep dive
|
||||||
|
|
||||||
@@ -28,7 +30,6 @@ so you can use it to send emails from your apps and scripts as well.
|
|||||||
- [ ] DKIM verification
|
- [ ] DKIM verification
|
||||||
- [ ] SPF verification
|
- [ ] SPF verification
|
||||||
- [ ] DMARC verification
|
- [ ] DMARC verification
|
||||||
- [ ] Blocklists
|
|
||||||
|
|
||||||
### Send
|
### 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** - Enable/disable banlist and show current values
|
||||||
* **!pm banlist:add** - Ban an IP
|
* **!pm banlist:add** - Ban an IP
|
||||||
* **!pm banlist:remove** - Unban an IP
|
* **!pm banlist:remove** - Unban an IP
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/getsentry/sentry-go"
|
"github.com/getsentry/sentry-go"
|
||||||
"github.com/raja/argon2pw"
|
"github.com/raja/argon2pw"
|
||||||
@@ -72,6 +73,28 @@ func (b *Bot) allowSend(actorID id.UserID, targetRoomID id.RoomID) bool {
|
|||||||
return !cfg.NoSend()
|
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
|
// IsBanned checks if address is banned
|
||||||
func (b *Bot) IsBanned(addr net.Addr) bool {
|
func (b *Bot) IsBanned(addr net.Addr) bool {
|
||||||
if !b.getBotSettings().BanlistEnabled() {
|
if !b.getBotSettings().BanlistEnabled() {
|
||||||
|
|||||||
@@ -216,6 +216,11 @@ func (b *Bot) initCommands() commandList {
|
|||||||
allowed: b.allowAdmin,
|
allowed: b.allowAdmin,
|
||||||
},
|
},
|
||||||
{allowed: b.allowAdmin}, // delimiter
|
{allowed: b.allowAdmin}, // delimiter
|
||||||
|
{
|
||||||
|
key: botOptionGreylist,
|
||||||
|
description: "Set automatic greylisting duration in minutes (0 - disabled)",
|
||||||
|
allowed: b.allowAdmin,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: commandBanlist,
|
key: commandBanlist,
|
||||||
description: "Enable/disable banlist and show current values",
|
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)
|
b.runCatchAll(ctx, commandSlice)
|
||||||
case commandDelete:
|
case commandDelete:
|
||||||
b.runDelete(ctx, commandSlice)
|
b.runDelete(ctx, commandSlice)
|
||||||
|
case botOptionGreylist:
|
||||||
|
b.runGreylist(ctx, commandSlice)
|
||||||
case commandBanlist:
|
case commandBanlist:
|
||||||
b.runBanlist(ctx, commandSlice)
|
b.runBanlist(ctx, commandSlice)
|
||||||
case commandBanlistAdd:
|
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, "")))
|
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) {
|
func (b *Bot) runBanlist(ctx context.Context, commandSlice []string) {
|
||||||
evt := eventFromContext(ctx)
|
evt := eventFromContext(ctx)
|
||||||
cfg := b.getBotSettings()
|
cfg := b.getBotSettings()
|
||||||
@@ -303,7 +350,7 @@ func (b *Bot) runBanlistRemove(ctx context.Context, commandSlice []string) {
|
|||||||
func (b *Bot) runBanlistReset(ctx context.Context) {
|
func (b *Bot) runBanlistReset(ctx context.Context) {
|
||||||
evt := eventFromContext(ctx)
|
evt := eventFromContext(ctx)
|
||||||
|
|
||||||
err := b.setBanlist(banList{})
|
err := b.setBanlist(bglist{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot set banlist: %v", err)
|
b.Error(ctx, evt.RoomID, "cannot set banlist: %v", err)
|
||||||
return
|
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"
|
botOptionQueueBatch = "queue:batch"
|
||||||
botOptionQueueRetries = "queue:retries"
|
botOptionQueueRetries = "queue:retries"
|
||||||
botOptionBanlistEnabled = "banlist:enabled"
|
botOptionBanlistEnabled = "banlist:enabled"
|
||||||
|
botOptionGreylist = "greylist"
|
||||||
)
|
)
|
||||||
|
|
||||||
type botSettings map[string]string
|
type botSettings map[string]string
|
||||||
@@ -56,6 +57,11 @@ func (s botSettings) BanlistEnabled() bool {
|
|||||||
return utils.Bool(s.Get(botOptionBanlistEnabled))
|
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)
|
// DKIMSignature (DNS TXT record)
|
||||||
func (s botSettings) DKIMSignature() string {
|
func (s botSettings) DKIMSignature() string {
|
||||||
return s.Get(botOptionDKIMSignature)
|
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 {
|
type matrixbot interface {
|
||||||
AllowAuth(string, string) bool
|
AllowAuth(string, string) bool
|
||||||
|
IsGreylisted(net.Addr) bool
|
||||||
IsBanned(net.Addr) bool
|
IsBanned(net.Addr) bool
|
||||||
Ban(net.Addr)
|
Ban(net.Addr)
|
||||||
GetMapping(string) (id.RoomID, bool)
|
GetMapping(string) (id.RoomID, bool)
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ func (m *mailServer) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session,
|
|||||||
getRoomID: m.bot.GetMapping,
|
getRoomID: m.bot.GetMapping,
|
||||||
getFilters: m.bot.GetIFOptions,
|
getFilters: m.bot.GetIFOptions,
|
||||||
receiveEmail: m.ReceiveEmail,
|
receiveEmail: m.ReceiveEmail,
|
||||||
|
ban: m.bot.Ban,
|
||||||
|
greylisted: m.bot.IsGreylisted,
|
||||||
log: m.log,
|
log: m.log,
|
||||||
domains: m.domains,
|
domains: m.domains,
|
||||||
addr: state.RemoteAddr,
|
addr: state.RemoteAddr,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type incomingSession struct {
|
|||||||
getRoomID func(string) (id.RoomID, bool)
|
getRoomID func(string) (id.RoomID, bool)
|
||||||
getFilters func(id.RoomID) utils.IncomingFilteringOptions
|
getFilters func(id.RoomID) utils.IncomingFilteringOptions
|
||||||
receiveEmail func(context.Context, *utils.Email) error
|
receiveEmail func(context.Context, *utils.Email) error
|
||||||
|
greylisted func(net.Addr) bool
|
||||||
ban func(net.Addr)
|
ban func(net.Addr)
|
||||||
domains []string
|
domains []string
|
||||||
|
|
||||||
@@ -76,6 +77,13 @@ func (s *incomingSession) Rcpt(to string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *incomingSession) Data(r io.Reader) 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()
|
parser := enmime.NewParser()
|
||||||
eml, err := parser.ReadEnvelope(r)
|
eml, err := parser.ReadEnvelope(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user