mailbox activation, closes #52
This commit is contained in:
@@ -64,7 +64,8 @@ env vars
|
|||||||
* **POSTMOOGLE_LOGLEVEL** - log level
|
* **POSTMOOGLE_LOGLEVEL** - log level
|
||||||
* **POSTMOOGLE_DB_DSN** - database connection string
|
* **POSTMOOGLE_DB_DSN** - database connection string
|
||||||
* **POSTMOOGLE_DB_DIALECT** - database dialect (postgres, sqlite3)
|
* **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_MAXSIZE** - max email size (including attachments) in megabytes
|
||||||
* **POSTMOOGLE_ADMINS** - a space-separated list of admin users. See `POSTMOOGLE_USERS` for syntax examples
|
* **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 dkim** - Get DKIM signature
|
||||||
* **!pm catch-all** - Configure catch-all mailbox
|
* **!pm catch-all** - Configure catch-all mailbox
|
||||||
* **!pm queue:batch** - max amount of emails to process on each queue check
|
* **!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 {
|
func (b *Bot) isReserved(mailbox string) bool {
|
||||||
for _, reserved := range b.reservedMailboxes {
|
for _, reserved := range b.mbxc.Reserved {
|
||||||
if mailbox == reserved {
|
if mailbox == reserved {
|
||||||
return true
|
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
|
||||||
|
}
|
||||||
14
bot/bot.go
14
bot/bot.go
@@ -14,13 +14,20 @@ import (
|
|||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Mailboxes config
|
||||||
|
type MBXConfig struct {
|
||||||
|
Reserved []string
|
||||||
|
Activation string
|
||||||
|
}
|
||||||
|
|
||||||
// Bot represents matrix bot
|
// Bot represents matrix bot
|
||||||
type Bot struct {
|
type Bot struct {
|
||||||
prefix string
|
prefix string
|
||||||
|
mbxc MBXConfig
|
||||||
domains []string
|
domains []string
|
||||||
allowedUsers []*regexp.Regexp
|
allowedUsers []*regexp.Regexp
|
||||||
allowedAdmins []*regexp.Regexp
|
allowedAdmins []*regexp.Regexp
|
||||||
reservedMailboxes []string
|
adminRooms []id.RoomID
|
||||||
commands commandList
|
commands commandList
|
||||||
banlist bglist
|
banlist bglist
|
||||||
rooms sync.Map
|
rooms sync.Map
|
||||||
@@ -37,14 +44,15 @@ func New(
|
|||||||
log *logger.Logger,
|
log *logger.Logger,
|
||||||
prefix string,
|
prefix string,
|
||||||
domains []string,
|
domains []string,
|
||||||
reserved []string,
|
|
||||||
admins []string,
|
admins []string,
|
||||||
|
mbxc MBXConfig,
|
||||||
) (*Bot, error) {
|
) (*Bot, error) {
|
||||||
b := &Bot{
|
b := &Bot{
|
||||||
reservedMailboxes: reserved,
|
|
||||||
domains: domains,
|
domains: domains,
|
||||||
prefix: prefix,
|
prefix: prefix,
|
||||||
rooms: sync.Map{},
|
rooms: sync.Map{},
|
||||||
|
adminRooms: []id.RoomID{},
|
||||||
|
mbxc: mbxc,
|
||||||
log: log,
|
log: log,
|
||||||
lp: lp,
|
lp: lp,
|
||||||
mu: map[string]*sync.Mutex{},
|
mu: map[string]*sync.Mutex{},
|
||||||
|
|||||||
@@ -189,6 +189,11 @@ func (b *Bot) initCommands() commandList {
|
|||||||
allowed: b.allowOwner,
|
allowed: b.allowOwner,
|
||||||
},
|
},
|
||||||
{allowed: b.allowAdmin}, // delimiter
|
{allowed: b.allowAdmin}, // delimiter
|
||||||
|
{
|
||||||
|
key: botOptionAdminRoom,
|
||||||
|
description: "Get or set admin room",
|
||||||
|
allowed: b.allowAdmin,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: botOptionUsers,
|
key: botOptionUsers,
|
||||||
description: "Get or set allowed users",
|
description: "Get or set allowed users",
|
||||||
@@ -280,6 +285,8 @@ func (b *Bot) handleCommand(ctx context.Context, evt *event.Event, commandSlice
|
|||||||
b.runSend(ctx)
|
b.runSend(ctx)
|
||||||
case commandDKIM:
|
case commandDKIM:
|
||||||
b.runDKIM(ctx, commandSlice)
|
b.runDKIM(ctx, commandSlice)
|
||||||
|
case botOptionAdminRoom:
|
||||||
|
b.runAdminRoom(ctx, commandSlice)
|
||||||
case commandUsers:
|
case commandUsers:
|
||||||
b.runUsers(ctx, commandSlice)
|
b.runUsers(ctx, commandSlice)
|
||||||
case commandCatchAll:
|
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, "")))
|
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) {
|
func (b *Bot) printGreylist(ctx context.Context, roomID id.RoomID) {
|
||||||
cfg := b.getBotSettings()
|
cfg := b.getBotSettings()
|
||||||
greylist := b.getGreylist()
|
greylist := b.getGreylist()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package bot
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/raja/argon2pw"
|
"github.com/raja/argon2pw"
|
||||||
|
|
||||||
@@ -84,6 +85,10 @@ func (b *Bot) setOption(ctx context.Context, name, value string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
evt := eventFromContext(ctx)
|
evt := eventFromContext(ctx)
|
||||||
|
// ignore request
|
||||||
|
if name == roomOptionActive {
|
||||||
|
return
|
||||||
|
}
|
||||||
if name == roomOptionMailbox {
|
if name == roomOptionMailbox {
|
||||||
existingID, ok := b.getMapping(value)
|
existingID, ok := b.getMapping(value)
|
||||||
if (ok && existingID != "" && existingID != evt.RoomID) || b.isReserved(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 != "" {
|
if old != "" {
|
||||||
b.rooms.Delete(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()))
|
value = fmt.Sprintf("%s@%s", value, utils.SanitizeDomain(cfg.Domain()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
bot/data.go
16
bot/data.go
@@ -1,5 +1,7 @@
|
|||||||
package bot
|
package bot
|
||||||
|
|
||||||
|
import "maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
var migrations = []string{}
|
var migrations = []string{}
|
||||||
|
|
||||||
func (b *Bot) migrate() error {
|
func (b *Bot) migrate() error {
|
||||||
@@ -32,20 +34,30 @@ func (b *Bot) migrate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) syncRooms() error {
|
func (b *Bot) syncRooms() error {
|
||||||
|
adminRoom := b.getBotSettings().AdminRoom()
|
||||||
|
if adminRoom != "" {
|
||||||
|
b.adminRooms = append(b.adminRooms, adminRoom)
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := b.lp.GetClient().JoinedRooms()
|
resp, err := b.lp.GetClient().JoinedRooms()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, roomID := range resp.JoinedRooms {
|
for _, roomID := range resp.JoinedRooms {
|
||||||
|
b.migrateRoomSettings(roomID)
|
||||||
cfg, serr := b.getRoomSettings(roomID)
|
cfg, serr := b.getRoomSettings(roomID)
|
||||||
if serr != nil {
|
if serr != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
b.migrateRoomSettings(roomID)
|
|
||||||
mailbox := cfg.Mailbox()
|
mailbox := cfg.Mailbox()
|
||||||
if mailbox != "" {
|
active := cfg.Active()
|
||||||
|
if mailbox != "" && active {
|
||||||
b.rooms.Store(mailbox, roomID)
|
b.rooms.Store(mailbox, roomID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.Owner() != "" && b.allowAdmin(id.UserID(cfg.Owner()), "") {
|
||||||
|
b.adminRooms = append(b.adminRooms, roomID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package bot
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
"gitlab.com/etke.cc/postmoogle/utils"
|
"gitlab.com/etke.cc/postmoogle/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -11,6 +13,7 @@ const acBotSettingsKey = "cc.etke.postmoogle.config"
|
|||||||
|
|
||||||
// bot options keys
|
// bot options keys
|
||||||
const (
|
const (
|
||||||
|
botOptionAdminRoom = "adminroom"
|
||||||
botOptionUsers = "users"
|
botOptionUsers = "users"
|
||||||
botOptionCatchAll = "catch-all"
|
botOptionCatchAll = "catch-all"
|
||||||
botOptionDKIMSignature = "dkim.pub"
|
botOptionDKIMSignature = "dkim.pub"
|
||||||
@@ -52,6 +55,11 @@ func (s botSettings) CatchAll() string {
|
|||||||
return s.Get(botOptionCatchAll)
|
return s.Get(botOptionCatchAll)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdminRoom option
|
||||||
|
func (s botSettings) AdminRoom() id.RoomID {
|
||||||
|
return id.RoomID(s.Get(botOptionAdminRoom))
|
||||||
|
}
|
||||||
|
|
||||||
// BanlistEnabled option
|
// BanlistEnabled option
|
||||||
func (s botSettings) BanlistEnabled() bool {
|
func (s botSettings) BanlistEnabled() bool {
|
||||||
return utils.Bool(s.Get(botOptionBanlistEnabled))
|
return utils.Bool(s.Get(botOptionBanlistEnabled))
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const acRoomSettingsKey = "cc.etke.postmoogle.settings"
|
|||||||
|
|
||||||
// option keys
|
// option keys
|
||||||
const (
|
const (
|
||||||
|
roomOptionActive = ".active"
|
||||||
roomOptionOwner = "owner"
|
roomOptionOwner = "owner"
|
||||||
roomOptionMailbox = "mailbox"
|
roomOptionMailbox = "mailbox"
|
||||||
roomOptionDomain = "domain"
|
roomOptionDomain = "domain"
|
||||||
@@ -55,6 +56,10 @@ func (s roomSettings) Owner() string {
|
|||||||
return s.Get(roomOptionOwner)
|
return s.Get(roomOptionOwner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s roomSettings) Active() bool {
|
||||||
|
return utils.Bool(s.Get(roomOptionActive))
|
||||||
|
}
|
||||||
|
|
||||||
func (s roomSettings) Password() string {
|
func (s roomSettings) Password() string {
|
||||||
return s.Get(roomOptionPassword)
|
return s.Get(roomOptionPassword)
|
||||||
}
|
}
|
||||||
@@ -188,6 +193,9 @@ func (b *Bot) migrateRoomSettings(roomID id.RoomID) {
|
|||||||
b.log.Error("cannot retrieve room settings: %v", err)
|
b.log.Error("cannot retrieve room settings: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if _, ok := cfg[roomOptionActive]; !ok {
|
||||||
|
cfg.Set(roomOptionActive, "true")
|
||||||
|
}
|
||||||
|
|
||||||
if cfg["spamlist:emails"] == "" && cfg["spamlist:localparts"] == "" && cfg["spamlist:hosts"] == "" {
|
if cfg["spamlist:emails"] == "" && cfg["spamlist:localparts"] == "" && cfg["spamlist:hosts"] == "" {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ func initBot(cfg *config.Config) {
|
|||||||
log.Fatal("cannot initialize matrix bot: %v", err)
|
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 {
|
if err != nil {
|
||||||
// nolint // Fatal = panic, not os.Exit()
|
// nolint // Fatal = panic, not os.Exit()
|
||||||
log.Fatal("cannot start matrix bot: %v", err)
|
log.Fatal("cannot start matrix bot: %v", err)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ func New() *Config {
|
|||||||
Admins: env.Slice("admins"),
|
Admins: env.Slice("admins"),
|
||||||
Mailboxes: Mailboxes{
|
Mailboxes: Mailboxes{
|
||||||
Reserved: env.Slice("mailboxes.reserved"),
|
Reserved: env.Slice("mailboxes.reserved"),
|
||||||
|
Activation: env.String("mailboxes.activation", defaultConfig.Mailboxes.Activation),
|
||||||
},
|
},
|
||||||
TLS: TLS{
|
TLS: TLS{
|
||||||
Certs: env.Slice("tls.cert"),
|
Certs: env.Slice("tls.cert"),
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ var defaultConfig = &Config{
|
|||||||
Prefix: "!pm",
|
Prefix: "!pm",
|
||||||
MaxSize: 1024,
|
MaxSize: 1024,
|
||||||
StatusMsg: "Delivering emails",
|
StatusMsg: "Delivering emails",
|
||||||
|
Mailboxes: Mailboxes{
|
||||||
|
Activation: "none",
|
||||||
|
},
|
||||||
DB: DB{
|
DB: DB{
|
||||||
DSN: "local.db",
|
DSN: "local.db",
|
||||||
Dialect: "sqlite3",
|
Dialect: "sqlite3",
|
||||||
|
|||||||
@@ -63,4 +63,5 @@ type Sentry struct {
|
|||||||
// Mailboxes config
|
// Mailboxes config
|
||||||
type Mailboxes struct {
|
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