big refactoring

This commit is contained in:
Aine
2022-11-25 23:33:38 +02:00
parent 14bad9f479
commit 8d6c4aeafe
23 changed files with 933 additions and 816 deletions

View File

@@ -41,7 +41,7 @@ func (b *Bot) allowOwner(actorID id.UserID, targetRoomID id.RoomID) bool {
if !b.allowUsers(actorID) {
return false
}
cfg, err := b.getRoomSettings(targetRoomID)
cfg, err := b.cfg.GetRoom(targetRoomID)
if err != nil {
b.Error(sentry.SetHubOnContext(context.Background(), sentry.CurrentHub()), targetRoomID, "failed to retrieve settings: %v", err)
return false
@@ -64,7 +64,7 @@ func (b *Bot) allowSend(actorID id.UserID, targetRoomID id.RoomID) bool {
return false
}
cfg, err := b.getRoomSettings(targetRoomID)
cfg, err := b.cfg.GetRoom(targetRoomID)
if err != nil {
b.Error(sentry.SetHubOnContext(context.Background(), sentry.CurrentHub()), targetRoomID, "failed to retrieve settings: %v", err)
return false
@@ -84,41 +84,37 @@ func (b *Bot) isReserved(mailbox string) bool {
// IsGreylisted checks if host is in greylist
func (b *Bot) IsGreylisted(addr net.Addr) bool {
if b.getBotSettings().Greylist() == 0 {
if b.cfg.GetBot().Greylist() == 0 {
return false
}
greylist := b.getGreylist()
greylist := b.cfg.GetGreylist()
greylistedAt, ok := greylist.Get(addr)
if !ok {
b.log.Debug("greylisting %s", addr.String())
greylist.Add(addr)
err := b.setGreylist(greylist)
err := b.cfg.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
duration := time.Duration(b.cfg.GetBot().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 {
return b.banlist.Has(addr)
return b.cfg.GetBanlist().Has(addr)
}
// Ban an address
func (b *Bot) Ban(addr net.Addr) {
if !b.getBotSettings().BanlistEnabled() {
return
}
b.log.Debug("banning %s", addr.String())
banlist := b.getBanlist()
b.log.Debug("attempting to ban %s", addr.String())
banlist := b.cfg.GetBanlist()
banlist.Add(addr)
err := b.setBanlist(banlist)
err := b.cfg.SetBanlist(banlist)
if err != nil {
b.log.Error("cannot update banlist with %s: %v", addr.String(), err)
}
@@ -141,7 +137,7 @@ func (b *Bot) AllowAuth(email, password string) (id.RoomID, bool) {
if !ok {
return "", false
}
cfg, err := b.getRoomSettings(roomID)
cfg, err := b.cfg.GetRoom(roomID)
if err != nil {
b.log.Error("failed to retrieve settings: %v", err)
return "", false

View File

@@ -12,6 +12,10 @@ import (
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/bot/config"
"gitlab.com/etke.cc/postmoogle/bot/queue"
"gitlab.com/etke.cc/postmoogle/utils"
)
// Mailboxes config
@@ -29,19 +33,22 @@ type Bot struct {
allowedAdmins []*regexp.Regexp
adminRooms []id.RoomID
commands commandList
banlist bglist
rooms sync.Map
sendmail func(string, string, string) error
cfg *config.Manager
log *logger.Logger
lp *linkpearl.Linkpearl
mu map[string]*sync.Mutex
mu utils.Mutex
q *queue.Queue
handledMembershipEvents sync.Map
}
// New creates a new matrix bot
func New(
q *queue.Queue,
lp *linkpearl.Linkpearl,
log *logger.Logger,
cfg *config.Manager,
prefix string,
domains []string,
admins []string,
@@ -53,9 +60,11 @@ func New(
rooms: sync.Map{},
adminRooms: []id.RoomID{},
mbxc: mbxc,
cfg: cfg,
log: log,
lp: lp,
mu: map[string]*sync.Mutex{},
mu: utils.NewMutex(),
q: q,
}
users, err := b.initBotUsers()
if err != nil {
@@ -114,7 +123,6 @@ func (b *Bot) Start(statusMsg string) error {
if err := b.syncRooms(); err != nil {
return err
}
b.syncBanlist()
b.initSync()
b.log.Info("Postmoogle has been started")

View File

@@ -6,10 +6,10 @@ import (
"strings"
"time"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/bot/config"
"gitlab.com/etke.cc/postmoogle/email"
"gitlab.com/etke.cc/postmoogle/utils"
)
@@ -19,10 +19,10 @@ const (
commandStop = "stop"
commandSend = "send"
commandDKIM = "dkim"
commandCatchAll = botOptionCatchAll
commandUsers = botOptionUsers
commandQueueBatch = botOptionQueueBatch
commandQueueRetries = botOptionQueueRetries
commandCatchAll = config.BotCatchAll
commandUsers = config.BotUsers
commandQueueBatch = config.BotQueueBatch
commandQueueRetries = config.BotQueueRetries
commandDelete = "delete"
commandBanlist = "banlist"
commandBanlistAdd = "banlist:add"
@@ -71,143 +71,143 @@ func (b *Bot) initCommands() commandList {
{allowed: b.allowOwner, description: "mailbox ownership"}, // delimiter
// options commands
{
key: roomOptionMailbox,
key: config.RoomMailbox,
description: "Get or set mailbox of the room",
sanitizer: utils.Mailbox,
allowed: b.allowOwner,
},
{
key: roomOptionDomain,
key: config.RoomDomain,
description: "Get or set default domain of the room",
sanitizer: utils.SanitizeDomain,
allowed: b.allowOwner,
},
{
key: roomOptionOwner,
key: config.RoomOwner,
description: "Get or set owner of the room",
sanitizer: func(s string) string { return s },
allowed: b.allowOwner,
},
{
key: roomOptionPassword,
key: config.RoomPassword,
description: "Get or set SMTP password of the room's mailbox",
allowed: b.allowOwner,
},
{allowed: b.allowOwner, description: "mailbox options"}, // delimiter
{
key: roomOptionNoSend,
key: config.RoomNoSend,
description: fmt.Sprintf(
"Get or set `%s` of the room (`true` - disable email sending; `false` - enable email sending)",
roomOptionNoSend,
config.RoomNoSend,
),
sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner,
},
{
key: roomOptionNoSender,
key: config.RoomNoSender,
description: fmt.Sprintf(
"Get or set `%s` of the room (`true` - hide email sender; `false` - show email sender)",
roomOptionNoSender,
config.RoomNoSender,
),
sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner,
},
{
key: roomOptionNoRecipient,
key: config.RoomNoRecipient,
description: fmt.Sprintf(
"Get or set `%s` of the room (`true` - hide recipient; `false` - show recipient)",
roomOptionNoRecipient,
config.RoomNoRecipient,
),
sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner,
},
{
key: roomOptionNoCC,
key: config.RoomNoCC,
description: fmt.Sprintf(
"Get or set `%s` of the room (`true` - hide CC; `false` - show CC)",
roomOptionNoCC,
config.RoomNoCC,
),
sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner,
},
{
key: roomOptionNoSubject,
key: config.RoomNoSubject,
description: fmt.Sprintf(
"Get or set `%s` of the room (`true` - hide email subject; `false` - show email subject)",
roomOptionNoSubject,
config.RoomNoSubject,
),
sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner,
},
{
key: roomOptionNoHTML,
key: config.RoomNoHTML,
description: fmt.Sprintf(
"Get or set `%s` of the room (`true` - ignore HTML in email; `false` - parse HTML in emails)",
roomOptionNoHTML,
config.RoomNoHTML,
),
sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner,
},
{
key: roomOptionNoThreads,
key: config.RoomNoThreads,
description: fmt.Sprintf(
"Get or set `%s` of the room (`true` - ignore email threads; `false` - convert email threads into matrix threads)",
roomOptionNoThreads,
config.RoomNoThreads,
),
sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner,
},
{
key: roomOptionNoFiles,
key: config.RoomNoFiles,
description: fmt.Sprintf(
"Get or set `%s` of the room (`true` - ignore email attachments; `false` - upload email attachments)",
roomOptionNoFiles,
config.RoomNoFiles,
),
sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner,
},
{allowed: b.allowOwner, description: "mailbox antispam"}, // delimiter
{
key: roomOptionSpamcheckMX,
key: config.RoomSpamcheckMX,
description: "only accept email from servers which seem prepared to receive it (those having valid MX records) (`true` - enable, `false` - disable)",
sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner,
},
{
key: roomOptionSpamcheckSPF,
key: config.RoomSpamcheckSPF,
description: "only accept email from senders which authorized to send it (those matching SPF records) (`true` - enable, `false` - disable)",
sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner,
},
{
key: roomOptionSpamcheckDKIM,
key: config.RoomSpamcheckDKIM,
description: "only accept correctly authorized emails (without DKIM signature at all or with valid DKIM signature) (`true` - enable, `false` - disable)",
sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner,
},
{
key: roomOptionSpamcheckSMTP,
key: config.RoomSpamcheckSMTP,
description: "only accept email from servers which seem prepared to receive it (those listening on an SMTP port) (`true` - enable, `false` - disable)",
sanitizer: utils.SanitizeBoolString,
allowed: b.allowOwner,
},
{
key: roomOptionSpamlist,
key: config.RoomSpamlist,
description: fmt.Sprintf(
"Get or set `%s` of the room (comma-separated list), eg: `spammer@example.com,*@spammer.org,spam@*`",
roomOptionSpamlist,
config.RoomSpamlist,
),
sanitizer: utils.SanitizeStringSlice,
allowed: b.allowOwner,
},
{allowed: b.allowAdmin, description: "server options"}, // delimiter
{
key: botOptionAdminRoom,
key: config.BotAdminRoom,
description: "Get or set admin room",
allowed: b.allowAdmin,
},
{
key: botOptionUsers,
key: config.BotUsers,
description: "Get or set allowed users",
allowed: b.allowAdmin,
},
@@ -245,7 +245,7 @@ func (b *Bot) initCommands() commandList {
},
{allowed: b.allowAdmin, description: "server antispam"}, // delimiter
{
key: botOptionGreylist,
key: config.BotGreylist,
description: "Set automatic greylisting duration in minutes (0 - disabled)",
allowed: b.allowAdmin,
},
@@ -272,12 +272,32 @@ func (b *Bot) initCommands() commandList {
}
}
func (b *Bot) handleCommand(ctx context.Context, evt *event.Event, commandSlice []string) {
func (b *Bot) handle(ctx context.Context) {
evt := eventFromContext(ctx)
err := b.lp.GetClient().MarkRead(evt.RoomID, evt.ID)
if err != nil {
b.log.Error("cannot send read receipt: %v", err)
}
content := evt.Content.AsMessage()
if content == nil {
b.Error(ctx, evt.RoomID, "cannot read message")
return
}
message := strings.TrimSpace(content.Body)
commandSlice := b.parseCommand(message, true)
if commandSlice == nil {
if utils.EventParent("", content) != "" {
b.SendEmailReply(ctx)
}
return
}
cmd := b.commands.get(commandSlice[0])
if cmd == nil {
return
}
_, err := b.lp.GetClient().UserTyping(evt.RoomID, true, 30*time.Second)
_, err = b.lp.GetClient().UserTyping(evt.RoomID, true, 30*time.Second)
if err != nil {
b.log.Error("cannot send typing notification: %v", err)
}
@@ -297,7 +317,7 @@ func (b *Bot) handleCommand(ctx context.Context, evt *event.Event, commandSlice
b.runSend(ctx)
case commandDKIM:
b.runDKIM(ctx, commandSlice)
case botOptionAdminRoom:
case config.BotAdminRoom:
b.runAdminRoom(ctx, commandSlice)
case commandUsers:
b.runUsers(ctx, commandSlice)
@@ -305,7 +325,7 @@ func (b *Bot) handleCommand(ctx context.Context, evt *event.Event, commandSlice
b.runCatchAll(ctx, commandSlice)
case commandDelete:
b.runDelete(ctx, commandSlice)
case botOptionGreylist:
case config.BotGreylist:
b.runGreylist(ctx, commandSlice)
case commandBanlist:
b.runBanlist(ctx, commandSlice)
@@ -348,7 +368,7 @@ func (b *Bot) sendIntroduction(ctx context.Context, roomID id.RoomID) {
msg.WriteString("To get started, assign an email address to this room by sending a `")
msg.WriteString(b.prefix)
msg.WriteString(" ")
msg.WriteString(roomOptionMailbox)
msg.WriteString(config.RoomMailbox)
msg.WriteString(" SOME_INBOX` command.\n")
msg.WriteString("You will then be able to send emails to ")
@@ -361,7 +381,7 @@ func (b *Bot) sendIntroduction(ctx context.Context, roomID id.RoomID) {
func (b *Bot) sendHelp(ctx context.Context) {
evt := eventFromContext(ctx)
cfg, serr := b.getRoomSettings(evt.RoomID)
cfg, serr := b.cfg.GetRoom(evt.RoomID)
if serr != nil {
b.log.Error("cannot retrieve settings: %v", serr)
}
@@ -392,7 +412,7 @@ func (b *Bot) sendHelp(ctx context.Context) {
case true:
msg.WriteString("(currently `")
msg.WriteString(value)
if cmd.key == roomOptionMailbox {
if cmd.key == config.RoomMailbox {
msg.WriteString(" (")
msg.WriteString(utils.EmailsList(value, cfg.Domain()))
msg.WriteString(")")
@@ -432,7 +452,7 @@ func (b *Bot) runSend(ctx context.Context) {
return
}
cfg, err := b.getRoomSettings(evt.RoomID)
cfg, err := b.cfg.GetRoom(evt.RoomID)
if err != nil {
b.Error(ctx, evt.RoomID, "failed to retrieve room settings: %v", err)
return
@@ -458,8 +478,8 @@ func (b *Bot) runSend(ctx context.Context) {
}
}
b.lock(evt.RoomID.String())
defer b.unlock(evt.RoomID.String())
b.mu.Lock(evt.RoomID.String())
defer b.mu.Unlock(evt.RoomID.String())
domain := utils.SanitizeDomain(cfg.Domain())
from := mailbox + "@" + domain
@@ -467,7 +487,7 @@ func (b *Bot) runSend(ctx context.Context) {
for _, to := range tos {
recipients := []string{to}
eml := email.New(ID, "", " "+ID, subject, from, to, to, "", body, htmlBody, nil)
data := eml.Compose(b.getBotSettings().DKIMPrivateKey())
data := eml.Compose(b.cfg.GetBot().DKIMPrivateKey())
if data == "" {
b.SendError(ctx, evt.RoomID, "email body is empty")
return
@@ -475,14 +495,14 @@ func (b *Bot) runSend(ctx context.Context) {
queued, err := b.Sendmail(evt.ID, from, to, data)
if queued {
b.log.Error("cannot send email: %v", err)
b.saveSentMetadata(ctx, queued, evt.ID, recipients, eml, &cfg)
b.saveSentMetadata(ctx, queued, evt.ID, recipients, eml, cfg)
continue
}
if err != nil {
b.Error(ctx, evt.RoomID, "cannot send email to %s: %v", to, err)
continue
}
b.saveSentMetadata(ctx, false, evt.ID, recipients, eml, &cfg)
b.saveSentMetadata(ctx, false, evt.ID, recipients, eml, cfg)
}
if len(tos) > 1 {
b.SendNotice(ctx, evt.RoomID, "All emails were sent.")

View File

@@ -11,12 +11,13 @@ import (
"gitlab.com/etke.cc/go/secgen"
"maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/bot/config"
"gitlab.com/etke.cc/postmoogle/utils"
)
func (b *Bot) sendMailboxes(ctx context.Context) {
evt := eventFromContext(ctx)
mailboxes := map[string]roomSettings{}
mailboxes := map[string]config.Room{}
slice := []string{}
b.rooms.Range(func(key any, value any) bool {
if key == nil {
@@ -34,7 +35,7 @@ func (b *Bot) sendMailboxes(ctx context.Context) {
if !ok {
return true
}
config, err := b.getRoomSettings(roomID)
config, err := b.cfg.GetRoom(roomID)
if err != nil {
b.log.Error("cannot retrieve settings: %v", err)
}
@@ -80,7 +81,7 @@ func (b *Bot) runDelete(ctx context.Context, commandSlice []string) {
roomID := v.(id.RoomID)
b.rooms.Delete(mailbox)
err := b.setRoomSettings(roomID, roomSettings{})
err := b.cfg.SetRoom(roomID, config.Room{})
if err != nil {
b.Error(ctx, evt.RoomID, "cannot update settings: %v", err)
return
@@ -91,7 +92,7 @@ func (b *Bot) runDelete(ctx context.Context, commandSlice []string) {
func (b *Bot) runUsers(ctx context.Context, commandSlice []string) {
evt := eventFromContext(ctx)
cfg := b.getBotSettings()
cfg := b.cfg.GetBot()
if len(commandSlice) < 2 {
var msg strings.Builder
users := cfg.Users()
@@ -122,9 +123,9 @@ func (b *Bot) runUsers(ctx context.Context, commandSlice []string) {
return
}
cfg.Set(botOptionUsers, strings.Join(patterns, " "))
cfg.Set(config.BotUsers, strings.Join(patterns, " "))
err = b.setBotSettings(cfg)
err = b.cfg.SetBot(cfg)
if err != nil {
b.Error(ctx, evt.RoomID, "cannot set bot config: %v", err)
}
@@ -134,10 +135,10 @@ func (b *Bot) runUsers(ctx context.Context, commandSlice []string) {
func (b *Bot) runDKIM(ctx context.Context, commandSlice []string) {
evt := eventFromContext(ctx)
cfg := b.getBotSettings()
cfg := b.cfg.GetBot()
if len(commandSlice) > 1 && commandSlice[1] == "reset" {
cfg.Set(botOptionDKIMPrivateKey, "")
cfg.Set(botOptionDKIMSignature, "")
cfg.Set(config.BotDKIMPrivateKey, "")
cfg.Set(config.BotDKIMSignature, "")
}
signature := cfg.DKIMSignature()
@@ -149,9 +150,9 @@ func (b *Bot) runDKIM(ctx context.Context, commandSlice []string) {
b.Error(ctx, evt.RoomID, "cannot generate DKIM signature: %v", derr)
return
}
cfg.Set(botOptionDKIMSignature, signature)
cfg.Set(botOptionDKIMPrivateKey, private)
err := b.setBotSettings(cfg)
cfg.Set(config.BotDKIMSignature, signature)
cfg.Set(config.BotDKIMPrivateKey, private)
err := b.cfg.SetBot(cfg)
if err != nil {
b.Error(ctx, evt.RoomID, "cannot save bot options: %v", err)
return
@@ -169,7 +170,7 @@ func (b *Bot) runDKIM(ctx context.Context, commandSlice []string) {
func (b *Bot) runCatchAll(ctx context.Context, commandSlice []string) {
evt := eventFromContext(ctx)
cfg := b.getBotSettings()
cfg := b.cfg.GetBot()
if len(commandSlice) < 2 {
var msg strings.Builder
msg.WriteString("Currently: `")
@@ -198,8 +199,8 @@ func (b *Bot) runCatchAll(ctx context.Context, commandSlice []string) {
return
}
cfg.Set(botOptionCatchAll, mailbox)
err := b.setBotSettings(cfg)
cfg.Set(config.BotCatchAll, mailbox)
err := b.cfg.SetBot(cfg)
if err != nil {
b.Error(ctx, evt.RoomID, "cannot save bot options: %v", err)
return
@@ -210,7 +211,7 @@ func (b *Bot) runCatchAll(ctx context.Context, commandSlice []string) {
func (b *Bot) runAdminRoom(ctx context.Context, commandSlice []string) {
evt := eventFromContext(ctx)
cfg := b.getBotSettings()
cfg := b.cfg.GetBot()
if len(commandSlice) < 2 {
var msg strings.Builder
msg.WriteString("Currently: `")
@@ -230,8 +231,8 @@ func (b *Bot) runAdminRoom(ctx context.Context, commandSlice []string) {
}
roomID := b.parseCommand(evt.Content.AsMessage().Body, false)[1] // get original value, without forced lower case
cfg.Set(botOptionAdminRoom, roomID)
err := b.setBotSettings(cfg)
cfg.Set(config.BotAdminRoom, roomID)
err := b.cfg.SetBot(cfg)
if err != nil {
b.Error(ctx, evt.RoomID, "cannot save bot options: %v", err)
return
@@ -243,8 +244,8 @@ func (b *Bot) runAdminRoom(ctx context.Context, commandSlice []string) {
}
func (b *Bot) printGreylist(ctx context.Context, roomID id.RoomID) {
cfg := b.getBotSettings()
greylist := b.getGreylist()
cfg := b.cfg.GetBot()
greylist := b.cfg.GetGreylist()
var msg strings.Builder
size := len(greylist)
duration := cfg.Greylist()
@@ -252,7 +253,7 @@ func (b *Bot) printGreylist(ctx context.Context, roomID id.RoomID) {
if duration == 0 {
msg.WriteString("disabled")
} else {
msg.WriteString(cfg.Get(botOptionGreylist))
msg.WriteString(cfg.Get(config.BotGreylist))
msg.WriteString("min")
}
msg.WriteString("`")
@@ -279,10 +280,10 @@ func (b *Bot) runGreylist(ctx context.Context, commandSlice []string) {
b.printGreylist(ctx, evt.RoomID)
return
}
cfg := b.getBotSettings()
cfg := b.cfg.GetBot()
value := utils.SanitizeIntString(commandSlice[1])
cfg.Set(botOptionGreylist, value)
err := b.setBotSettings(cfg)
cfg.Set(config.BotGreylist, value)
err := b.cfg.SetBot(cfg)
if err != nil {
b.Error(ctx, evt.RoomID, "cannot set bot config: %v", err)
}
@@ -291,14 +292,14 @@ func (b *Bot) runGreylist(ctx context.Context, commandSlice []string) {
func (b *Bot) runBanlist(ctx context.Context, commandSlice []string) {
evt := eventFromContext(ctx)
cfg := b.getBotSettings()
cfg := b.cfg.GetBot()
if len(commandSlice) < 2 {
banlist := b.getBanlist()
banlist := b.cfg.GetBanlist()
var msg strings.Builder
size := len(banlist)
if size > 0 {
msg.WriteString("Currently: `")
msg.WriteString(cfg.Get(botOptionBanlistEnabled))
msg.WriteString(cfg.Get(config.BotBanlistEnabled))
msg.WriteString("`, total: ")
msg.WriteString(strconv.Itoa(size))
msg.WriteString(" hosts (`")
@@ -319,12 +320,11 @@ func (b *Bot) runBanlist(ctx context.Context, commandSlice []string) {
return
}
value := utils.SanitizeBoolString(commandSlice[1])
cfg.Set(botOptionBanlistEnabled, value)
err := b.setBotSettings(cfg)
cfg.Set(config.BotBanlistEnabled, value)
err := b.cfg.SetBot(cfg)
if err != nil {
b.Error(ctx, evt.RoomID, "cannot set bot config: %v", err)
}
b.syncBanlist()
b.SendNotice(ctx, evt.RoomID, "banlist has been updated")
}
@@ -334,7 +334,7 @@ func (b *Bot) runBanlistAdd(ctx context.Context, commandSlice []string) {
b.runBanlist(ctx, commandSlice)
return
}
banlist := b.getBanlist()
banlist := b.cfg.GetBanlist()
ips := commandSlice[1:]
for _, ip := range ips {
@@ -346,7 +346,7 @@ func (b *Bot) runBanlistAdd(ctx context.Context, commandSlice []string) {
banlist.Add(addr)
}
err := b.setBanlist(banlist)
err := b.cfg.SetBanlist(banlist)
if err != nil {
b.Error(ctx, evt.RoomID, "cannot set banlist: %v", err)
return
@@ -361,7 +361,7 @@ func (b *Bot) runBanlistRemove(ctx context.Context, commandSlice []string) {
b.runBanlist(ctx, commandSlice)
return
}
banlist := b.getBanlist()
banlist := b.cfg.GetBanlist()
ips := commandSlice[1:]
for _, ip := range ips {
@@ -373,7 +373,7 @@ func (b *Bot) runBanlistRemove(ctx context.Context, commandSlice []string) {
banlist.Remove(addr)
}
err := b.setBanlist(banlist)
err := b.cfg.SetBanlist(banlist)
if err != nil {
b.Error(ctx, evt.RoomID, "cannot set banlist: %v", err)
return
@@ -385,7 +385,7 @@ func (b *Bot) runBanlistRemove(ctx context.Context, commandSlice []string) {
func (b *Bot) runBanlistReset(ctx context.Context) {
evt := eventFromContext(ctx)
err := b.setBanlist(bglist{})
err := b.cfg.SetBanlist(config.List{})
if err != nil {
b.Error(ctx, evt.RoomID, "cannot set banlist: %v", err)
return

View File

@@ -7,18 +7,19 @@ import (
"github.com/raja/argon2pw"
"gitlab.com/etke.cc/postmoogle/bot/config"
"gitlab.com/etke.cc/postmoogle/utils"
)
func (b *Bot) runStop(ctx context.Context) {
evt := eventFromContext(ctx)
cfg, err := b.getRoomSettings(evt.RoomID)
cfg, err := b.cfg.GetRoom(evt.RoomID)
if err != nil {
b.Error(ctx, evt.RoomID, "failed to retrieve settings: %v", err)
return
}
mailbox := cfg.Get(roomOptionMailbox)
mailbox := cfg.Get(config.RoomMailbox)
if mailbox == "" {
b.SendNotice(ctx, evt.RoomID, "that room is not configured yet")
return
@@ -26,7 +27,7 @@ func (b *Bot) runStop(ctx context.Context) {
b.rooms.Delete(mailbox)
err = b.setRoomSettings(evt.RoomID, roomSettings{})
err = b.cfg.SetRoom(evt.RoomID, config.Room{})
if err != nil {
b.Error(ctx, evt.RoomID, "cannot update settings: %v", err)
return
@@ -45,7 +46,7 @@ func (b *Bot) handleOption(ctx context.Context, cmd []string) {
func (b *Bot) getOption(ctx context.Context, name string) {
evt := eventFromContext(ctx)
cfg, err := b.getRoomSettings(evt.RoomID)
cfg, err := b.cfg.GetRoom(evt.RoomID)
if err != nil {
b.Error(ctx, evt.RoomID, "failed to retrieve settings: %v", err)
return
@@ -60,14 +61,14 @@ func (b *Bot) getOption(ctx context.Context, name string) {
return
}
if name == roomOptionMailbox {
if name == config.RoomMailbox {
value = utils.EmailsList(value, cfg.Domain())
}
msg := fmt.Sprintf("`%s` of this room is `%s`\n"+
"To set it to a new value, send a `%s %s VALUE` command.",
name, value, b.prefix, name)
if name == roomOptionPassword {
if name == config.RoomPassword {
msg = fmt.Sprintf("There is an SMTP password already set for this room/mailbox. "+
"It's stored in a secure hashed manner, so we can't tell you what the original raw password was. "+
"To find the raw password, try to find your old message which had originally set it, "+
@@ -86,10 +87,10 @@ func (b *Bot) setOption(ctx context.Context, name, value string) {
evt := eventFromContext(ctx)
// ignore request
if name == roomOptionActive {
if name == config.RoomActive {
return
}
if name == roomOptionMailbox {
if name == config.RoomMailbox {
existingID, ok := b.getMapping(value)
if (ok && existingID != "" && existingID != evt.RoomID) || b.isReserved(value) {
b.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Mailbox `%s` (%s) already taken, kupo", value, utils.EmailsList(value, "")))
@@ -97,13 +98,13 @@ func (b *Bot) setOption(ctx context.Context, name, value string) {
}
}
cfg, err := b.getRoomSettings(evt.RoomID)
cfg, err := b.cfg.GetRoom(evt.RoomID)
if err != nil {
b.Error(ctx, evt.RoomID, "failed to retrieve settings: %v", err)
return
}
if name == roomOptionPassword {
if name == config.RoomPassword {
value = b.parseCommand(evt.Content.AsMessage().Body, false)[1] // get original value, without forced lower case
value, err = argon2pw.GenerateSaltedHash(value)
if err != nil {
@@ -115,24 +116,24 @@ func (b *Bot) setOption(ctx context.Context, name, value string) {
old := cfg.Get(name)
cfg.Set(name, value)
if name == roomOptionMailbox {
cfg.Set(roomOptionOwner, evt.Sender.String())
if name == config.RoomMailbox {
cfg.Set(config.RoomOwner, evt.Sender.String())
if old != "" {
b.rooms.Delete(old)
}
active := b.ActivateMailbox(evt.Sender, evt.RoomID, value)
cfg.Set(roomOptionActive, strconv.FormatBool(active))
cfg.Set(config.RoomActive, strconv.FormatBool(active))
value = fmt.Sprintf("%s@%s", value, utils.SanitizeDomain(cfg.Domain()))
}
err = b.setRoomSettings(evt.RoomID, cfg)
err = b.cfg.SetRoom(evt.RoomID, cfg)
if err != nil {
b.Error(ctx, evt.RoomID, "cannot update settings: %v", err)
return
}
msg := fmt.Sprintf("`%s` of this room set to `%s`", name, value)
if name == roomOptionPassword {
if name == config.RoomPassword {
msg = "SMTP password has been set"
}
b.SendNotice(ctx, evt.RoomID, msg)

92
bot/config/bot.go Normal file
View File

@@ -0,0 +1,92 @@
package config
import (
"strings"
"maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/utils"
)
// account data key
const acBotKey = "cc.etke.postmoogle.config"
// bot options keys
const (
BotAdminRoom = "adminroom"
BotUsers = "users"
BotCatchAll = "catch-all"
BotDKIMSignature = "dkim.pub"
BotDKIMPrivateKey = "dkim.pem"
BotQueueBatch = "queue:batch"
BotQueueRetries = "queue:retries"
BotBanlistEnabled = "banlist:enabled"
BotGreylist = "greylist"
)
// Bot map
type Bot map[string]string
// Get option
func (s Bot) Get(key string) string {
return s[strings.ToLower(strings.TrimSpace(key))]
}
// Set option
func (s Bot) Set(key, value string) {
s[strings.ToLower(strings.TrimSpace(key))] = value
}
// Users option
func (s Bot) Users() []string {
value := s.Get(BotUsers)
if value == "" {
return []string{}
}
if strings.Contains(value, " ") {
return strings.Split(value, " ")
}
return []string{value}
}
// CatchAll option
func (s Bot) CatchAll() string {
return s.Get(BotCatchAll)
}
// AdminRoom option
func (s Bot) AdminRoom() id.RoomID {
return id.RoomID(s.Get(BotAdminRoom))
}
// BanlistEnabled option
func (s Bot) BanlistEnabled() bool {
return utils.Bool(s.Get(BotBanlistEnabled))
}
// Greylist option (duration in minutes)
func (s Bot) Greylist() int {
return utils.Int(s.Get(BotGreylist))
}
// DKIMSignature (DNS TXT record)
func (s Bot) DKIMSignature() string {
return s.Get(BotDKIMSignature)
}
// DKIMPrivateKey keep it secret
func (s Bot) DKIMPrivateKey() string {
return s.Get(BotDKIMPrivateKey)
}
// QueueBatch option
func (s Bot) QueueBatch() int {
return utils.Int(s.Get(BotQueueBatch))
}
// QueueRetries option
func (s Bot) QueueRetries() int {
return utils.Int(s.Get(BotQueueRetries))
}

76
bot/config/lists.go Normal file
View File

@@ -0,0 +1,76 @@
package config
import (
"net"
"sort"
"time"
)
// account data keys
const (
acBanlistKey = "cc.etke.postmoogle.banlist"
acGreylistKey = "cc.etke.postmoogle.greylist"
)
// List config
type List map[string]string
// Slice returns slice of ban- or greylist items
func (l List) Slice() []string {
slice := make([]string, 0, len(l))
for item := range l {
slice = append(slice, item)
}
sort.Strings(slice)
return slice
}
func (l List) 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 (l List) Has(addr net.Addr) bool {
_, ok := l[l.getKey(addr)]
return ok
}
// Get when addr was added in ban- or greylist
func (l List) Get(addr net.Addr) (time.Time, bool) {
from := l[l.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 (l List) Add(addr net.Addr) {
key := l.getKey(addr)
if _, ok := l[key]; ok {
return
}
l[key] = time.Now().UTC().Format(time.RFC1123Z)
}
// Remove an addr from ban- or greylist
func (l List) Remove(addr net.Addr) {
key := l.getKey(addr)
if _, ok := l[key]; !ok {
return
}
delete(l, key)
}

120
bot/config/manager.go Normal file
View File

@@ -0,0 +1,120 @@
package config
import (
"fmt"
"gitlab.com/etke.cc/go/logger"
"gitlab.com/etke.cc/linkpearl"
"maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/utils"
)
// Manager of configs
type Manager struct {
bl List
ble bool
mu utils.Mutex
log *logger.Logger
lp *linkpearl.Linkpearl
}
// New config manager
func New(lp *linkpearl.Linkpearl, log *logger.Logger) *Manager {
m := &Manager{
mu: utils.NewMutex(),
bl: make(List, 0),
lp: lp,
log: log,
}
m.ble = m.GetBot().BanlistEnabled()
return m
}
// GetBot config
func (m *Manager) GetBot() Bot {
config, err := m.lp.GetAccountData(acBotKey)
if err != nil {
m.log.Error("cannot get bot settings: %v", utils.UnwrapError(err))
}
if config == nil {
config = make(Bot, 0)
}
return config
}
// SetBot config
func (m *Manager) SetBot(cfg Bot) error {
m.ble = cfg.BanlistEnabled()
return utils.UnwrapError(m.lp.SetAccountData(acBotKey, cfg))
}
// GetRoom config
func (m *Manager) GetRoom(roomID id.RoomID) (Room, error) {
config, err := m.lp.GetRoomAccountData(roomID, acRoomKey)
if config == nil {
config = make(Room, 0)
}
return config, utils.UnwrapError(err)
}
// SetRoom config
func (m *Manager) SetRoom(roomID id.RoomID, cfg Room) error {
return utils.UnwrapError(m.lp.SetRoomAccountData(roomID, acRoomKey, cfg))
}
// GetBanlist config
func (m *Manager) GetBanlist() List {
if len(m.bl) > 0 || !m.ble {
return m.bl
}
m.mu.Lock("banlist")
defer m.mu.Unlock("banlist")
config, err := m.lp.GetAccountData(acBanlistKey)
if err != nil {
m.log.Error("cannot get banlist: %v", utils.UnwrapError(err))
}
if config == nil {
config = make(List, 0)
}
m.bl = config
return config
}
// SetBanlist config
func (m *Manager) SetBanlist(cfg List) error {
if !m.ble {
return fmt.Errorf("banlist is disabled, kupo")
}
m.mu.Lock("banlist")
if cfg == nil {
cfg = make(List, 0)
}
m.bl = cfg
defer m.mu.Unlock("banlist")
return utils.UnwrapError(m.lp.SetAccountData(acBanlistKey, cfg))
}
// GetGreylist config
func (m *Manager) GetGreylist() List {
config, err := m.lp.GetAccountData(acGreylistKey)
if err != nil {
m.log.Error("cannot get banlist: %v", utils.UnwrapError(err))
}
if config == nil {
config = make(List, 0)
}
return config
}
// SetGreylist config
func (m *Manager) SetGreylist(cfg List) error {
return utils.UnwrapError(m.lp.SetAccountData(acGreylistKey, cfg))
}

183
bot/config/room.go Normal file
View File

@@ -0,0 +1,183 @@
package config
import (
"strings"
"gitlab.com/etke.cc/postmoogle/email"
"gitlab.com/etke.cc/postmoogle/utils"
)
// account data key
const acRoomKey = "cc.etke.postmoogle.settings"
type Room map[string]string
// option keys
const (
RoomActive = ".active"
RoomOwner = "owner"
RoomMailbox = "mailbox"
RoomDomain = "domain"
RoomNoSend = "nosend"
RoomNoCC = "nocc"
RoomNoSender = "nosender"
RoomNoRecipient = "norecipient"
RoomNoSubject = "nosubject"
RoomNoHTML = "nohtml"
RoomNoThreads = "nothreads"
RoomNoFiles = "nofiles"
RoomPassword = "password"
RoomSpamcheckDKIM = "spamcheck:dkim"
RoomSpamcheckSMTP = "spamcheck:smtp"
RoomSpamcheckSPF = "spamcheck:spf"
RoomSpamcheckMX = "spamcheck:mx"
RoomSpamlist = "spamlist"
)
// Get option
func (s Room) Get(key string) string {
return s[strings.ToLower(strings.TrimSpace(key))]
}
// Set option
func (s Room) Set(key, value string) {
s[strings.ToLower(strings.TrimSpace(key))] = value
}
func (s Room) Mailbox() string {
return s.Get(RoomMailbox)
}
func (s Room) Domain() string {
return s.Get(RoomDomain)
}
func (s Room) Owner() string {
return s.Get(RoomOwner)
}
func (s Room) Active() bool {
return utils.Bool(s.Get(RoomActive))
}
func (s Room) Password() string {
return s.Get(RoomPassword)
}
func (s Room) NoSend() bool {
return utils.Bool(s.Get(RoomNoSend))
}
func (s Room) NoCC() bool {
return utils.Bool(s.Get(RoomNoCC))
}
func (s Room) NoSender() bool {
return utils.Bool(s.Get(RoomNoSender))
}
func (s Room) NoRecipient() bool {
return utils.Bool(s.Get(RoomNoRecipient))
}
func (s Room) NoSubject() bool {
return utils.Bool(s.Get(RoomNoSubject))
}
func (s Room) NoHTML() bool {
return utils.Bool(s.Get(RoomNoHTML))
}
func (s Room) NoThreads() bool {
return utils.Bool(s.Get(RoomNoThreads))
}
func (s Room) NoFiles() bool {
return utils.Bool(s.Get(RoomNoFiles))
}
func (s Room) SpamcheckDKIM() bool {
return utils.Bool(s.Get(RoomSpamcheckDKIM))
}
func (s Room) SpamcheckSMTP() bool {
return utils.Bool(s.Get(RoomSpamcheckSMTP))
}
func (s Room) SpamcheckSPF() bool {
return utils.Bool(s.Get(RoomSpamcheckSPF))
}
func (s Room) SpamcheckMX() bool {
return utils.Bool(s.Get(RoomSpamcheckMX))
}
func (s Room) Spamlist() []string {
return utils.StringSlice(s.Get(RoomSpamlist))
}
func (s Room) MigrateSpamlistSettings() {
uniq := map[string]struct{}{}
emails := utils.StringSlice(s.Get("spamlist:emails"))
localparts := utils.StringSlice(s.Get("spamlist:localparts"))
hosts := utils.StringSlice(s.Get("spamlist:hosts"))
list := utils.StringSlice(s.Get(RoomSpamlist))
delete(s, "spamlist:emails")
delete(s, "spamlist:localparts")
delete(s, "spamlist:hosts")
for _, email := range emails {
if email == "" {
continue
}
uniq[email] = struct{}{}
}
for _, localpart := range localparts {
if localpart == "" {
continue
}
uniq[localpart+"@*"] = struct{}{}
}
for _, host := range hosts {
if host == "" {
continue
}
uniq["*@"+host] = struct{}{}
}
for _, item := range list {
if item == "" {
continue
}
uniq[item] = struct{}{}
}
spamlist := make([]string, 0, len(uniq))
for item := range uniq {
spamlist = append(spamlist, item)
}
s.Set(RoomSpamlist, strings.Join(spamlist, ","))
}
// ContentOptions converts room display settings to content options
func (s Room) ContentOptions() *email.ContentOptions {
return &email.ContentOptions{
CC: !s.NoCC(),
HTML: !s.NoHTML(),
Sender: !s.NoSender(),
Recipient: !s.NoRecipient(),
Subject: !s.NoSubject(),
Threads: !s.NoThreads(),
ToKey: "cc.etke.postmoogle.to",
CcKey: "cc.etke.postmoogle.cc",
FromKey: "cc.etke.postmoogle.from",
RcptToKey: "cc.etke.postmoogle.rcptTo",
SubjectKey: "cc.etke.postmoogle.subject",
InReplyToKey: "cc.etke.postmoogle.inReplyTo",
MessageIDKey: "cc.etke.postmoogle.messageID",
ReferencesKey: "cc.etke.postmoogle.references",
}
}

View File

@@ -1,6 +1,10 @@
package bot
import "maunium.net/go/mautrix/id"
import (
"maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/bot/config"
)
var migrations = []string{}
@@ -34,7 +38,7 @@ func (b *Bot) migrate() error {
}
func (b *Bot) syncRooms() error {
adminRoom := b.getBotSettings().AdminRoom()
adminRoom := b.cfg.GetBot().AdminRoom()
if adminRoom != "" {
b.adminRooms = append(b.adminRooms, adminRoom)
}
@@ -45,7 +49,7 @@ func (b *Bot) syncRooms() error {
}
for _, roomID := range resp.JoinedRooms {
b.migrateRoomSettings(roomID)
cfg, serr := b.getRoomSettings(roomID)
cfg, serr := b.cfg.GetRoom(roomID)
if serr != nil {
continue
}
@@ -63,13 +67,37 @@ func (b *Bot) syncRooms() error {
return nil
}
func (b *Bot) syncBanlist() {
b.lock("banlist")
defer b.unlock("banlist")
if !b.getBotSettings().BanlistEnabled() {
b.banlist = make(bglist, 0)
func (b *Bot) migrateRoomSettings(roomID id.RoomID) {
cfg, err := b.cfg.GetRoom(roomID)
if err != nil {
b.log.Error("cannot retrieve room settings: %v", err)
return
}
b.banlist = b.getBanlist()
if _, ok := cfg[config.RoomActive]; !ok {
cfg.Set(config.RoomActive, "true")
}
if cfg["spamlist:emails"] == "" && cfg["spamlist:localparts"] == "" && cfg["spamlist:hosts"] == "" {
return
}
cfg.MigrateSpamlistSettings()
err = b.cfg.SetRoom(roomID, cfg)
if err != nil {
b.log.Error("cannot migrate room settings: %v", err)
}
}
func (b *Bot) initBotUsers() ([]string, error) {
cfg := b.cfg.GetBot()
cfgUsers := cfg.Users()
if len(cfgUsers) > 0 {
return cfgUsers, nil
}
_, homeserver, err := b.lp.GetClient().UserID.Parse()
if err != nil {
return nil, err
}
cfg.Set(config.BotUsers, "@*:"+homeserver)
return cfg.Users(), b.cfg.SetBot(cfg)
}

View File

@@ -9,13 +9,13 @@ import (
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/bot/config"
"gitlab.com/etke.cc/postmoogle/email"
"gitlab.com/etke.cc/postmoogle/utils"
)
// account data keys
const (
acQueueKey = "cc.etke.postmoogle.mailqueue"
acMessagePrefix = "cc.etke.postmoogle.message"
acLastEventPrefix = "cc.etke.postmoogle.last"
)
@@ -35,6 +35,7 @@ const (
// SetSendmail sets mail sending func to the bot
func (b *Bot) SetSendmail(sendmail func(string, string, string) error) {
b.sendmail = sendmail
b.q.SetSendmail(sendmail)
}
// Sendmail tries to send email immediately, but if it gets 4xx error (greylisting),
@@ -44,7 +45,7 @@ func (b *Bot) Sendmail(eventID id.EventID, from, to, data string) (bool, error)
if err != nil {
if strings.HasPrefix(err.Error(), "4") {
b.log.Debug("email %s (from=%s to=%s) was added to the queue: %v", eventID, from, to, err)
return true, b.enqueueEmail(eventID.String(), from, to, data)
return true, b.q.Add(eventID.String(), from, to, data)
}
return false, err
}
@@ -54,7 +55,7 @@ func (b *Bot) Sendmail(eventID id.EventID, from, to, data string) (bool, error)
// GetDKIMprivkey returns DKIM private key
func (b *Bot) GetDKIMprivkey() string {
return b.getBotSettings().DKIMPrivateKey()
return b.cfg.GetBot().DKIMPrivateKey()
}
func (b *Bot) getMapping(mailbox string) (id.RoomID, bool) {
@@ -75,7 +76,7 @@ func (b *Bot) getMapping(mailbox string) (id.RoomID, bool) {
func (b *Bot) GetMapping(mailbox string) (id.RoomID, bool) {
roomID, ok := b.getMapping(mailbox)
if !ok {
catchAll := b.getBotSettings().CatchAll()
catchAll := b.cfg.GetBot().CatchAll()
if catchAll == "" {
return roomID, ok
}
@@ -87,10 +88,9 @@ func (b *Bot) GetMapping(mailbox string) (id.RoomID, bool) {
// GetIFOptions returns incoming email filtering options (room settings)
func (b *Bot) GetIFOptions(roomID id.RoomID) email.IncomingFilteringOptions {
cfg, err := b.getRoomSettings(roomID)
cfg, err := b.cfg.GetRoom(roomID)
if err != nil {
b.log.Error("cannot retrieve room settings: %v", err)
return roomSettings{}
}
return cfg
@@ -102,13 +102,13 @@ func (b *Bot) IncomingEmail(ctx context.Context, email *email.Email) error {
if !ok {
return errors.New("room not found")
}
cfg, err := b.getRoomSettings(roomID)
cfg, err := b.cfg.GetRoom(roomID)
if err != nil {
b.Error(ctx, roomID, "cannot get settings: %v", err)
}
b.lock(roomID.String())
defer b.unlock(roomID.String())
b.mu.Lock(roomID.String())
defer b.mu.Unlock(roomID.String())
var threadID id.EventID
if email.InReplyTo != "" || email.References != "" {
@@ -143,7 +143,7 @@ func (b *Bot) SendEmailReply(ctx context.Context) {
if !b.allowSend(evt.Sender, evt.RoomID) {
return
}
cfg, err := b.getRoomSettings(evt.RoomID)
cfg, err := b.cfg.GetRoom(evt.RoomID)
if err != nil {
b.Error(ctx, evt.RoomID, "cannot retrieve room settings: %v", err)
return
@@ -154,8 +154,8 @@ func (b *Bot) SendEmailReply(ctx context.Context) {
return
}
b.lock(evt.RoomID.String())
defer b.unlock(evt.RoomID.String())
b.mu.Lock(evt.RoomID.String())
defer b.mu.Unlock(evt.RoomID.String())
meta := b.getParentEmail(evt, mailbox)
@@ -181,7 +181,7 @@ func (b *Bot) SendEmailReply(ctx context.Context) {
meta.References = meta.References + " " + meta.MessageID
b.log.Debug("send email reply: %+v", meta)
eml := email.New(meta.MessageID, meta.InReplyTo, meta.References, meta.Subject, meta.From, meta.To, meta.RcptTo, meta.CC, body, htmlBody, nil)
data := eml.Compose(b.getBotSettings().DKIMPrivateKey())
data := eml.Compose(b.cfg.GetBot().DKIMPrivateKey())
if data == "" {
b.SendError(ctx, evt.RoomID, "email body is empty")
return
@@ -194,7 +194,7 @@ func (b *Bot) SendEmailReply(ctx context.Context) {
queued, err = b.Sendmail(evt.ID, meta.From, to, data)
if queued {
b.log.Error("cannot send email: %v", err)
b.saveSentMetadata(ctx, queued, meta.ThreadID, recipients, eml, &cfg)
b.saveSentMetadata(ctx, queued, meta.ThreadID, recipients, eml, cfg)
hasErr = true
continue
}
@@ -207,7 +207,7 @@ func (b *Bot) SendEmailReply(ctx context.Context) {
}
if !hasErr {
b.saveSentMetadata(ctx, queued, meta.ThreadID, recipients, eml, &cfg)
b.saveSentMetadata(ctx, queued, meta.ThreadID, recipients, eml, cfg)
}
}
@@ -352,7 +352,7 @@ func (b *Bot) getParentEmail(evt *event.Event, newFromMailbox string) *parentEma
// saveSentMetadata used to save metadata from !pm sent and thread reply events to a separate notice message
// because that metadata is needed to determine email thread relations
func (b *Bot) saveSentMetadata(ctx context.Context, queued bool, threadID id.EventID, recipients []string, eml *email.Email, cfg *roomSettings) {
func (b *Bot) saveSentMetadata(ctx context.Context, queued bool, threadID id.EventID, recipients []string, eml *email.Email, cfg config.Room) {
addrs := strings.Join(recipients, ", ")
text := "Email has been sent to " + addrs
if queued {

View File

@@ -1,32 +0,0 @@
package bot
import (
"context"
"strings"
"gitlab.com/etke.cc/postmoogle/utils"
)
func (b *Bot) handle(ctx context.Context) {
evt := eventFromContext(ctx)
err := b.lp.GetClient().MarkRead(evt.RoomID, evt.ID)
if err != nil {
b.log.Error("cannot send read receipt: %v", err)
}
content := evt.Content.AsMessage()
if content == nil {
b.Error(ctx, evt.RoomID, "cannot read message")
return
}
message := strings.TrimSpace(content.Body)
cmd := b.parseCommand(message, true)
if cmd == nil {
if utils.EventParent("", content) != "" {
b.SendEmailReply(ctx)
}
return
}
b.handleCommand(ctx, evt, cmd)
}

View File

@@ -1,24 +0,0 @@
package bot
import (
"sync"
)
func (b *Bot) lock(key string) {
_, ok := b.mu[key]
if !ok {
b.mu[key] = &sync.Mutex{}
}
b.mu[key].Lock()
}
func (b *Bot) unlock(key string) {
_, ok := b.mu[key]
if !ok {
return
}
b.mu[key].Unlock()
delete(b.mu, key)
}

View File

@@ -1,153 +0,0 @@
package bot
import (
"strconv"
)
const (
defaultQueueBatch = 1
defaultQueueRetries = 3
)
// ProcessQueue starts queue processing
func (b *Bot) ProcessQueue() {
b.log.Debug("staring queue processing...")
cfg := b.getBotSettings()
batchSize := cfg.QueueBatch()
if batchSize == 0 {
batchSize = defaultQueueBatch
}
retries := cfg.QueueRetries()
if retries == 0 {
retries = defaultQueueRetries
}
b.popqueue(batchSize, retries)
b.log.Debug("ended queue processing")
}
// popqueue gets emails from queue and tries to send them,
// if an email was sent successfully - it will be removed from queue
func (b *Bot) popqueue(batchSize, maxTries int) {
b.lock(acQueueKey)
defer b.unlock(acQueueKey)
index, err := b.lp.GetAccountData(acQueueKey)
if err != nil {
b.log.Error("cannot get queue index: %v", err)
}
i := 0
for id, itemkey := range index {
if i > batchSize {
b.log.Debug("finished re-deliveries from queue")
return
}
if dequeue := b.processQueueItem(itemkey, maxTries); dequeue {
b.log.Debug("email %s has been delivered", id)
err = b.dequeueEmail(id)
if err != nil {
b.log.Error("cannot dequeue email %s: %v", id, err)
}
}
i++
}
}
// processQueueItem tries to process an item from queue
// returns should the item be dequeued or not
func (b *Bot) processQueueItem(itemkey string, maxRetries int) bool {
b.lock(itemkey)
defer b.unlock(itemkey)
item, err := b.lp.GetAccountData(itemkey)
if err != nil {
b.log.Error("cannot retrieve a queue item %s: %v", itemkey, err)
return false
}
b.log.Debug("processing queue item %+v", item)
attempts, err := strconv.Atoi(item["attempts"])
if err != nil {
b.log.Error("cannot parse attempts of %s: %v", itemkey, err)
return false
}
if attempts > maxRetries {
return true
}
err = b.sendmail(item["from"], item["to"], item["data"])
if err == nil {
b.log.Debug("email %s from queue was delivered")
return true
}
b.log.Debug("attempted to deliver email id=%s, retry=%s, but it's not ready yet: %v", item["id"], item["attempts"], err)
attempts++
item["attempts"] = strconv.Itoa(attempts)
err = b.lp.SetAccountData(itemkey, item)
if err != nil {
b.log.Error("cannot update attempt count on email %s: %v", itemkey, err)
}
return false
}
// enqueueEmail adds an email to the queue
func (b *Bot) enqueueEmail(id, from, to, data string) error {
itemkey := acQueueKey + "." + id
item := map[string]string{
"attempts": "0",
"data": data,
"from": from,
"to": to,
"id": id,
}
b.lock(itemkey)
defer b.unlock(itemkey)
err := b.lp.SetAccountData(itemkey, item)
if err != nil {
b.log.Error("cannot enqueue email id=%s: %v", id, err)
return err
}
b.lock(acQueueKey)
defer b.unlock(acQueueKey)
queueIndex, err := b.lp.GetAccountData(acQueueKey)
if err != nil {
b.log.Error("cannot get queue index: %v", err)
return err
}
queueIndex[id] = itemkey
err = b.lp.SetAccountData(acQueueKey, queueIndex)
if err != nil {
b.log.Error("cannot save queue index: %v", err)
return err
}
return nil
}
// dequeueEmail removes an email from the queue
func (b *Bot) dequeueEmail(id string) error {
index, err := b.lp.GetAccountData(acQueueKey)
if err != nil {
b.log.Error("cannot get queue index: %v", err)
return err
}
itemkey := index[id]
if itemkey == "" {
itemkey = acQueueKey + "." + id
}
delete(index, id)
err = b.lp.SetAccountData(acQueueKey, index)
if err != nil {
b.log.Error("cannot update queue index: %v", err)
return err
}
b.lock(itemkey)
defer b.unlock(itemkey)
return b.lp.SetAccountData(itemkey, nil)
}

79
bot/queue/manager.go Normal file
View File

@@ -0,0 +1,79 @@
package queue
import (
"gitlab.com/etke.cc/go/logger"
"gitlab.com/etke.cc/linkpearl"
"gitlab.com/etke.cc/postmoogle/bot/config"
"gitlab.com/etke.cc/postmoogle/utils"
)
const (
acQueueKey = "cc.etke.postmoogle.mailqueue"
defaultQueueBatch = 1
defaultQueueRetries = 3
)
// Queue manager
type Queue struct {
mu utils.Mutex
lp *linkpearl.Linkpearl
cfg *config.Manager
log *logger.Logger
sendmail func(string, string, string) error
}
// New queue
func New(lp *linkpearl.Linkpearl, cfg *config.Manager, log *logger.Logger) *Queue {
return &Queue{
mu: utils.Mutex{},
lp: lp,
cfg: cfg,
log: log,
}
}
// SetSendmail func
func (q *Queue) SetSendmail(function func(string, string, string) error) {
q.sendmail = function
}
// Process queue
func (q *Queue) Process() {
q.log.Debug("staring queue processing...")
cfg := q.cfg.GetBot()
batchSize := cfg.QueueBatch()
if batchSize == 0 {
batchSize = defaultQueueBatch
}
maxRetries := cfg.QueueRetries()
if maxRetries == 0 {
maxRetries = defaultQueueRetries
}
q.mu.Lock(acQueueKey)
defer q.mu.Unlock(acQueueKey)
index, err := q.lp.GetAccountData(acQueueKey)
if err != nil {
q.log.Error("cannot get queue index: %v", err)
}
i := 0
for id, itemkey := range index {
if i > batchSize {
q.log.Debug("finished re-deliveries from queue")
return
}
if dequeue := q.try(itemkey, maxRetries); dequeue {
q.log.Debug("email %q has been delivered", id)
err = q.Remove(id)
if err != nil {
q.log.Error("cannot dequeue email %q: %v", id, err)
}
}
i++
}
q.log.Debug("ended queue processing")
}

101
bot/queue/queue.go Normal file
View File

@@ -0,0 +1,101 @@
package queue
import (
"strconv"
)
// Add to queue
func (q *Queue) Add(id, from, to, data string) error {
itemkey := acQueueKey + "." + id
item := map[string]string{
"attempts": "0",
"data": data,
"from": from,
"to": to,
"id": id,
}
q.mu.Lock(itemkey)
defer q.mu.Unlock(itemkey)
err := q.lp.SetAccountData(itemkey, item)
if err != nil {
q.log.Error("cannot enqueue email id=%q: %v", id, err)
return err
}
q.mu.Lock(acQueueKey)
defer q.mu.Unlock(acQueueKey)
queueIndex, err := q.lp.GetAccountData(acQueueKey)
if err != nil {
q.log.Error("cannot get queue index: %v", err)
return err
}
queueIndex[id] = itemkey
err = q.lp.SetAccountData(acQueueKey, queueIndex)
if err != nil {
q.log.Error("cannot save queue index: %v", err)
return err
}
return nil
}
// Remove from queue
func (q *Queue) Remove(id string) error {
index, err := q.lp.GetAccountData(acQueueKey)
if err != nil {
q.log.Error("cannot get queue index: %v", err)
return err
}
itemkey := index[id]
if itemkey == "" {
itemkey = acQueueKey + "." + id
}
delete(index, id)
err = q.lp.SetAccountData(acQueueKey, index)
if err != nil {
q.log.Error("cannot update queue index: %v", err)
return err
}
q.mu.Lock(itemkey)
defer q.mu.Unlock(itemkey)
return q.lp.SetAccountData(itemkey, nil)
}
// try to send email
func (q *Queue) try(itemkey string, maxRetries int) bool {
q.mu.Lock(itemkey)
defer q.mu.Unlock(itemkey)
item, err := q.lp.GetAccountData(itemkey)
if err != nil {
q.log.Error("cannot retrieve a queue item %q: %v", itemkey, err)
return false
}
q.log.Debug("processing queue item %+v", item)
attempts, err := strconv.Atoi(item["attempts"])
if err != nil {
q.log.Error("cannot parse attempts of %q: %v", itemkey, err)
return false
}
if attempts > maxRetries {
return true
}
err = q.sendmail(item["from"], item["to"], item["data"])
if err == nil {
q.log.Debug("email %q from queue was delivered")
return true
}
q.log.Debug("attempted to deliver email id=%q, retry=%q, but it's not ready yet: %v", item["id"], item["attempts"], err)
attempts++
item["attempts"] = strconv.Itoa(attempts)
err = q.lp.SetAccountData(itemkey, item)
if err != nil {
q.log.Error("cannot update attempt count on email %q: %v", itemkey, err)
}
return false
}

View File

@@ -1,122 +0,0 @@
package bot
import (
"strings"
"maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/utils"
)
// account data key
const acBotSettingsKey = "cc.etke.postmoogle.config"
// bot options keys
const (
botOptionAdminRoom = "adminroom"
botOptionUsers = "users"
botOptionCatchAll = "catch-all"
botOptionDKIMSignature = "dkim.pub"
botOptionDKIMPrivateKey = "dkim.pem"
botOptionQueueBatch = "queue:batch"
botOptionQueueRetries = "queue:retries"
botOptionBanlistEnabled = "banlist:enabled"
botOptionGreylist = "greylist"
)
type botSettings map[string]string
// Get option
func (s botSettings) Get(key string) string {
return s[strings.ToLower(strings.TrimSpace(key))]
}
// Set option
func (s botSettings) Set(key, value string) {
s[strings.ToLower(strings.TrimSpace(key))] = value
}
// Users option
func (s botSettings) Users() []string {
value := s.Get(botOptionUsers)
if value == "" {
return []string{}
}
if strings.Contains(value, " ") {
return strings.Split(value, " ")
}
return []string{value}
}
// CatchAll option
func (s botSettings) CatchAll() string {
return s.Get(botOptionCatchAll)
}
// AdminRoom option
func (s botSettings) AdminRoom() id.RoomID {
return id.RoomID(s.Get(botOptionAdminRoom))
}
// BanlistEnabled option
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)
}
// DKIMPrivateKey keep it secret
func (s botSettings) DKIMPrivateKey() string {
return s.Get(botOptionDKIMPrivateKey)
}
// QueueBatch option
func (s botSettings) QueueBatch() int {
return utils.Int(s.Get(botOptionQueueBatch))
}
// QueueRetries option
func (s botSettings) QueueRetries() int {
return utils.Int(s.Get(botOptionQueueRetries))
}
func (b *Bot) initBotUsers() ([]string, error) {
config := b.getBotSettings()
cfgUsers := config.Users()
if len(cfgUsers) > 0 {
return cfgUsers, nil
}
_, homeserver, err := b.lp.GetClient().UserID.Parse()
if err != nil {
return nil, err
}
config.Set(botOptionUsers, "@*:"+homeserver)
return config.Users(), b.setBotSettings(config)
}
func (b *Bot) getBotSettings() botSettings {
config, err := b.lp.GetAccountData(acBotSettingsKey)
if err != nil {
b.log.Error("cannot get bot settings: %v", utils.UnwrapError(err))
}
if config == nil {
config = map[string]string{}
}
return config
}
func (b *Bot) setBotSettings(cfg botSettings) error {
return utils.UnwrapError(b.lp.SetAccountData(acBotSettingsKey, cfg))
}

View File

@@ -1,116 +0,0 @@
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 = make(bglist, 0)
}
return config
}
func (b *Bot) setBanlist(cfg bglist) error {
b.lock("banlist")
if cfg == nil {
cfg = make(bglist, 0)
}
b.banlist = cfg
defer b.unlock("banlist")
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 = make(bglist, 0)
}
return config
}
func (b *Bot) setGreylist(cfg bglist) error {
return utils.UnwrapError(b.lp.SetAccountData(acGreylistKey, cfg))
}

View File

@@ -1,218 +0,0 @@
package bot
import (
"strings"
"maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/email"
"gitlab.com/etke.cc/postmoogle/utils"
)
// account data key
const acRoomSettingsKey = "cc.etke.postmoogle.settings"
// option keys
const (
roomOptionActive = ".active"
roomOptionOwner = "owner"
roomOptionMailbox = "mailbox"
roomOptionDomain = "domain"
roomOptionNoSend = "nosend"
roomOptionNoCC = "nocc"
roomOptionNoSender = "nosender"
roomOptionNoRecipient = "norecipient"
roomOptionNoSubject = "nosubject"
roomOptionNoHTML = "nohtml"
roomOptionNoThreads = "nothreads"
roomOptionNoFiles = "nofiles"
roomOptionPassword = "password"
roomOptionSpamcheckDKIM = "spamcheck:dkim"
roomOptionSpamcheckSMTP = "spamcheck:smtp"
roomOptionSpamcheckSPF = "spamcheck:spf"
roomOptionSpamcheckMX = "spamcheck:mx"
roomOptionSpamlist = "spamlist"
)
type roomSettings map[string]string
// Get option
func (s roomSettings) Get(key string) string {
return s[strings.ToLower(strings.TrimSpace(key))]
}
// Set option
func (s roomSettings) Set(key, value string) {
s[strings.ToLower(strings.TrimSpace(key))] = value
}
func (s roomSettings) Mailbox() string {
return s.Get(roomOptionMailbox)
}
func (s roomSettings) Domain() string {
return s.Get(roomOptionDomain)
}
func (s roomSettings) Owner() string {
return s.Get(roomOptionOwner)
}
func (s roomSettings) Active() bool {
return utils.Bool(s.Get(roomOptionActive))
}
func (s roomSettings) Password() string {
return s.Get(roomOptionPassword)
}
func (s roomSettings) NoSend() bool {
return utils.Bool(s.Get(roomOptionNoSend))
}
func (s roomSettings) NoCC() bool {
return utils.Bool(s.Get(roomOptionNoCC))
}
func (s roomSettings) NoSender() bool {
return utils.Bool(s.Get(roomOptionNoSender))
}
func (s roomSettings) NoRecipient() bool {
return utils.Bool(s.Get(roomOptionNoRecipient))
}
func (s roomSettings) NoSubject() bool {
return utils.Bool(s.Get(roomOptionNoSubject))
}
func (s roomSettings) NoHTML() bool {
return utils.Bool(s.Get(roomOptionNoHTML))
}
func (s roomSettings) NoThreads() bool {
return utils.Bool(s.Get(roomOptionNoThreads))
}
func (s roomSettings) NoFiles() bool {
return utils.Bool(s.Get(roomOptionNoFiles))
}
func (s roomSettings) SpamcheckDKIM() bool {
return utils.Bool(s.Get(roomOptionSpamcheckDKIM))
}
func (s roomSettings) SpamcheckSMTP() bool {
return utils.Bool(s.Get(roomOptionSpamcheckSMTP))
}
func (s roomSettings) SpamcheckSPF() bool {
return utils.Bool(s.Get(roomOptionSpamcheckSPF))
}
func (s roomSettings) SpamcheckMX() bool {
return utils.Bool(s.Get(roomOptionSpamcheckMX))
}
func (s roomSettings) Spamlist() []string {
return utils.StringSlice(s.Get(roomOptionSpamlist))
}
func (s roomSettings) migrateSpamlistSettings() {
uniq := map[string]struct{}{}
emails := utils.StringSlice(s.Get("spamlist:emails"))
localparts := utils.StringSlice(s.Get("spamlist:localparts"))
hosts := utils.StringSlice(s.Get("spamlist:hosts"))
list := utils.StringSlice(s.Get(roomOptionSpamlist))
delete(s, "spamlist:emails")
delete(s, "spamlist:localparts")
delete(s, "spamlist:hosts")
for _, email := range emails {
if email == "" {
continue
}
uniq[email] = struct{}{}
}
for _, localpart := range localparts {
if localpart == "" {
continue
}
uniq[localpart+"@*"] = struct{}{}
}
for _, host := range hosts {
if host == "" {
continue
}
uniq["*@"+host] = struct{}{}
}
for _, item := range list {
if item == "" {
continue
}
uniq[item] = struct{}{}
}
spamlist := make([]string, 0, len(uniq))
for item := range uniq {
spamlist = append(spamlist, item)
}
s.Set(roomOptionSpamlist, strings.Join(spamlist, ","))
}
// ContentOptions converts room display settings to content options
func (s roomSettings) ContentOptions() *email.ContentOptions {
return &email.ContentOptions{
CC: !s.NoCC(),
HTML: !s.NoHTML(),
Sender: !s.NoSender(),
Recipient: !s.NoRecipient(),
Subject: !s.NoSubject(),
Threads: !s.NoThreads(),
ToKey: eventToKey,
CcKey: eventCcKey,
FromKey: eventFromKey,
RcptToKey: eventRcptToKey,
SubjectKey: eventSubjectKey,
MessageIDKey: eventMessageIDkey,
InReplyToKey: eventInReplyToKey,
ReferencesKey: eventReferencesKey,
}
}
func (b *Bot) getRoomSettings(roomID id.RoomID) (roomSettings, error) {
config, err := b.lp.GetRoomAccountData(roomID, acRoomSettingsKey)
if config == nil {
config = map[string]string{}
}
return config, utils.UnwrapError(err)
}
func (b *Bot) setRoomSettings(roomID id.RoomID, cfg roomSettings) error {
return utils.UnwrapError(b.lp.SetRoomAccountData(roomID, acRoomSettingsKey, cfg))
}
func (b *Bot) migrateRoomSettings(roomID id.RoomID) {
cfg, err := b.getRoomSettings(roomID)
if err != nil {
b.log.Error("cannot retrieve room settings: %v", err)
return
}
if _, ok := cfg[roomOptionActive]; !ok {
cfg.Set(roomOptionActive, "true")
}
if cfg["spamlist:emails"] == "" && cfg["spamlist:localparts"] == "" && cfg["spamlist:hosts"] == "" {
return
}
cfg.migrateSpamlistSettings()
err = b.setRoomSettings(roomID, cfg)
if err != nil {
b.log.Error("cannot migrate room settings: %v", err)
}
}

View File

@@ -18,13 +18,17 @@ import (
lpcfg "gitlab.com/etke.cc/linkpearl/config"
"gitlab.com/etke.cc/postmoogle/bot"
mxconfig "gitlab.com/etke.cc/postmoogle/bot/config"
"gitlab.com/etke.cc/postmoogle/bot/queue"
"gitlab.com/etke.cc/postmoogle/config"
"gitlab.com/etke.cc/postmoogle/smtp"
"gitlab.com/etke.cc/postmoogle/utils"
)
var (
q *queue.Queue
hc *healthchecks.Client
mxc *mxconfig.Manager
mxb *bot.Bot
cron *crontab.Crontab
smtpm *smtp.Manager
@@ -47,7 +51,7 @@ func main() {
log.Debug("starting internal components...")
initSentry(cfg)
initHealthchecks(cfg)
initBot(cfg)
initMatrix(cfg)
initSMTP(cfg)
initCron()
initShutdown(quit)
@@ -85,12 +89,15 @@ func initHealthchecks(cfg *config.Config) {
go hc.Auto(cfg.Monitoring.HealthechsDuration)
}
func initBot(cfg *config.Config) {
func initMatrix(cfg *config.Config) {
db, err := sql.Open(cfg.DB.Dialect, cfg.DB.DSN)
if err != nil {
log.Fatal("cannot initialize SQL database: %v", err)
}
mxlog := logger.New("matrix.", cfg.LogLevel)
cfglog := logger.New("config.", cfg.LogLevel)
qlog := logger.New("queue.", cfg.LogLevel)
lp, err := linkpearl.New(&lpcfg.Config{
Homeserver: cfg.Homeserver,
Login: cfg.Login,
@@ -114,7 +121,9 @@ func initBot(cfg *config.Config) {
log.Fatal("cannot initialize matrix bot: %v", err)
}
mxb, err = bot.New(lp, mxlog, cfg.Prefix, cfg.Domains, cfg.Admins, bot.MBXConfig(cfg.Mailboxes))
mxc = mxconfig.New(lp, cfglog)
q = queue.New(lp, mxc, qlog)
mxb, err = bot.New(q, lp, mxlog, mxc, cfg.Prefix, cfg.Domains, cfg.Admins, bot.MBXConfig(cfg.Mailboxes))
if err != nil {
// nolint // Fatal = panic, not os.Exit()
log.Fatal("cannot start matrix bot: %v", err)
@@ -133,15 +142,16 @@ func initSMTP(cfg *config.Config) {
LogLevel: cfg.LogLevel,
MaxSize: cfg.MaxSize,
Bot: mxb,
Callers: []smtp.Caller{mxb, q},
})
}
func initCron() {
cron = crontab.New()
err := cron.AddJob("* * * * *", mxb.ProcessQueue)
err := cron.AddJob("* * * * *", q.Process)
if err != nil {
log.Error("cannot start ProcessQueue cronjob: %v", err)
log.Error("cannot start queue processing cronjob: %v", err)
}
}

29
smtp/logger.go Normal file
View File

@@ -0,0 +1,29 @@
package smtp
import (
"strings"
)
// loggerWrapper is a wrapper around logger.Logger to implement smtp.Logger interface
type loggerWrapper struct {
log func(string, ...interface{})
}
func (l loggerWrapper) Printf(format string, v ...interface{}) {
l.log(format, v...)
}
func (l loggerWrapper) Println(v ...interface{}) {
msg := strings.Repeat("%v ", len(v))
l.log(msg, v...)
}
// loggerWriter is a wrapper around io.Writer to implement io.Writer interface
type loggerWriter struct {
log func(string)
}
func (l loggerWriter) Write(p []byte) (n int, err error) {
l.log(string(p))
return len(p), nil
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"crypto/tls"
"net"
"os"
"time"
"github.com/emersion/go-smtp"
@@ -26,6 +25,7 @@ type Config struct {
LogLevel string
MaxSize int
Bot matrixbot
Callers []Caller
}
type Manager struct {
@@ -47,10 +47,14 @@ type matrixbot interface {
GetMapping(string) (id.RoomID, bool)
GetIFOptions(id.RoomID) email.IncomingFilteringOptions
IncomingEmail(context.Context, *email.Email) error
SetSendmail(func(string, string, string) error)
GetDKIMprivkey() string
}
// Caller is Sendmail caller
type Caller interface {
SetSendmail(func(string, string, string) error)
}
// NewManager creates new SMTP server manager
func NewManager(cfg *Config) *Manager {
log := logger.New("smtp.", cfg.LogLevel)
@@ -59,9 +63,12 @@ func NewManager(cfg *Config) *Manager {
bot: cfg.Bot,
domains: cfg.Domains,
}
cfg.Bot.SetSendmail(mailsrv.SendEmail)
for _, caller := range cfg.Callers {
caller.SetSendmail(mailsrv.SendEmail)
}
s := smtp.NewServer(mailsrv)
s.ErrorLog = loggerWrapper{func(s string, i ...interface{}) { log.Error(s, i...) }}
s.ReadTimeout = 10 * time.Second
s.WriteTimeout = 10 * time.Second
s.MaxMessageBytes = cfg.MaxSize * 1024 * 1024
@@ -73,7 +80,7 @@ func NewManager(cfg *Config) *Manager {
s.Domain = cfg.Domains[0]
}
if log.GetLevel() == "DEBUG" || log.GetLevel() == "TRACE" {
s.Debug = os.Stdout
s.Debug = loggerWriter{func(s string) { log.Debug(s) }}
}
m := &Manager{

32
utils/mutex.go Normal file
View File

@@ -0,0 +1,32 @@
package utils
import "sync"
// Mutex map
type Mutex map[string]*sync.Mutex
// NewMutex map
func NewMutex() Mutex {
return Mutex{}
}
// Lock by key
func (m Mutex) Lock(key string) {
_, ok := m[key]
if !ok {
m[key] = &sync.Mutex{}
}
m[key].Lock()
}
// Unlock by key
func (m Mutex) Unlock(key string) {
_, ok := m[key]
if !ok {
return
}
m[key].Unlock()
delete(m, key)
}