mailbox activation, closes #52

This commit is contained in:
Aine
2022-11-21 15:37:44 +02:00
parent a5edaaea78
commit 21772d7360
15 changed files with 186 additions and 17 deletions

View File

@@ -64,7 +64,8 @@ env vars
* **POSTMOOGLE_LOGLEVEL** - log level
* **POSTMOOGLE_DB_DSN** - database connection string
* **POSTMOOGLE_DB_DIALECT** - database dialect (postgres, sqlite3)
* **POSTMOOGLE_MAILBOXES_RESERVED** - space separated list of reserved mailboxes (e.g.: `postmaster admin root`), nobody can create them
* **POSTMOOGLE_MAILBOXES_RESERVED** - space separated list of reserved mailboxes, [docs/mailboxes.md](docs/mailboxes.md)
* **POSTMOOGLE_MAILBOXES_ACTIVATION** - activation flow for new mailboxes, [docs/mailboxes.md](docs/mailboxes.md)
* **POSTMOOGLE_MAXSIZE** - max email size (including attachments) in megabytes
* **POSTMOOGLE_ADMINS** - a space-separated list of admin users. See `POSTMOOGLE_USERS` for syntax examples
@@ -117,6 +118,7 @@ If you want to change them - check available options in the help message (`!pm h
---
* **!pm adminroom** - Get or set admin room
* **!pm dkim** - Get DKIM signature
* **!pm catch-all** - Configure catch-all mailbox
* **!pm queue:batch** - max amount of emails to process on each queue check

View File

@@ -74,7 +74,7 @@ func (b *Bot) allowSend(actorID id.UserID, targetRoomID id.RoomID) bool {
}
func (b *Bot) isReserved(mailbox string) bool {
for _, reserved := range b.reservedMailboxes {
for _, reserved := range b.mbxc.Reserved {
if mailbox == reserved {
return true
}

54
bot/activation.go Normal file
View File

@@ -0,0 +1,54 @@
package bot
import (
"fmt"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
)
type activationFlow func(id.UserID, id.RoomID, string) bool
func (b *Bot) getActivationFlow() activationFlow {
switch b.mbxc.Activation {
case "none":
return b.activateNone
case "notify":
return b.activateNotify
default:
return b.activateNone
}
}
// ActivateMailbox using the configured flow
func (b *Bot) ActivateMailbox(ownerID id.UserID, roomID id.RoomID, mailbox string) bool {
flow := b.getActivationFlow()
return flow(ownerID, roomID, mailbox)
}
func (b *Bot) activateNone(ownerID id.UserID, roomID id.RoomID, mailbox string) bool {
b.log.Debug("activating mailbox %q (%q) of %q through flow 'none'", mailbox, roomID, ownerID)
b.rooms.Store(mailbox, roomID)
return true
}
func (b *Bot) activateNotify(ownerID id.UserID, roomID id.RoomID, mailbox string) bool {
b.log.Debug("activating mailbox %q (%q) of %q through flow 'notify'", mailbox, roomID, ownerID)
b.rooms.Store(mailbox, roomID)
if len(b.adminRooms) == 0 {
return true
}
msg := fmt.Sprintf("Mailbox %q has been registered by %q for the room %q", mailbox, ownerID, roomID)
for _, adminRoom := range b.adminRooms {
content := format.RenderMarkdown(msg, true, true)
_, err := b.lp.Send(adminRoom, &content)
if err != nil {
b.log.Info("cannot send mailbox activation notification to the admin room %q", adminRoom)
continue
}
break
}
return true
}

View File

@@ -14,13 +14,20 @@ import (
"maunium.net/go/mautrix/id"
)
// Mailboxes config
type MBXConfig struct {
Reserved []string
Activation string
}
// Bot represents matrix bot
type Bot struct {
prefix string
mbxc MBXConfig
domains []string
allowedUsers []*regexp.Regexp
allowedAdmins []*regexp.Regexp
reservedMailboxes []string
adminRooms []id.RoomID
commands commandList
banlist bglist
rooms sync.Map
@@ -37,17 +44,18 @@ func New(
log *logger.Logger,
prefix string,
domains []string,
reserved []string,
admins []string,
mbxc MBXConfig,
) (*Bot, error) {
b := &Bot{
reservedMailboxes: reserved,
domains: domains,
prefix: prefix,
rooms: sync.Map{},
log: log,
lp: lp,
mu: map[string]*sync.Mutex{},
domains: domains,
prefix: prefix,
rooms: sync.Map{},
adminRooms: []id.RoomID{},
mbxc: mbxc,
log: log,
lp: lp,
mu: map[string]*sync.Mutex{},
}
users, err := b.initBotUsers()
if err != nil {

View File

@@ -189,6 +189,11 @@ func (b *Bot) initCommands() commandList {
allowed: b.allowOwner,
},
{allowed: b.allowAdmin}, // delimiter
{
key: botOptionAdminRoom,
description: "Get or set admin room",
allowed: b.allowAdmin,
},
{
key: botOptionUsers,
description: "Get or set allowed users",
@@ -280,6 +285,8 @@ func (b *Bot) handleCommand(ctx context.Context, evt *event.Event, commandSlice
b.runSend(ctx)
case commandDKIM:
b.runDKIM(ctx, commandSlice)
case botOptionAdminRoom:
b.runAdminRoom(ctx, commandSlice)
case commandUsers:
b.runUsers(ctx, commandSlice)
case commandCatchAll:

View File

@@ -208,6 +208,40 @@ func (b *Bot) runCatchAll(ctx context.Context, commandSlice []string) {
b.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Catch-all is set to: `%s` (%s).", mailbox, utils.EmailsList(mailbox, "")))
}
func (b *Bot) runAdminRoom(ctx context.Context, commandSlice []string) {
evt := eventFromContext(ctx)
cfg := b.getBotSettings()
if len(commandSlice) < 2 {
var msg strings.Builder
msg.WriteString("Currently: `")
if cfg.AdminRoom() != "" {
msg.WriteString(cfg.AdminRoom().String())
} else {
msg.WriteString("not set")
}
msg.WriteString("`\n\n")
msg.WriteString("Usage: `")
msg.WriteString(b.prefix)
msg.WriteString(" adminroom ROOM_ID`")
msg.WriteString("where ROOM_ID is valid and existing matrix room id\n")
b.SendNotice(ctx, evt.RoomID, msg.String())
return
}
roomID := b.parseCommand(evt.Content.AsMessage().Body, false)[1] // get original value, without forced lower case
cfg.Set(botOptionAdminRoom, roomID)
err := b.setBotSettings(cfg)
if err != nil {
b.Error(ctx, evt.RoomID, "cannot save bot options: %v", err)
return
}
b.adminRooms = append([]id.RoomID{id.RoomID(roomID)}, b.adminRooms...) // make it the first room in list on the fly
b.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Admin Room is set to: `%s`.", roomID))
}
func (b *Bot) printGreylist(ctx context.Context, roomID id.RoomID) {
cfg := b.getBotSettings()
greylist := b.getGreylist()

View File

@@ -3,6 +3,7 @@ package bot
import (
"context"
"fmt"
"strconv"
"github.com/raja/argon2pw"
@@ -84,6 +85,10 @@ func (b *Bot) setOption(ctx context.Context, name, value string) {
}
evt := eventFromContext(ctx)
// ignore request
if name == roomOptionActive {
return
}
if name == roomOptionMailbox {
existingID, ok := b.getMapping(value)
if (ok && existingID != "" && existingID != evt.RoomID) || b.isReserved(value) {
@@ -115,7 +120,8 @@ func (b *Bot) setOption(ctx context.Context, name, value string) {
if old != "" {
b.rooms.Delete(old)
}
b.rooms.Store(value, evt.RoomID)
active := b.ActivateMailbox(evt.Sender, evt.RoomID, value)
cfg.Set(roomOptionActive, strconv.FormatBool(active))
value = fmt.Sprintf("%s@%s", value, utils.SanitizeDomain(cfg.Domain()))
}

View File

@@ -1,5 +1,7 @@
package bot
import "maunium.net/go/mautrix/id"
var migrations = []string{}
func (b *Bot) migrate() error {
@@ -32,20 +34,30 @@ func (b *Bot) migrate() error {
}
func (b *Bot) syncRooms() error {
adminRoom := b.getBotSettings().AdminRoom()
if adminRoom != "" {
b.adminRooms = append(b.adminRooms, adminRoom)
}
resp, err := b.lp.GetClient().JoinedRooms()
if err != nil {
return err
}
for _, roomID := range resp.JoinedRooms {
b.migrateRoomSettings(roomID)
cfg, serr := b.getRoomSettings(roomID)
if serr != nil {
continue
}
b.migrateRoomSettings(roomID)
mailbox := cfg.Mailbox()
if mailbox != "" {
active := cfg.Active()
if mailbox != "" && active {
b.rooms.Store(mailbox, roomID)
}
if cfg.Owner() != "" && b.allowAdmin(id.UserID(cfg.Owner()), "") {
b.adminRooms = append(b.adminRooms, roomID)
}
}
return nil

View File

@@ -3,6 +3,8 @@ package bot
import (
"strings"
"maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/utils"
)
@@ -11,6 +13,7 @@ const acBotSettingsKey = "cc.etke.postmoogle.config"
// bot options keys
const (
botOptionAdminRoom = "adminroom"
botOptionUsers = "users"
botOptionCatchAll = "catch-all"
botOptionDKIMSignature = "dkim.pub"
@@ -52,6 +55,11 @@ 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))

View File

@@ -14,6 +14,7 @@ const acRoomSettingsKey = "cc.etke.postmoogle.settings"
// option keys
const (
roomOptionActive = ".active"
roomOptionOwner = "owner"
roomOptionMailbox = "mailbox"
roomOptionDomain = "domain"
@@ -55,6 +56,10 @@ 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)
}
@@ -188,6 +193,9 @@ func (b *Bot) migrateRoomSettings(roomID id.RoomID) {
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

View File

@@ -98,7 +98,7 @@ func initBot(cfg *config.Config) {
log.Fatal("cannot initialize matrix bot: %v", err)
}
mxb, err = bot.New(lp, mxlog, cfg.Prefix, cfg.Domains, cfg.Mailboxes.Reserved, cfg.Admins)
mxb, err = bot.New(lp, mxlog, 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)

View File

@@ -23,7 +23,8 @@ func New() *Config {
StatusMsg: env.String("statusmsg", defaultConfig.StatusMsg),
Admins: env.Slice("admins"),
Mailboxes: Mailboxes{
Reserved: env.Slice("mailboxes.reserved"),
Reserved: env.Slice("mailboxes.reserved"),
Activation: env.String("mailboxes.activation", defaultConfig.Mailboxes.Activation),
},
TLS: TLS{
Certs: env.Slice("tls.cert"),

View File

@@ -7,6 +7,9 @@ var defaultConfig = &Config{
Prefix: "!pm",
MaxSize: 1024,
StatusMsg: "Delivering emails",
Mailboxes: Mailboxes{
Activation: "none",
},
DB: DB{
DSN: "local.db",
Dialect: "sqlite3",

View File

@@ -62,5 +62,6 @@ type Sentry struct {
// Mailboxes config
type Mailboxes struct {
Reserved []string
Reserved []string
Activation string
}

25
docs/mailboxes.md Normal file
View File

@@ -0,0 +1,25 @@
# Mailboxes configuration
## `POSTMOOGLE_MAILBOXES_RESERVED`
Space separated list of reserved mailboxes, example:
```bash
export POSTMOOGLE_MAILBOXES_RESERVED=admin root postmaster
```
Nobody can create a mailbox from that list
## `POSTMOOGLE_MAILBOXES_ACTIVATION`
Type of activation flow:
### `none` (default)
If `POSTMOOGLE_MAILBOXES_ACTIVATION=none` mailbox will be just created as is, without any additional checks.
### `notify`
If `POSTMOOGLE_MAILBOXES_ACTIVATION=notify`, mailbox will be created as in `none` case **and** notification will be sent to one of the mailboxes managed by a postmoogle admin.
To make it work, a postmoogle admin (or multiple admins) should either set `!pm adminroom` or create at least one mailbox.