diff --git a/bot/access.go b/bot/access.go index c8964ad..19b6322 100644 --- a/bot/access.go +++ b/bot/access.go @@ -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 diff --git a/bot/bot.go b/bot/bot.go index 82a4d48..81485f7 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -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") diff --git a/bot/command.go b/bot/command.go index b010c43..4a5ce24 100644 --- a/bot/command.go +++ b/bot/command.go @@ -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.") diff --git a/bot/command_admin.go b/bot/command_admin.go index e7e9088..911597d 100644 --- a/bot/command_admin.go +++ b/bot/command_admin.go @@ -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 diff --git a/bot/command_owner.go b/bot/command_owner.go index 6c99959..4198a45 100644 --- a/bot/command_owner.go +++ b/bot/command_owner.go @@ -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) diff --git a/bot/config/bot.go b/bot/config/bot.go new file mode 100644 index 0000000..aa85764 --- /dev/null +++ b/bot/config/bot.go @@ -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)) +} diff --git a/bot/config/lists.go b/bot/config/lists.go new file mode 100644 index 0000000..0e28745 --- /dev/null +++ b/bot/config/lists.go @@ -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) +} diff --git a/bot/config/manager.go b/bot/config/manager.go new file mode 100644 index 0000000..d5b1938 --- /dev/null +++ b/bot/config/manager.go @@ -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)) +} diff --git a/bot/config/room.go b/bot/config/room.go new file mode 100644 index 0000000..cbf1ee6 --- /dev/null +++ b/bot/config/room.go @@ -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", + } +} diff --git a/bot/data.go b/bot/data.go index 2327fec..e6c9a1e 100644 --- a/bot/data.go +++ b/bot/data.go @@ -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) } diff --git a/bot/email.go b/bot/email.go index b35ac26..57215f1 100644 --- a/bot/email.go +++ b/bot/email.go @@ -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 { diff --git a/bot/message.go b/bot/message.go deleted file mode 100644 index 1211dd4..0000000 --- a/bot/message.go +++ /dev/null @@ -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) -} diff --git a/bot/mutex.go b/bot/mutex.go deleted file mode 100644 index 5fc0041..0000000 --- a/bot/mutex.go +++ /dev/null @@ -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) -} diff --git a/bot/queue.go b/bot/queue.go deleted file mode 100644 index 865c097..0000000 --- a/bot/queue.go +++ /dev/null @@ -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) -} diff --git a/bot/queue/manager.go b/bot/queue/manager.go new file mode 100644 index 0000000..beb7923 --- /dev/null +++ b/bot/queue/manager.go @@ -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") +} diff --git a/bot/queue/queue.go b/bot/queue/queue.go new file mode 100644 index 0000000..077aa2e --- /dev/null +++ b/bot/queue/queue.go @@ -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 +} diff --git a/bot/settings_bot.go b/bot/settings_bot.go deleted file mode 100644 index dd6bede..0000000 --- a/bot/settings_bot.go +++ /dev/null @@ -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)) -} diff --git a/bot/settings_lists.go b/bot/settings_lists.go deleted file mode 100644 index b3a3443..0000000 --- a/bot/settings_lists.go +++ /dev/null @@ -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)) -} diff --git a/bot/settings_room.go b/bot/settings_room.go deleted file mode 100644 index 2b451a9..0000000 --- a/bot/settings_room.go +++ /dev/null @@ -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) - } -} diff --git a/cmd/cmd.go b/cmd/cmd.go index a95fde5..a9af1be 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -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) } } diff --git a/smtp/logger.go b/smtp/logger.go new file mode 100644 index 0000000..aabdd84 --- /dev/null +++ b/smtp/logger.go @@ -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 +} diff --git a/smtp/manager.go b/smtp/manager.go index 08227dc..2dea6af 100644 --- a/smtp/manager.go +++ b/smtp/manager.go @@ -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{ diff --git a/utils/mutex.go b/utils/mutex.go new file mode 100644 index 0000000..a5ffb03 --- /dev/null +++ b/utils/mutex.go @@ -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) +}