mailbox activation, closes #52
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
54
bot/activation.go
Normal 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
|
||||
}
|
||||
26
bot/bot.go
26
bot/bot.go
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
|
||||
16
bot/data.go
16
bot/data.go
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -62,5 +62,6 @@ type Sentry struct {
|
||||
|
||||
// Mailboxes config
|
||||
type Mailboxes struct {
|
||||
Reserved []string
|
||||
Reserved []string
|
||||
Activation string
|
||||
}
|
||||
|
||||
25
docs/mailboxes.md
Normal file
25
docs/mailboxes.md
Normal 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.
|
||||
Reference in New Issue
Block a user