Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bebfa6df92 | ||
|
|
8bdd46fb32 | ||
|
|
da41bd31fb | ||
|
|
7fbb279830 | ||
|
|
816db6f409 | ||
|
|
e2f5f4c731 | ||
|
|
6be4891165 | ||
|
|
18f1113d33 | ||
|
|
b413e5871a | ||
|
|
8f3a74d46c | ||
|
|
480c99cf79 | ||
|
|
74defa85e4 |
10
README.md
10
README.md
@@ -18,6 +18,7 @@ so you can use it to send emails from your apps and scripts as well.
|
|||||||
- [x] Configuration in room's account data
|
- [x] Configuration in room's account data
|
||||||
- [x] Receive emails to matrix rooms
|
- [x] Receive emails to matrix rooms
|
||||||
- [x] Receive attachments
|
- [x] Receive attachments
|
||||||
|
- [x] Subaddressing support
|
||||||
- [x] Catch-all mailbox
|
- [x] Catch-all mailbox
|
||||||
- [x] Map email threads to matrix threads
|
- [x] Map email threads to matrix threads
|
||||||
- [x] Multi-domain support
|
- [x] Multi-domain support
|
||||||
@@ -35,6 +36,8 @@ so you can use it to send emails from your apps and scripts as well.
|
|||||||
- [x] SMTP server (you can use Postmoogle as general purpose SMTP server to send emails from your scripts or apps)
|
- [x] SMTP server (you can use Postmoogle as general purpose SMTP server to send emails from your scripts or apps)
|
||||||
- [x] Send a message to matrix room with special format to send a new email, even to multiple email addresses at once
|
- [x] Send a message to matrix room with special format to send a new email, even to multiple email addresses at once
|
||||||
- [x] Reply to matrix thread sends reply into email thread
|
- [x] Reply to matrix thread sends reply into email thread
|
||||||
|
- [x] Email signatures
|
||||||
|
- [x] Email autoreply / autoresponder for new email threads
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@@ -120,6 +123,8 @@ If you want to change them - check available options in the help message (`!pm h
|
|||||||
|
|
||||||
> The following section is visible to the mailbox owners only
|
> The following section is visible to the mailbox owners only
|
||||||
|
|
||||||
|
* **`!pm autoreply`** - Get or set autoreply of the room (markdown supported) that will be sent on any new incoming email thread
|
||||||
|
* **`!pm signature`** - Get or set signature of the room (markdown supported)
|
||||||
* **`!pm nosend`** - Get or set `nosend` of the room (`true` - disable email sending; `false` - enable email sending)
|
* **`!pm nosend`** - Get or set `nosend` of the room (`true` - disable email sending; `false` - enable email sending)
|
||||||
* **`!pm noreplies`** - Get or set `noreplies` of the room (`true` - ignore matrix replies; `false` - parse matrix replies)
|
* **`!pm noreplies`** - Get or set `noreplies` of the room (`true` - ignore matrix replies; `false` - parse matrix replies)
|
||||||
* **`!pm nosender`** - Get or set `nosender` of the room (`true` - hide email sender; `false` - show email sender)
|
* **`!pm nosender`** - Get or set `nosender` of the room (`true` - hide email sender; `false` - show email sender)
|
||||||
@@ -149,7 +154,7 @@ If you want to change them - check available options in the help message (`!pm h
|
|||||||
> The following section is visible to the mailbox owners only
|
> The following section is visible to the mailbox owners only
|
||||||
|
|
||||||
* **`!pm spam:list`** - Show comma-separated spamlist of the room, eg: `spammer@example.com,*@spammer.org,spam@*`
|
* **`!pm spam:list`** - Show comma-separated spamlist of the room, eg: `spammer@example.com,*@spammer.org,spam@*`
|
||||||
* **`!pm spam:add`** - Mark an email address (or pattern) as spam
|
* **`!pm spam:add`** - Mark an email address (or pattern) as spam (or you can react to the email with emoji: ⛔️,🛑, or 🚫)
|
||||||
* **`!pm spam:remove`** - Unmark an email address (or pattern) as spam
|
* **`!pm spam:remove`** - Unmark an email address (or pattern) as spam
|
||||||
* **`!pm spam:reset`** - Reset spamlist
|
* **`!pm spam:reset`** - Reset spamlist
|
||||||
|
|
||||||
@@ -176,6 +181,9 @@ If you want to change them - check available options in the help message (`!pm h
|
|||||||
|
|
||||||
* **`!pm greylist`** - Set automatic greylisting duration in minutes (0 - disabled)
|
* **`!pm greylist`** - Set automatic greylisting duration in minutes (0 - disabled)
|
||||||
* **`!pm banlist`** - Enable/disable banlist and show current values
|
* **`!pm banlist`** - Enable/disable banlist and show current values
|
||||||
|
* **`!pm banlist:auth`** - Enable/disable automatic banning for invalid auth credentials
|
||||||
|
* **`!pm banlist:auto`** - Enable/disable automatic banning for invalid emails
|
||||||
|
* **`!pm banlist:totals`** - List banlist totals only
|
||||||
* **`!pm banlist:add`** - Ban an IP
|
* **`!pm banlist:add`** - Ban an IP
|
||||||
* **`!pm banlist:remove`** - Unban an IP
|
* **`!pm banlist:remove`** - Unban an IP
|
||||||
* **`!pm banlist:reset`** - Reset banlist
|
* **`!pm banlist:reset`** - Reset banlist
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/getsentry/sentry-go"
|
|
||||||
"github.com/raja/argon2pw"
|
"github.com/raja/argon2pw"
|
||||||
"gitlab.com/etke.cc/go/mxidwc"
|
"gitlab.com/etke.cc/go/mxidwc"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
@@ -43,7 +42,7 @@ func (b *Bot) allowOwner(actorID id.UserID, targetRoomID id.RoomID) bool {
|
|||||||
}
|
}
|
||||||
cfg, err := b.cfg.GetRoom(targetRoomID)
|
cfg, err := b.cfg.GetRoom(targetRoomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(sentry.SetHubOnContext(context.Background(), sentry.CurrentHub()), targetRoomID, "failed to retrieve settings: %v", err)
|
b.Error(context.Background(), "failed to retrieve settings: %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +65,7 @@ func (b *Bot) allowSend(actorID id.UserID, targetRoomID id.RoomID) bool {
|
|||||||
|
|
||||||
cfg, err := b.cfg.GetRoom(targetRoomID)
|
cfg, err := b.cfg.GetRoom(targetRoomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(sentry.SetHubOnContext(context.Background(), sentry.CurrentHub()), targetRoomID, "failed to retrieve settings: %v", err)
|
b.Error(context.Background(), "failed to retrieve settings: %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +79,7 @@ func (b *Bot) allowReply(actorID id.UserID, targetRoomID id.RoomID) bool {
|
|||||||
|
|
||||||
cfg, err := b.cfg.GetRoom(targetRoomID)
|
cfg, err := b.cfg.GetRoom(targetRoomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(sentry.SetHubOnContext(context.Background(), sentry.CurrentHub()), targetRoomID, "failed to retrieve settings: %v", err)
|
b.Error(context.Background(), "failed to retrieve settings: %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,15 +135,59 @@ func (b *Bot) IsTrusted(addr net.Addr) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ban an address
|
// Ban an address automatically
|
||||||
func (b *Bot) Ban(addr net.Addr) {
|
func (b *Bot) BanAuto(addr net.Addr) {
|
||||||
|
if !b.cfg.GetBot().BanlistEnabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !b.cfg.GetBot().BanlistAuto() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.IsTrusted(addr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.log.Debug().Str("addr", addr.String()).Msg("attempting to automatically ban")
|
||||||
|
banlist := b.cfg.GetBanlist()
|
||||||
|
banlist.Add(addr)
|
||||||
|
err := b.cfg.SetBanlist(banlist)
|
||||||
|
if err != nil {
|
||||||
|
b.log.Error().Err(err).Str("addr", addr.String()).Msg("cannot update banlist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ban an address for incorrect auth automatically
|
||||||
|
func (b *Bot) BanAuth(addr net.Addr) {
|
||||||
|
if !b.cfg.GetBot().BanlistEnabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !b.cfg.GetBot().BanlistAuth() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.IsTrusted(addr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.log.Debug().Str("addr", addr.String()).Msg("attempting to automatically ban")
|
||||||
|
banlist := b.cfg.GetBanlist()
|
||||||
|
banlist.Add(addr)
|
||||||
|
err := b.cfg.SetBanlist(banlist)
|
||||||
|
if err != nil {
|
||||||
|
b.log.Error().Err(err).Str("addr", addr.String()).Msg("cannot update banlist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ban an address manually
|
||||||
|
func (b *Bot) BanManually(addr net.Addr) {
|
||||||
if !b.cfg.GetBot().BanlistEnabled() {
|
if !b.cfg.GetBot().BanlistEnabled() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if b.IsTrusted(addr) {
|
if b.IsTrusted(addr) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
b.log.Debug().Str("addr", addr.String()).Msg("attempting to ban")
|
b.log.Debug().Str("addr", addr.String()).Msg("attempting to manually ban")
|
||||||
banlist := b.cfg.GetBanlist()
|
banlist := b.cfg.GetBanlist()
|
||||||
banlist.Add(addr)
|
banlist.Add(addr)
|
||||||
err := b.cfg.SetBanlist(banlist)
|
err := b.cfg.SetBanlist(banlist)
|
||||||
|
|||||||
41
bot/bot.go
41
bot/bot.go
@@ -6,11 +6,9 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/getsentry/sentry-go"
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"gitlab.com/etke.cc/linkpearl"
|
"gitlab.com/etke.cc/linkpearl"
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
"maunium.net/go/mautrix/format"
|
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
"gitlab.com/etke.cc/postmoogle/bot/config"
|
"gitlab.com/etke.cc/postmoogle/bot/config"
|
||||||
@@ -93,28 +91,31 @@ func New(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Error message to the log and matrix room
|
// Error message to the log and matrix room
|
||||||
func (b *Bot) Error(ctx context.Context, roomID id.RoomID, message string, args ...interface{}) {
|
func (b *Bot) Error(ctx context.Context, message string, args ...interface{}) {
|
||||||
|
evt := eventFromContext(ctx)
|
||||||
|
threadID := threadIDFromContext(ctx)
|
||||||
|
if threadID == "" {
|
||||||
|
threadID = linkpearl.EventParent(evt.ID, evt.Content.AsMessage())
|
||||||
|
}
|
||||||
|
|
||||||
err := fmt.Errorf(message, args...)
|
err := fmt.Errorf(message, args...)
|
||||||
b.log.Error().Err(err).Msg("something is wrong")
|
b.log.Error().Err(err).Msg(err.Error())
|
||||||
|
if evt == nil {
|
||||||
if roomID != "" {
|
return
|
||||||
b.SendError(ctx, roomID, err.Error())
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// SendError sends an error message to the matrix room
|
var noThreads bool
|
||||||
func (b *Bot) SendError(ctx context.Context, roomID id.RoomID, message string) {
|
cfg, cerr := b.cfg.GetRoom(evt.RoomID)
|
||||||
b.SendNotice(ctx, roomID, "ERROR: "+message)
|
if cerr == nil {
|
||||||
}
|
noThreads = cfg.NoThreads()
|
||||||
|
|
||||||
// SendNotice sends a notice message to the matrix room
|
|
||||||
func (b *Bot) SendNotice(ctx context.Context, roomID id.RoomID, message string) {
|
|
||||||
parsed := format.RenderMarkdown(message, true, true)
|
|
||||||
parsed.MsgType = event.MsgNotice
|
|
||||||
_, err := b.lp.Send(roomID, &event.Content{Parsed: &parsed})
|
|
||||||
if err != nil {
|
|
||||||
sentry.GetHubFromContext(ctx).CaptureException(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var relatesTo *event.RelatesTo
|
||||||
|
if threadID != "" {
|
||||||
|
relatesTo = linkpearl.RelatesTo(threadID, noThreads)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.lp.SendNotice(evt.RoomID, "ERROR: "+err.Error(), relatesTo)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start performs matrix /sync
|
// Start performs matrix /sync
|
||||||
|
|||||||
140
bot/command.go
140
bot/command.go
@@ -6,6 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitlab.com/etke.cc/linkpearl"
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
"maunium.net/go/mautrix/format"
|
"maunium.net/go/mautrix/format"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
@@ -30,6 +31,9 @@ const (
|
|||||||
commandSpamlistReset = "spam:reset"
|
commandSpamlistReset = "spam:reset"
|
||||||
commandDelete = "delete"
|
commandDelete = "delete"
|
||||||
commandBanlist = "banlist"
|
commandBanlist = "banlist"
|
||||||
|
commandBanlistTotals = "banlist:totals"
|
||||||
|
commandBanlistAuto = "banlist:auto"
|
||||||
|
commandBanlistAuth = "banlist:auth"
|
||||||
commandBanlistAdd = "banlist:add"
|
commandBanlistAdd = "banlist:add"
|
||||||
commandBanlistRemove = "banlist:remove"
|
commandBanlistRemove = "banlist:remove"
|
||||||
commandBanlistReset = "banlist:reset"
|
commandBanlistReset = "banlist:reset"
|
||||||
@@ -99,6 +103,18 @@ func (b *Bot) initCommands() commandList {
|
|||||||
allowed: b.allowOwner,
|
allowed: b.allowOwner,
|
||||||
},
|
},
|
||||||
{allowed: b.allowOwner, description: "mailbox options"}, // delimiter
|
{allowed: b.allowOwner, description: "mailbox options"}, // delimiter
|
||||||
|
{
|
||||||
|
key: config.RoomAutoreply,
|
||||||
|
description: "Get or set autoreply of the room (markdown supported) that will be send for any new incoming email thread",
|
||||||
|
sanitizer: func(s string) string { return s },
|
||||||
|
allowed: b.allowOwner,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: config.RoomSignature,
|
||||||
|
description: "Get or set signature of the room (markdown supported)",
|
||||||
|
sanitizer: func(s string) string { return s },
|
||||||
|
allowed: b.allowOwner,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: config.RoomNoSend,
|
key: config.RoomNoSend,
|
||||||
description: fmt.Sprintf(
|
description: fmt.Sprintf(
|
||||||
@@ -223,7 +239,7 @@ func (b *Bot) initCommands() commandList {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: commandSpamlistAdd,
|
key: commandSpamlistAdd,
|
||||||
description: "Mark an email address (or pattern) as spam",
|
description: "Mark an email address (or pattern) as spam (or you can react to the email with emoji: ⛔️,🛑, or 🚫)",
|
||||||
allowed: b.allowOwner,
|
allowed: b.allowOwner,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -290,6 +306,21 @@ func (b *Bot) initCommands() commandList {
|
|||||||
description: "Enable/disable banlist and show current values",
|
description: "Enable/disable banlist and show current values",
|
||||||
allowed: b.allowAdmin,
|
allowed: b.allowAdmin,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: commandBanlistAuth,
|
||||||
|
description: "Enable/disable automatic banning of IP addresses when they try to auth with invalid credentials",
|
||||||
|
allowed: b.allowAdmin,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: commandBanlistAuto,
|
||||||
|
description: "Enable/disable automatic banning of IP addresses when they try to send invalid emails",
|
||||||
|
allowed: b.allowAdmin,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: commandBanlistTotals,
|
||||||
|
description: "List banlist totals only",
|
||||||
|
allowed: b.allowAdmin,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: commandBanlistAdd,
|
key: commandBanlistAdd,
|
||||||
description: "Ban an IP",
|
description: "Ban an IP",
|
||||||
@@ -317,7 +348,7 @@ func (b *Bot) handle(ctx context.Context) {
|
|||||||
|
|
||||||
content := evt.Content.AsMessage()
|
content := evt.Content.AsMessage()
|
||||||
if content == nil {
|
if content == nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot read message")
|
b.Error(ctx, "cannot read message")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// ignore notices
|
// ignore notices
|
||||||
@@ -327,7 +358,7 @@ func (b *Bot) handle(ctx context.Context) {
|
|||||||
message := strings.TrimSpace(content.Body)
|
message := strings.TrimSpace(content.Body)
|
||||||
commandSlice := b.parseCommand(message, true)
|
commandSlice := b.parseCommand(message, true)
|
||||||
if commandSlice == nil {
|
if commandSlice == nil {
|
||||||
if utils.EventParent("", content) != "" {
|
if linkpearl.EventParent("", content) != "" {
|
||||||
b.SendEmailReply(ctx)
|
b.SendEmailReply(ctx)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -344,7 +375,7 @@ func (b *Bot) handle(ctx context.Context) {
|
|||||||
defer b.lp.GetClient().UserTyping(evt.RoomID, false, 30*time.Second) //nolint:errcheck
|
defer b.lp.GetClient().UserTyping(evt.RoomID, false, 30*time.Second) //nolint:errcheck
|
||||||
|
|
||||||
if !cmd.allowed(evt.Sender, evt.RoomID) {
|
if !cmd.allowed(evt.Sender, evt.RoomID) {
|
||||||
b.SendNotice(ctx, evt.RoomID, "not allowed to do that, kupo")
|
b.lp.SendNotice(evt.RoomID, "not allowed to do that, kupo")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,6 +406,12 @@ func (b *Bot) handle(ctx context.Context) {
|
|||||||
b.runGreylist(ctx, commandSlice)
|
b.runGreylist(ctx, commandSlice)
|
||||||
case commandBanlist:
|
case commandBanlist:
|
||||||
b.runBanlist(ctx, commandSlice)
|
b.runBanlist(ctx, commandSlice)
|
||||||
|
case commandBanlistAuth:
|
||||||
|
b.runBanlistAuth(ctx, commandSlice)
|
||||||
|
case commandBanlistAuto:
|
||||||
|
b.runBanlistAuto(ctx, commandSlice)
|
||||||
|
case commandBanlistTotals:
|
||||||
|
b.runBanlistTotals(ctx)
|
||||||
case commandBanlistAdd:
|
case commandBanlistAdd:
|
||||||
b.runBanlistAdd(ctx, commandSlice)
|
b.runBanlistAdd(ctx, commandSlice)
|
||||||
case commandBanlistRemove:
|
case commandBanlistRemove:
|
||||||
@@ -405,7 +442,7 @@ func (b *Bot) parseCommand(message string, toLower bool) []string {
|
|||||||
return strings.Split(strings.TrimSpace(message), " ")
|
return strings.Split(strings.TrimSpace(message), " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) sendIntroduction(ctx context.Context, roomID id.RoomID) {
|
func (b *Bot) sendIntroduction(roomID id.RoomID) {
|
||||||
var msg strings.Builder
|
var msg strings.Builder
|
||||||
msg.WriteString("Hello, kupo!\n\n")
|
msg.WriteString("Hello, kupo!\n\n")
|
||||||
|
|
||||||
@@ -421,7 +458,7 @@ func (b *Bot) sendIntroduction(ctx context.Context, roomID id.RoomID) {
|
|||||||
msg.WriteString(utils.EmailsList("SOME_INBOX", ""))
|
msg.WriteString(utils.EmailsList("SOME_INBOX", ""))
|
||||||
msg.WriteString("` and have them appear in this room.")
|
msg.WriteString("` and have them appear in this room.")
|
||||||
|
|
||||||
b.SendNotice(ctx, roomID, msg.String())
|
b.lp.SendNotice(roomID, msg.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) getHelpValue(cfg config.Room, cmd command) string {
|
func (b *Bot) getHelpValue(cfg config.Room, cmd command) string {
|
||||||
@@ -481,40 +518,19 @@ func (b *Bot) sendHelp(ctx context.Context) {
|
|||||||
msg.WriteString("\n")
|
msg.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
b.SendNotice(ctx, evt.RoomID, msg.String())
|
b.lp.SendNotice(evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID, cfg.NoThreads()))
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocognit // TODO
|
|
||||||
func (b *Bot) runSend(ctx context.Context) {
|
func (b *Bot) runSend(ctx context.Context) {
|
||||||
evt := eventFromContext(ctx)
|
evt := eventFromContext(ctx)
|
||||||
if !b.allowSend(evt.Sender, evt.RoomID) {
|
to, subject, body, shouldSend := b.getSendDetails(ctx)
|
||||||
return
|
if !shouldSend {
|
||||||
}
|
|
||||||
commandSlice := b.parseCommand(evt.Content.AsMessage().Body, false)
|
|
||||||
to, subject, body, err := utils.ParseSend(commandSlice)
|
|
||||||
if err == utils.ErrInvalidArgs {
|
|
||||||
b.SendNotice(ctx, evt.RoomID, fmt.Sprintf(
|
|
||||||
"Usage:\n"+
|
|
||||||
"```\n"+
|
|
||||||
"%s send someone@example.com\n"+
|
|
||||||
"Subject goes here on a line of its own\n"+
|
|
||||||
"Email content goes here\n"+
|
|
||||||
"on as many lines\n"+
|
|
||||||
"as you want.\n"+
|
|
||||||
"```",
|
|
||||||
b.prefix))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := b.cfg.GetRoom(evt.RoomID)
|
cfg, err := b.cfg.GetRoom(evt.RoomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "failed to retrieve room settings: %v", err)
|
b.Error(ctx, "failed to retrieve room settings: %v", err)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mailbox := cfg.Mailbox()
|
|
||||||
if mailbox == "" {
|
|
||||||
b.SendNotice(ctx, evt.RoomID, "mailbox is not configured, kupo")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,10 +540,60 @@ func (b *Bot) runSend(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tos := strings.Split(to, ",")
|
tos := strings.Split(to, ",")
|
||||||
|
b.runSendCommand(ctx, cfg, tos, subject, body, htmlBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) getSendDetails(ctx context.Context) (string, string, string, bool) {
|
||||||
|
evt := eventFromContext(ctx)
|
||||||
|
if !b.allowSend(evt.Sender, evt.RoomID) {
|
||||||
|
return "", "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := b.cfg.GetRoom(evt.RoomID)
|
||||||
|
if err != nil {
|
||||||
|
b.Error(ctx, "failed to retrieve room settings: %v", err)
|
||||||
|
return "", "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
commandSlice := b.parseCommand(evt.Content.AsMessage().Body, false)
|
||||||
|
to, subject, body, err := utils.ParseSend(commandSlice)
|
||||||
|
if err == utils.ErrInvalidArgs {
|
||||||
|
b.lp.SendNotice(evt.RoomID, fmt.Sprintf(
|
||||||
|
"Usage:\n"+
|
||||||
|
"```\n"+
|
||||||
|
"%s send someone@example.com\n"+
|
||||||
|
"Subject goes here on a line of its own\n"+
|
||||||
|
"Email content goes here\n"+
|
||||||
|
"on as many lines\n"+
|
||||||
|
"as you want.\n"+
|
||||||
|
"```",
|
||||||
|
b.prefix),
|
||||||
|
linkpearl.RelatesTo(evt.ID, cfg.NoThreads()),
|
||||||
|
)
|
||||||
|
return "", "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
mailbox := cfg.Mailbox()
|
||||||
|
if mailbox == "" {
|
||||||
|
b.lp.SendNotice(evt.RoomID, "mailbox is not configured, kupo", linkpearl.RelatesTo(evt.ID, cfg.NoThreads()))
|
||||||
|
return "", "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
signature := cfg.Signature()
|
||||||
|
if signature != "" {
|
||||||
|
body += "\n\n---\n" + signature
|
||||||
|
}
|
||||||
|
|
||||||
|
return to, subject, body, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) runSendCommand(ctx context.Context, cfg config.Room, tos []string, subject, body, htmlBody string) {
|
||||||
|
evt := eventFromContext(ctx)
|
||||||
|
|
||||||
// validate first
|
// validate first
|
||||||
for _, to := range tos {
|
for _, to := range tos {
|
||||||
if !email.AddressValid(to) {
|
if !email.AddressValid(to) {
|
||||||
b.Error(ctx, evt.RoomID, "email address is not valid")
|
b.Error(ctx, "email address is not valid")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -536,29 +602,29 @@ func (b *Bot) runSend(ctx context.Context) {
|
|||||||
defer b.mu.Unlock(evt.RoomID.String())
|
defer b.mu.Unlock(evt.RoomID.String())
|
||||||
|
|
||||||
domain := utils.SanitizeDomain(cfg.Domain())
|
domain := utils.SanitizeDomain(cfg.Domain())
|
||||||
from := mailbox + "@" + domain
|
from := cfg.Mailbox() + "@" + domain
|
||||||
ID := email.MessageID(evt.ID, domain)
|
ID := email.MessageID(evt.ID, domain)
|
||||||
for _, to := range tos {
|
for _, to := range tos {
|
||||||
recipients := []string{to}
|
recipients := []string{to}
|
||||||
eml := email.New(ID, "", " "+ID, subject, from, to, to, "", body, htmlBody, nil, nil)
|
eml := email.New(ID, "", " "+ID, subject, from, to, to, "", body, htmlBody, nil, nil)
|
||||||
data := eml.Compose(b.cfg.GetBot().DKIMPrivateKey())
|
data := eml.Compose(b.cfg.GetBot().DKIMPrivateKey())
|
||||||
if data == "" {
|
if data == "" {
|
||||||
b.SendError(ctx, evt.RoomID, "email body is empty")
|
b.lp.SendNotice(evt.RoomID, "email body is empty", linkpearl.RelatesTo(evt.ID, cfg.NoThreads()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
queued, err := b.Sendmail(evt.ID, from, to, data)
|
queued, err := b.Sendmail(evt.ID, from, to, data)
|
||||||
if queued {
|
if queued {
|
||||||
b.log.Error().Err(err).Msg("cannot send email")
|
b.log.Warn().Err(err).Msg("email has been queued")
|
||||||
b.saveSentMetadata(ctx, queued, evt.ID, recipients, eml, cfg)
|
b.saveSentMetadata(ctx, queued, evt.ID, recipients, eml, cfg)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot send email to %s: %v", to, err)
|
b.Error(ctx, "cannot send email to %s: %v", to, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
b.saveSentMetadata(ctx, false, evt.ID, recipients, eml, cfg)
|
b.saveSentMetadata(ctx, false, evt.ID, recipients, eml, cfg)
|
||||||
}
|
}
|
||||||
if len(tos) > 1 {
|
if len(tos) > 1 {
|
||||||
b.SendNotice(ctx, evt.RoomID, "All emails were sent.")
|
b.lp.SendNotice(evt.RoomID, "All emails were sent.", linkpearl.RelatesTo(evt.ID, cfg.NoThreads()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitlab.com/etke.cc/go/secgen"
|
"gitlab.com/etke.cc/go/secgen"
|
||||||
|
"gitlab.com/etke.cc/linkpearl"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
"gitlab.com/etke.cc/postmoogle/bot/config"
|
"gitlab.com/etke.cc/postmoogle/bot/config"
|
||||||
@@ -47,7 +49,7 @@ func (b *Bot) sendMailboxes(ctx context.Context) {
|
|||||||
sort.Strings(slice)
|
sort.Strings(slice)
|
||||||
|
|
||||||
if len(slice) == 0 {
|
if len(slice) == 0 {
|
||||||
b.SendNotice(ctx, evt.RoomID, "No mailboxes are managed by the bot so far, kupo!")
|
b.lp.SendNotice(evt.RoomID, "No mailboxes are managed by the bot so far, kupo!", linkpearl.RelatesTo(evt.ID))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,20 +64,20 @@ func (b *Bot) sendMailboxes(ctx context.Context) {
|
|||||||
msg.WriteString("\n")
|
msg.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
b.SendNotice(ctx, evt.RoomID, msg.String())
|
b.lp.SendNotice(evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) runDelete(ctx context.Context, commandSlice []string) {
|
func (b *Bot) runDelete(ctx context.Context, commandSlice []string) {
|
||||||
evt := eventFromContext(ctx)
|
evt := eventFromContext(ctx)
|
||||||
if len(commandSlice) < 2 {
|
if len(commandSlice) < 2 {
|
||||||
b.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Usage: `%s delete MAILBOX`", b.prefix))
|
b.lp.SendNotice(evt.RoomID, fmt.Sprintf("Usage: `%s delete MAILBOX`", b.prefix), linkpearl.RelatesTo(evt.ID))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mailbox := utils.Mailbox(commandSlice[1])
|
mailbox := utils.Mailbox(commandSlice[1])
|
||||||
|
|
||||||
v, ok := b.rooms.Load(mailbox)
|
v, ok := b.rooms.Load(mailbox)
|
||||||
if v == nil || !ok {
|
if v == nil || !ok {
|
||||||
b.SendError(ctx, evt.RoomID, "mailbox does not exists, kupo")
|
b.lp.SendNotice(evt.RoomID, "mailbox does not exists, kupo", linkpearl.RelatesTo(evt.ID))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
roomID := v.(id.RoomID)
|
roomID := v.(id.RoomID)
|
||||||
@@ -83,11 +85,11 @@ func (b *Bot) runDelete(ctx context.Context, commandSlice []string) {
|
|||||||
b.rooms.Delete(mailbox)
|
b.rooms.Delete(mailbox)
|
||||||
err := b.cfg.SetRoom(roomID, config.Room{})
|
err := b.cfg.SetRoom(roomID, config.Room{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot update settings: %v", err)
|
b.Error(ctx, "cannot update settings: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b.SendNotice(ctx, evt.RoomID, "mailbox has been deleted")
|
b.lp.SendNotice(evt.RoomID, "mailbox has been deleted", linkpearl.RelatesTo(evt.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) runUsers(ctx context.Context, commandSlice []string) {
|
func (b *Bot) runUsers(ctx context.Context, commandSlice []string) {
|
||||||
@@ -107,19 +109,19 @@ func (b *Bot) runUsers(ctx context.Context, commandSlice []string) {
|
|||||||
msg.WriteString("where each pattern is like `@someone:example.com`, ")
|
msg.WriteString("where each pattern is like `@someone:example.com`, ")
|
||||||
msg.WriteString("`@bot.*:example.com`, `@*:another.com`, or `@*:*`\n")
|
msg.WriteString("`@bot.*:example.com`, `@*:another.com`, or `@*:*`\n")
|
||||||
|
|
||||||
b.SendNotice(ctx, evt.RoomID, msg.String())
|
b.lp.SendNotice(evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, homeserver, err := b.lp.GetClient().UserID.Parse()
|
_, homeserver, err := b.lp.GetClient().UserID.Parse()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.SendError(ctx, evt.RoomID, fmt.Sprintf("invalid userID: %v", err))
|
b.lp.SendNotice(evt.RoomID, fmt.Sprintf("invalid userID: %v", err), linkpearl.RelatesTo(evt.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
patterns := commandSlice[1:]
|
patterns := commandSlice[1:]
|
||||||
allowedUsers, err := parseMXIDpatterns(patterns, "@*:"+homeserver)
|
allowedUsers, err := parseMXIDpatterns(patterns, "@*:"+homeserver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.SendError(ctx, evt.RoomID, fmt.Sprintf("invalid patterns: %v", err))
|
b.lp.SendNotice(evt.RoomID, fmt.Sprintf("invalid patterns: %v", err), linkpearl.RelatesTo(evt.ID))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,10 +129,10 @@ func (b *Bot) runUsers(ctx context.Context, commandSlice []string) {
|
|||||||
|
|
||||||
err = b.cfg.SetBot(cfg)
|
err = b.cfg.SetBot(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot set bot config: %v", err)
|
b.Error(ctx, "cannot set bot config: %v", err)
|
||||||
}
|
}
|
||||||
b.allowedUsers = allowedUsers
|
b.allowedUsers = allowedUsers
|
||||||
b.SendNotice(ctx, evt.RoomID, "allowed users updated")
|
b.lp.SendNotice(evt.RoomID, "allowed users updated", linkpearl.RelatesTo(evt.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) runDKIM(ctx context.Context, commandSlice []string) {
|
func (b *Bot) runDKIM(ctx context.Context, commandSlice []string) {
|
||||||
@@ -147,25 +149,27 @@ func (b *Bot) runDKIM(ctx context.Context, commandSlice []string) {
|
|||||||
var derr error
|
var derr error
|
||||||
signature, private, derr = secgen.DKIM()
|
signature, private, derr = secgen.DKIM()
|
||||||
if derr != nil {
|
if derr != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot generate DKIM signature: %v", derr)
|
b.Error(ctx, "cannot generate DKIM signature: %v", derr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cfg.Set(config.BotDKIMSignature, signature)
|
cfg.Set(config.BotDKIMSignature, signature)
|
||||||
cfg.Set(config.BotDKIMPrivateKey, private)
|
cfg.Set(config.BotDKIMPrivateKey, private)
|
||||||
err := b.cfg.SetBot(cfg)
|
err := b.cfg.SetBot(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot save bot options: %v", err)
|
b.Error(ctx, "cannot save bot options: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
b.SendNotice(ctx, evt.RoomID, fmt.Sprintf(
|
b.lp.SendNotice(evt.RoomID, fmt.Sprintf(
|
||||||
"DKIM signature is: `%s`.\n"+
|
"DKIM signature is: `%s`.\n"+
|
||||||
"You need to add it to DNS records of all domains added to postmoogle (if not already):\n"+
|
"You need to add it to DNS records of all domains added to postmoogle (if not already):\n"+
|
||||||
"Add new DNS record with type = `TXT`, key (subdomain/from): `postmoogle._domainkey` and value (to):\n ```\n%s\n```\n"+
|
"Add new DNS record with type = `TXT`, key (subdomain/from): `postmoogle._domainkey` and value (to):\n ```\n%s\n```\n"+
|
||||||
"Without that record other email servers may reject your emails as spam, kupo.\n"+
|
"Without that record other email servers may reject your emails as spam, kupo.\n"+
|
||||||
"To reset the signature, send `%s dkim reset`",
|
"To reset the signature, send `%s dkim reset`",
|
||||||
signature, signature, b.prefix))
|
signature, signature, b.prefix),
|
||||||
|
linkpearl.RelatesTo(evt.ID),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) runCatchAll(ctx context.Context, commandSlice []string) {
|
func (b *Bot) runCatchAll(ctx context.Context, commandSlice []string) {
|
||||||
@@ -188,25 +192,25 @@ func (b *Bot) runCatchAll(ctx context.Context, commandSlice []string) {
|
|||||||
msg.WriteString(" catch-all MAILBOX`")
|
msg.WriteString(" catch-all MAILBOX`")
|
||||||
msg.WriteString("where mailbox is valid and existing mailbox name\n")
|
msg.WriteString("where mailbox is valid and existing mailbox name\n")
|
||||||
|
|
||||||
b.SendNotice(ctx, evt.RoomID, msg.String())
|
b.lp.SendNotice(evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mailbox := utils.Mailbox(commandSlice[1])
|
mailbox := utils.Mailbox(commandSlice[1])
|
||||||
_, ok := b.GetMapping(mailbox)
|
_, ok := b.GetMapping(mailbox)
|
||||||
if !ok {
|
if !ok {
|
||||||
b.SendError(ctx, evt.RoomID, "mailbox does not exist, kupo.")
|
b.lp.SendNotice(evt.RoomID, "mailbox does not exist, kupo.", linkpearl.RelatesTo(evt.ID))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Set(config.BotCatchAll, mailbox)
|
cfg.Set(config.BotCatchAll, mailbox)
|
||||||
err := b.cfg.SetBot(cfg)
|
err := b.cfg.SetBot(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot save bot options: %v", err)
|
b.Error(ctx, "cannot save bot options: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Catch-all is set to: `%s` (%s).", mailbox, utils.EmailsList(mailbox, "")))
|
b.lp.SendNotice(evt.RoomID, fmt.Sprintf("Catch-all is set to: `%s` (%s).", mailbox, utils.EmailsList(mailbox, "")), linkpearl.RelatesTo(evt.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) runAdminRoom(ctx context.Context, commandSlice []string) {
|
func (b *Bot) runAdminRoom(ctx context.Context, commandSlice []string) {
|
||||||
@@ -226,7 +230,7 @@ func (b *Bot) runAdminRoom(ctx context.Context, commandSlice []string) {
|
|||||||
msg.WriteString(" adminroom ROOM_ID`")
|
msg.WriteString(" adminroom ROOM_ID`")
|
||||||
msg.WriteString("where ROOM_ID is valid and existing matrix room id\n")
|
msg.WriteString("where ROOM_ID is valid and existing matrix room id\n")
|
||||||
|
|
||||||
b.SendNotice(ctx, evt.RoomID, msg.String())
|
b.lp.SendNotice(evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,13 +238,13 @@ func (b *Bot) runAdminRoom(ctx context.Context, commandSlice []string) {
|
|||||||
cfg.Set(config.BotAdminRoom, roomID)
|
cfg.Set(config.BotAdminRoom, roomID)
|
||||||
err := b.cfg.SetBot(cfg)
|
err := b.cfg.SetBot(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot save bot options: %v", err)
|
b.Error(ctx, "cannot save bot options: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b.adminRooms = append([]id.RoomID{id.RoomID(roomID)}, b.adminRooms...) // make it the first room in list on the fly
|
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))
|
b.lp.SendNotice(evt.RoomID, fmt.Sprintf("Admin Room is set to: `%s`.", roomID), linkpearl.RelatesTo(evt.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) printGreylist(ctx context.Context, roomID id.RoomID) {
|
func (b *Bot) printGreylist(ctx context.Context, roomID id.RoomID) {
|
||||||
@@ -271,7 +275,7 @@ func (b *Bot) printGreylist(ctx context.Context, roomID id.RoomID) {
|
|||||||
msg.WriteString("where `MIN` is duration in minutes for automatic greylisting\n")
|
msg.WriteString("where `MIN` is duration in minutes for automatic greylisting\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
b.SendNotice(ctx, roomID, msg.String())
|
b.lp.SendNotice(roomID, msg.String(), linkpearl.RelatesTo(eventFromContext(ctx).ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) runGreylist(ctx context.Context, commandSlice []string) {
|
func (b *Bot) runGreylist(ctx context.Context, commandSlice []string) {
|
||||||
@@ -285,9 +289,9 @@ func (b *Bot) runGreylist(ctx context.Context, commandSlice []string) {
|
|||||||
cfg.Set(config.BotGreylist, value)
|
cfg.Set(config.BotGreylist, value)
|
||||||
err := b.cfg.SetBot(cfg)
|
err := b.cfg.SetBot(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot set bot config: %v", err)
|
b.Error(ctx, "cannot set bot config: %v", err)
|
||||||
}
|
}
|
||||||
b.SendNotice(ctx, evt.RoomID, "greylist duration has been updated")
|
b.lp.SendNotice(evt.RoomID, "greylist duration has been updated", linkpearl.RelatesTo(evt.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) runBanlist(ctx context.Context, commandSlice []string) {
|
func (b *Bot) runBanlist(ctx context.Context, commandSlice []string) {
|
||||||
@@ -302,9 +306,7 @@ func (b *Bot) runBanlist(ctx context.Context, commandSlice []string) {
|
|||||||
msg.WriteString(cfg.Get(config.BotBanlistEnabled))
|
msg.WriteString(cfg.Get(config.BotBanlistEnabled))
|
||||||
msg.WriteString("`, total: ")
|
msg.WriteString("`, total: ")
|
||||||
msg.WriteString(strconv.Itoa(size))
|
msg.WriteString(strconv.Itoa(size))
|
||||||
msg.WriteString(" hosts (`")
|
msg.WriteString("\n\n")
|
||||||
msg.WriteString(strings.Join(banlist.Slice(), "`, `"))
|
|
||||||
msg.WriteString("`)\n\n")
|
|
||||||
}
|
}
|
||||||
if !cfg.BanlistEnabled() {
|
if !cfg.BanlistEnabled() {
|
||||||
msg.WriteString("To enable banlist, send `")
|
msg.WriteString("To enable banlist, send `")
|
||||||
@@ -314,18 +316,92 @@ func (b *Bot) runBanlist(ctx context.Context, commandSlice []string) {
|
|||||||
msg.WriteString("To ban somebody: `")
|
msg.WriteString("To ban somebody: `")
|
||||||
msg.WriteString(b.prefix)
|
msg.WriteString(b.prefix)
|
||||||
msg.WriteString(" banlist:add IP1 IP2 IP3...`")
|
msg.WriteString(" banlist:add IP1 IP2 IP3...`")
|
||||||
msg.WriteString("where each ip is IPv4 or IPv6\n")
|
msg.WriteString("where each ip is IPv4 or IPv6\n\n")
|
||||||
|
msg.WriteString("You can find current banlist values below:\n")
|
||||||
|
|
||||||
b.SendNotice(ctx, evt.RoomID, msg.String())
|
b.lp.SendNotice(evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID))
|
||||||
|
b.addBanlistTimeline(ctx, false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
value := utils.SanitizeBoolString(commandSlice[1])
|
value := utils.SanitizeBoolString(commandSlice[1])
|
||||||
cfg.Set(config.BotBanlistEnabled, value)
|
cfg.Set(config.BotBanlistEnabled, value)
|
||||||
err := b.cfg.SetBot(cfg)
|
err := b.cfg.SetBot(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot set bot config: %v", err)
|
b.Error(ctx, "cannot set bot config: %v", err)
|
||||||
}
|
}
|
||||||
b.SendNotice(ctx, evt.RoomID, "banlist has been updated")
|
b.lp.SendNotice(evt.RoomID, "banlist has been updated", linkpearl.RelatesTo(evt.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) runBanlistTotals(ctx context.Context) {
|
||||||
|
evt := eventFromContext(ctx)
|
||||||
|
banlist := b.cfg.GetBanlist()
|
||||||
|
var msg strings.Builder
|
||||||
|
size := len(banlist)
|
||||||
|
if size == 0 {
|
||||||
|
b.lp.SendNotice(evt.RoomID, "banlist is empty, kupo.", linkpearl.RelatesTo(evt.ID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.WriteString("Total: ")
|
||||||
|
msg.WriteString(strconv.Itoa(size))
|
||||||
|
msg.WriteString(" hosts banned\n\n")
|
||||||
|
msg.WriteString("You can find daily totals below:\n")
|
||||||
|
b.lp.SendNotice(evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID))
|
||||||
|
b.addBanlistTimeline(ctx, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) runBanlistAuth(ctx context.Context, commandSlice []string) {
|
||||||
|
evt := eventFromContext(ctx)
|
||||||
|
cfg := b.cfg.GetBot()
|
||||||
|
if len(commandSlice) < 2 {
|
||||||
|
var msg strings.Builder
|
||||||
|
msg.WriteString("Currently: `")
|
||||||
|
msg.WriteString(cfg.Get(config.BotBanlistAuth))
|
||||||
|
msg.WriteString("`\n\n")
|
||||||
|
|
||||||
|
if !cfg.BanlistAuth() {
|
||||||
|
msg.WriteString("To enable automatic banning for invalid credentials, send `")
|
||||||
|
msg.WriteString(b.prefix)
|
||||||
|
msg.WriteString(" banlist:auth true` (banlist itself must be enabled!)\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.lp.SendNotice(evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
value := utils.SanitizeBoolString(commandSlice[1])
|
||||||
|
cfg.Set(config.BotBanlistAuth, value)
|
||||||
|
err := b.cfg.SetBot(cfg)
|
||||||
|
if err != nil {
|
||||||
|
b.Error(ctx, "cannot set bot config: %v", err)
|
||||||
|
}
|
||||||
|
b.lp.SendNotice(evt.RoomID, "auth banning has been updated", linkpearl.RelatesTo(evt.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) runBanlistAuto(ctx context.Context, commandSlice []string) {
|
||||||
|
evt := eventFromContext(ctx)
|
||||||
|
cfg := b.cfg.GetBot()
|
||||||
|
if len(commandSlice) < 2 {
|
||||||
|
var msg strings.Builder
|
||||||
|
msg.WriteString("Currently: `")
|
||||||
|
msg.WriteString(cfg.Get(config.BotBanlistAuto))
|
||||||
|
msg.WriteString("`\n\n")
|
||||||
|
|
||||||
|
if !cfg.BanlistAuto() {
|
||||||
|
msg.WriteString("To enable automatic banning for invalid emails, send `")
|
||||||
|
msg.WriteString(b.prefix)
|
||||||
|
msg.WriteString(" banlist:auto true` (banlist itself must be enabled!)\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.lp.SendNotice(evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
value := utils.SanitizeBoolString(commandSlice[1])
|
||||||
|
cfg.Set(config.BotBanlistAuto, value)
|
||||||
|
err := b.cfg.SetBot(cfg)
|
||||||
|
if err != nil {
|
||||||
|
b.Error(ctx, "cannot set bot config: %v", err)
|
||||||
|
}
|
||||||
|
b.lp.SendNotice(evt.RoomID, "auto banning has been updated", linkpearl.RelatesTo(evt.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) runBanlistAdd(ctx context.Context, commandSlice []string) {
|
func (b *Bot) runBanlistAdd(ctx context.Context, commandSlice []string) {
|
||||||
@@ -335,7 +411,7 @@ func (b *Bot) runBanlistAdd(ctx context.Context, commandSlice []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !b.cfg.GetBot().BanlistEnabled() {
|
if !b.cfg.GetBot().BanlistEnabled() {
|
||||||
b.SendNotice(ctx, evt.RoomID, "banlist is disabled, you have to enable it first, kupo")
|
b.lp.SendNotice(evt.RoomID, "banlist is disabled, you have to enable it first, kupo", linkpearl.RelatesTo(evt.ID))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
banlist := b.cfg.GetBanlist()
|
banlist := b.cfg.GetBanlist()
|
||||||
@@ -344,7 +420,7 @@ func (b *Bot) runBanlistAdd(ctx context.Context, commandSlice []string) {
|
|||||||
for _, ip := range ips {
|
for _, ip := range ips {
|
||||||
addr, err := net.ResolveIPAddr("ip", ip)
|
addr, err := net.ResolveIPAddr("ip", ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot add %s to banlist: %v", ip, err)
|
b.Error(ctx, "cannot add %s to banlist: %v", ip, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
banlist.Add(addr)
|
banlist.Add(addr)
|
||||||
@@ -352,11 +428,11 @@ func (b *Bot) runBanlistAdd(ctx context.Context, commandSlice []string) {
|
|||||||
|
|
||||||
err := b.cfg.SetBanlist(banlist)
|
err := b.cfg.SetBanlist(banlist)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot set banlist: %v", err)
|
b.Error(ctx, "cannot set banlist: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b.SendNotice(ctx, evt.RoomID, "banlist has been updated, kupo")
|
b.lp.SendNotice(evt.RoomID, "banlist has been updated, kupo", linkpearl.RelatesTo(evt.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) runBanlistRemove(ctx context.Context, commandSlice []string) {
|
func (b *Bot) runBanlistRemove(ctx context.Context, commandSlice []string) {
|
||||||
@@ -366,7 +442,7 @@ func (b *Bot) runBanlistRemove(ctx context.Context, commandSlice []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !b.cfg.GetBot().BanlistEnabled() {
|
if !b.cfg.GetBot().BanlistEnabled() {
|
||||||
b.SendNotice(ctx, evt.RoomID, "banlist is disabled, you have to enable it first, kupo")
|
b.lp.SendNotice(evt.RoomID, "banlist is disabled, you have to enable it first, kupo", linkpearl.RelatesTo(evt.ID))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
banlist := b.cfg.GetBanlist()
|
banlist := b.cfg.GetBanlist()
|
||||||
@@ -375,7 +451,7 @@ func (b *Bot) runBanlistRemove(ctx context.Context, commandSlice []string) {
|
|||||||
for _, ip := range ips {
|
for _, ip := range ips {
|
||||||
addr, err := net.ResolveIPAddr("ip", ip)
|
addr, err := net.ResolveIPAddr("ip", ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot remove %s from banlist: %v", ip, err)
|
b.Error(ctx, "cannot remove %s from banlist: %v", ip, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
banlist.Remove(addr)
|
banlist.Remove(addr)
|
||||||
@@ -383,25 +459,63 @@ func (b *Bot) runBanlistRemove(ctx context.Context, commandSlice []string) {
|
|||||||
|
|
||||||
err := b.cfg.SetBanlist(banlist)
|
err := b.cfg.SetBanlist(banlist)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot set banlist: %v", err)
|
b.Error(ctx, "cannot set banlist: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b.SendNotice(ctx, evt.RoomID, "banlist has been updated, kupo")
|
b.lp.SendNotice(evt.RoomID, "banlist has been updated, kupo", linkpearl.RelatesTo(evt.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) addBanlistTimeline(ctx context.Context, onlyTotals bool) {
|
||||||
|
evt := eventFromContext(ctx)
|
||||||
|
banlist := b.cfg.GetBanlist()
|
||||||
|
timeline := map[string][]string{}
|
||||||
|
for ip, ts := range banlist {
|
||||||
|
key := "???"
|
||||||
|
date, _ := time.ParseInLocation(time.RFC1123Z, ts, time.UTC) //nolint:errcheck // stored in that format
|
||||||
|
if !date.IsZero() {
|
||||||
|
key = date.Truncate(24 * time.Hour).Format(time.DateOnly)
|
||||||
|
}
|
||||||
|
if _, ok := timeline[key]; !ok {
|
||||||
|
timeline[key] = []string{}
|
||||||
|
}
|
||||||
|
timeline[key] = append(timeline[key], ip)
|
||||||
|
}
|
||||||
|
keys := utils.MapKeys(timeline)
|
||||||
|
|
||||||
|
for _, chunk := range utils.Chunks(keys, 7) {
|
||||||
|
var txt strings.Builder
|
||||||
|
for _, day := range chunk {
|
||||||
|
data := timeline[day]
|
||||||
|
sort.Strings(data)
|
||||||
|
txt.WriteString("* `")
|
||||||
|
txt.WriteString(day)
|
||||||
|
if onlyTotals {
|
||||||
|
txt.WriteString("` ")
|
||||||
|
txt.WriteString(strconv.Itoa(len(data)))
|
||||||
|
txt.WriteString(" hosts banned\n")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
txt.WriteString("` `")
|
||||||
|
txt.WriteString(strings.Join(data, "`, `"))
|
||||||
|
txt.WriteString("`\n")
|
||||||
|
}
|
||||||
|
b.lp.SendNotice(evt.RoomID, txt.String(), linkpearl.RelatesTo(evt.ID))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) runBanlistReset(ctx context.Context) {
|
func (b *Bot) runBanlistReset(ctx context.Context) {
|
||||||
evt := eventFromContext(ctx)
|
evt := eventFromContext(ctx)
|
||||||
if !b.cfg.GetBot().BanlistEnabled() {
|
if !b.cfg.GetBot().BanlistEnabled() {
|
||||||
b.SendNotice(ctx, evt.RoomID, "banlist is disabled, you have to enable it first, kupo")
|
b.lp.SendNotice(evt.RoomID, "banlist is disabled, you have to enable it first, kupo", linkpearl.RelatesTo(evt.ID))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := b.cfg.SetBanlist(config.List{})
|
err := b.cfg.SetBanlist(config.List{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot set banlist: %v", err)
|
b.Error(ctx, "cannot set banlist: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b.SendNotice(ctx, evt.RoomID, "banlist has been reset, kupo")
|
b.lp.SendNotice(evt.RoomID, "banlist has been reset, kupo", linkpearl.RelatesTo(evt.ID))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/raja/argon2pw"
|
"github.com/raja/argon2pw"
|
||||||
|
"gitlab.com/etke.cc/linkpearl"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
|
|
||||||
"gitlab.com/etke.cc/postmoogle/bot/config"
|
"gitlab.com/etke.cc/postmoogle/bot/config"
|
||||||
@@ -17,13 +18,13 @@ func (b *Bot) runStop(ctx context.Context) {
|
|||||||
evt := eventFromContext(ctx)
|
evt := eventFromContext(ctx)
|
||||||
cfg, err := b.cfg.GetRoom(evt.RoomID)
|
cfg, err := b.cfg.GetRoom(evt.RoomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "failed to retrieve settings: %v", err)
|
b.Error(ctx, "failed to retrieve settings: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mailbox := cfg.Get(config.RoomMailbox)
|
mailbox := cfg.Get(config.RoomMailbox)
|
||||||
if mailbox == "" {
|
if mailbox == "" {
|
||||||
b.SendNotice(ctx, evt.RoomID, "that room is not configured yet")
|
b.lp.SendNotice(evt.RoomID, "that room is not configured yet", linkpearl.RelatesTo(evt.ID, cfg.NoThreads()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,11 +32,11 @@ func (b *Bot) runStop(ctx context.Context) {
|
|||||||
|
|
||||||
err = b.cfg.SetRoom(evt.RoomID, config.Room{})
|
err = b.cfg.SetRoom(evt.RoomID, config.Room{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot update settings: %v", err)
|
b.Error(ctx, "cannot update settings: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b.SendNotice(ctx, evt.RoomID, "mailbox has been disabled")
|
b.lp.SendNotice(evt.RoomID, "mailbox has been disabled", linkpearl.RelatesTo(evt.ID, cfg.NoThreads()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) handleOption(ctx context.Context, cmd []string) {
|
func (b *Bot) handleOption(ctx context.Context, cmd []string) {
|
||||||
@@ -43,14 +44,23 @@ func (b *Bot) handleOption(ctx context.Context, cmd []string) {
|
|||||||
b.getOption(ctx, cmd[0])
|
b.getOption(ctx, cmd[0])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
b.setOption(ctx, cmd[0], cmd[1])
|
switch cmd[0] {
|
||||||
|
case config.RoomActive:
|
||||||
|
return
|
||||||
|
case config.RoomMailbox:
|
||||||
|
b.setMailbox(ctx, cmd[1])
|
||||||
|
case config.RoomPassword:
|
||||||
|
b.setPassword(ctx)
|
||||||
|
default:
|
||||||
|
b.setOption(ctx, cmd[0], cmd[1])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) getOption(ctx context.Context, name string) {
|
func (b *Bot) getOption(ctx context.Context, name string) {
|
||||||
evt := eventFromContext(ctx)
|
evt := eventFromContext(ctx)
|
||||||
cfg, err := b.cfg.GetRoom(evt.RoomID)
|
cfg, err := b.cfg.GetRoom(evt.RoomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "failed to retrieve settings: %v", err)
|
b.Error(ctx, "failed to retrieve settings: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +73,7 @@ func (b *Bot) getOption(ctx context.Context, name string) {
|
|||||||
msg := fmt.Sprintf("`%s` is not set, kupo.\n"+
|
msg := fmt.Sprintf("`%s` is not set, kupo.\n"+
|
||||||
"To set it, send a `%s %s VALUE` command.",
|
"To set it, send a `%s %s VALUE` command.",
|
||||||
name, b.prefix, name)
|
name, b.prefix, name)
|
||||||
b.SendNotice(ctx, evt.RoomID, msg)
|
b.lp.SendNotice(evt.RoomID, msg, linkpearl.RelatesTo(evt.ID, cfg.NoThreads()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,10 +91,67 @@ func (b *Bot) getOption(ctx context.Context, name string) {
|
|||||||
"or just set a new one with `%s %s NEW_PASSWORD`.",
|
"or just set a new one with `%s %s NEW_PASSWORD`.",
|
||||||
b.prefix, name)
|
b.prefix, name)
|
||||||
}
|
}
|
||||||
b.SendNotice(ctx, evt.RoomID, msg)
|
b.lp.SendNotice(evt.RoomID, msg, linkpearl.RelatesTo(evt.ID, cfg.NoThreads()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) setMailbox(ctx context.Context, value string) {
|
||||||
|
evt := eventFromContext(ctx)
|
||||||
|
existingID, ok := b.getMapping(value)
|
||||||
|
if (ok && existingID != "" && existingID != evt.RoomID) || b.isReserved(value) {
|
||||||
|
b.lp.SendNotice(evt.RoomID, fmt.Sprintf("Mailbox `%s` (%s) already taken, kupo", value, utils.EmailsList(value, "")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := b.cfg.GetRoom(evt.RoomID)
|
||||||
|
if err != nil {
|
||||||
|
b.Error(ctx, "failed to retrieve settings: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
old := cfg.Get(config.RoomMailbox)
|
||||||
|
cfg.Set(config.RoomMailbox, value)
|
||||||
|
cfg.Set(config.RoomOwner, evt.Sender.String())
|
||||||
|
if old != "" {
|
||||||
|
b.rooms.Delete(old)
|
||||||
|
}
|
||||||
|
active := b.ActivateMailbox(evt.Sender, evt.RoomID, value)
|
||||||
|
cfg.Set(config.RoomActive, strconv.FormatBool(active))
|
||||||
|
value = fmt.Sprintf("%s@%s", value, utils.SanitizeDomain(cfg.Domain()))
|
||||||
|
|
||||||
|
err = b.cfg.SetRoom(evt.RoomID, cfg)
|
||||||
|
if err != nil {
|
||||||
|
b.Error(ctx, "cannot update settings: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := fmt.Sprintf("mailbox of this room set to `%s`", value)
|
||||||
|
b.lp.SendNotice(evt.RoomID, msg, linkpearl.RelatesTo(evt.ID, cfg.NoThreads()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) setPassword(ctx context.Context) {
|
||||||
|
evt := eventFromContext(ctx)
|
||||||
|
cfg, err := b.cfg.GetRoom(evt.RoomID)
|
||||||
|
if err != nil {
|
||||||
|
b.Error(ctx, "failed to retrieve settings: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
value := b.parseCommand(evt.Content.AsMessage().Body, false)[1] // get original value, without forced lower case
|
||||||
|
value, err = argon2pw.GenerateSaltedHash(value)
|
||||||
|
if err != nil {
|
||||||
|
b.Error(ctx, "failed to hash password: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Set(config.RoomPassword, value)
|
||||||
|
err = b.cfg.SetRoom(evt.RoomID, cfg)
|
||||||
|
if err != nil {
|
||||||
|
b.Error(ctx, "cannot update settings: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b.lp.SendNotice(evt.RoomID, "SMTP password has been set", linkpearl.RelatesTo(evt.ID, cfg.NoThreads()))
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocognit
|
|
||||||
func (b *Bot) setOption(ctx context.Context, name, value string) {
|
func (b *Bot) setOption(ctx context.Context, name, value string) {
|
||||||
cmd := b.commands.get(name)
|
cmd := b.commands.get(name)
|
||||||
if cmd != nil && cmd.sanitizer != nil {
|
if cmd != nil && cmd.sanitizer != nil {
|
||||||
@@ -92,57 +159,36 @@ func (b *Bot) setOption(ctx context.Context, name, value string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
evt := eventFromContext(ctx)
|
evt := eventFromContext(ctx)
|
||||||
// ignore request
|
|
||||||
if name == config.RoomActive {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
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, "")))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := b.cfg.GetRoom(evt.RoomID)
|
cfg, err := b.cfg.GetRoom(evt.RoomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "failed to retrieve settings: %v", err)
|
b.Error(ctx, "failed to retrieve settings: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if name == config.RoomPassword {
|
if name == config.RoomAutoreply ||
|
||||||
value = b.parseCommand(evt.Content.AsMessage().Body, false)[1] // get original value, without forced lower case
|
name == config.RoomSignature {
|
||||||
value, err = argon2pw.GenerateSaltedHash(value)
|
value = strings.Join(b.parseCommand(evt.Content.AsMessage().Body, false)[1:], " ")
|
||||||
if err != nil {
|
}
|
||||||
b.Error(ctx, evt.RoomID, "failed to hash password: %v", err)
|
|
||||||
return
|
if value == "reset" {
|
||||||
}
|
value = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
old := cfg.Get(name)
|
old := cfg.Get(name)
|
||||||
cfg.Set(name, value)
|
if old == value {
|
||||||
|
b.lp.SendNotice(evt.RoomID, "nothing changed, kupo.", linkpearl.RelatesTo(evt.ID, cfg.NoThreads()))
|
||||||
if name == config.RoomMailbox {
|
return
|
||||||
cfg.Set(config.RoomOwner, evt.Sender.String())
|
|
||||||
if old != "" {
|
|
||||||
b.rooms.Delete(old)
|
|
||||||
}
|
|
||||||
active := b.ActivateMailbox(evt.Sender, evt.RoomID, value)
|
|
||||||
cfg.Set(config.RoomActive, strconv.FormatBool(active))
|
|
||||||
value = fmt.Sprintf("%s@%s", value, utils.SanitizeDomain(cfg.Domain()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg.Set(name, value)
|
||||||
err = b.cfg.SetRoom(evt.RoomID, cfg)
|
err = b.cfg.SetRoom(evt.RoomID, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot update settings: %v", err)
|
b.Error(ctx, "cannot update settings: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := fmt.Sprintf("`%s` of this room set to `%s`", name, value)
|
msg := fmt.Sprintf("`%s` of this room set to `%s`", name, value)
|
||||||
if name == config.RoomPassword {
|
b.lp.SendNotice(evt.RoomID, msg, linkpearl.RelatesTo(evt.ID, cfg.NoThreads()))
|
||||||
msg = "SMTP password has been set"
|
|
||||||
}
|
|
||||||
b.SendNotice(ctx, evt.RoomID, msg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) runSpamlistAdd(ctx context.Context, commandSlice []string) {
|
func (b *Bot) runSpamlistAdd(ctx context.Context, commandSlice []string) {
|
||||||
@@ -151,12 +197,12 @@ func (b *Bot) runSpamlistAdd(ctx context.Context, commandSlice []string) {
|
|||||||
b.getOption(ctx, config.RoomSpamlist)
|
b.getOption(ctx, config.RoomSpamlist)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
roomCfg, err := b.cfg.GetRoom(evt.RoomID)
|
cfg, err := b.cfg.GetRoom(evt.RoomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot get room settings: %v", err)
|
b.Error(ctx, "cannot get room settings: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
spamlist := utils.StringSlice(roomCfg[config.RoomSpamlist])
|
spamlist := utils.StringSlice(cfg[config.RoomSpamlist])
|
||||||
for _, newItem := range commandSlice[1:] {
|
for _, newItem := range commandSlice[1:] {
|
||||||
newItem = strings.TrimSpace(newItem)
|
newItem = strings.TrimSpace(newItem)
|
||||||
if slices.Contains(spamlist, newItem) {
|
if slices.Contains(spamlist, newItem) {
|
||||||
@@ -165,14 +211,19 @@ func (b *Bot) runSpamlistAdd(ctx context.Context, commandSlice []string) {
|
|||||||
spamlist = append(spamlist, newItem)
|
spamlist = append(spamlist, newItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
roomCfg.Set(config.RoomSpamlist, utils.SliceString(spamlist))
|
cfg.Set(config.RoomSpamlist, utils.SliceString(spamlist))
|
||||||
err = b.cfg.SetRoom(evt.RoomID, roomCfg)
|
err = b.cfg.SetRoom(evt.RoomID, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot store room settings: %v", err)
|
b.Error(ctx, "cannot store room settings: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b.SendNotice(ctx, evt.RoomID, "spamlist has been updated, kupo")
|
threadID := threadIDFromContext(ctx)
|
||||||
|
if threadID == "" {
|
||||||
|
threadID = evt.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
b.lp.SendNotice(evt.RoomID, "spamlist has been updated, kupo", linkpearl.RelatesTo(threadID, cfg.NoThreads()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) runSpamlistRemove(ctx context.Context, commandSlice []string) {
|
func (b *Bot) runSpamlistRemove(ctx context.Context, commandSlice []string) {
|
||||||
@@ -181,13 +232,13 @@ func (b *Bot) runSpamlistRemove(ctx context.Context, commandSlice []string) {
|
|||||||
b.getOption(ctx, config.RoomSpamlist)
|
b.getOption(ctx, config.RoomSpamlist)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
roomCfg, err := b.cfg.GetRoom(evt.RoomID)
|
cfg, err := b.cfg.GetRoom(evt.RoomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot get room settings: %v", err)
|
b.Error(ctx, "cannot get room settings: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
toRemove := map[int]struct{}{}
|
toRemove := map[int]struct{}{}
|
||||||
spamlist := utils.StringSlice(roomCfg[config.RoomSpamlist])
|
spamlist := utils.StringSlice(cfg[config.RoomSpamlist])
|
||||||
for _, item := range commandSlice[1:] {
|
for _, item := range commandSlice[1:] {
|
||||||
item = strings.TrimSpace(item)
|
item = strings.TrimSpace(item)
|
||||||
idx := slices.Index(spamlist, item)
|
idx := slices.Index(spamlist, item)
|
||||||
@@ -197,7 +248,7 @@ func (b *Bot) runSpamlistRemove(ctx context.Context, commandSlice []string) {
|
|||||||
toRemove[idx] = struct{}{}
|
toRemove[idx] = struct{}{}
|
||||||
}
|
}
|
||||||
if len(toRemove) == 0 {
|
if len(toRemove) == 0 {
|
||||||
b.SendNotice(ctx, evt.RoomID, "nothing new, kupo.")
|
b.lp.SendNotice(evt.RoomID, "nothing new, kupo.", linkpearl.RelatesTo(evt.ID, cfg.NoThreads()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,35 +260,35 @@ func (b *Bot) runSpamlistRemove(ctx context.Context, commandSlice []string) {
|
|||||||
updatedSpamlist = append(updatedSpamlist, item)
|
updatedSpamlist = append(updatedSpamlist, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
roomCfg.Set(config.RoomSpamlist, utils.SliceString(updatedSpamlist))
|
cfg.Set(config.RoomSpamlist, utils.SliceString(updatedSpamlist))
|
||||||
err = b.cfg.SetRoom(evt.RoomID, roomCfg)
|
err = b.cfg.SetRoom(evt.RoomID, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot store room settings: %v", err)
|
b.Error(ctx, "cannot store room settings: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b.SendNotice(ctx, evt.RoomID, "spamlist has been updated, kupo")
|
b.lp.SendNotice(evt.RoomID, "spamlist has been updated, kupo", linkpearl.RelatesTo(evt.ID, cfg.NoThreads()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) runSpamlistReset(ctx context.Context) {
|
func (b *Bot) runSpamlistReset(ctx context.Context) {
|
||||||
evt := eventFromContext(ctx)
|
evt := eventFromContext(ctx)
|
||||||
roomCfg, err := b.cfg.GetRoom(evt.RoomID)
|
cfg, err := b.cfg.GetRoom(evt.RoomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot get room settings: %v", err)
|
b.Error(ctx, "cannot get room settings: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
spamlist := utils.StringSlice(roomCfg[config.RoomSpamlist])
|
spamlist := utils.StringSlice(cfg[config.RoomSpamlist])
|
||||||
if len(spamlist) == 0 {
|
if len(spamlist) == 0 {
|
||||||
b.SendNotice(ctx, evt.RoomID, "spamlist is empty, kupo.")
|
b.lp.SendNotice(evt.RoomID, "spamlist is empty, kupo.", linkpearl.RelatesTo(evt.ID, cfg.NoThreads()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
roomCfg.Set(config.RoomSpamlist, "")
|
cfg.Set(config.RoomSpamlist, "")
|
||||||
err = b.cfg.SetRoom(evt.RoomID, roomCfg)
|
err = b.cfg.SetRoom(evt.RoomID, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot store room settings: %v", err)
|
b.Error(ctx, "cannot store room settings: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b.SendNotice(ctx, evt.RoomID, "spamlist has been reset, kupo.")
|
b.lp.SendNotice(evt.RoomID, "spamlist has been reset, kupo.", linkpearl.RelatesTo(evt.ID, cfg.NoThreads()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ const (
|
|||||||
BotQueueBatch = "queue:batch"
|
BotQueueBatch = "queue:batch"
|
||||||
BotQueueRetries = "queue:retries"
|
BotQueueRetries = "queue:retries"
|
||||||
BotBanlistEnabled = "banlist:enabled"
|
BotBanlistEnabled = "banlist:enabled"
|
||||||
|
BotBanlistAuto = "banlist:auto"
|
||||||
|
BotBanlistAuth = "banlist:auth"
|
||||||
BotGreylist = "greylist"
|
BotGreylist = "greylist"
|
||||||
BotMautrix015Migration = "mautrix015migration"
|
BotMautrix015Migration = "mautrix015migration"
|
||||||
)
|
)
|
||||||
@@ -72,6 +74,16 @@ func (s Bot) BanlistEnabled() bool {
|
|||||||
return utils.Bool(s.Get(BotBanlistEnabled))
|
return utils.Bool(s.Get(BotBanlistEnabled))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BanlistAuto option
|
||||||
|
func (s Bot) BanlistAuto() bool {
|
||||||
|
return utils.Bool(s.Get(BotBanlistAuto))
|
||||||
|
}
|
||||||
|
|
||||||
|
// BanlistAuth option
|
||||||
|
func (s Bot) BanlistAuth() bool {
|
||||||
|
return utils.Bool(s.Get(BotBanlistAuth))
|
||||||
|
}
|
||||||
|
|
||||||
// Greylist option (duration in minutes)
|
// Greylist option (duration in minutes)
|
||||||
func (s Bot) Greylist() int {
|
func (s Bot) Greylist() int {
|
||||||
return utils.Int(s.Get(BotGreylist))
|
return utils.Int(s.Get(BotGreylist))
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func (m *Manager) GetBot() Bot {
|
|||||||
var config Bot
|
var config Bot
|
||||||
config, err = m.lp.GetAccountData(acBotKey)
|
config, err = m.lp.GetAccountData(acBotKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.log.Error().Err(utils.UnwrapError(err)).Msg("cannot get bot settings")
|
m.log.Error().Err(err).Msg("cannot get bot settings")
|
||||||
}
|
}
|
||||||
if config == nil {
|
if config == nil {
|
||||||
config = make(Bot, 0)
|
config = make(Bot, 0)
|
||||||
@@ -44,7 +44,7 @@ func (m *Manager) GetBot() Bot {
|
|||||||
|
|
||||||
// SetBot config
|
// SetBot config
|
||||||
func (m *Manager) SetBot(cfg Bot) error {
|
func (m *Manager) SetBot(cfg Bot) error {
|
||||||
return utils.UnwrapError(m.lp.SetAccountData(acBotKey, cfg))
|
return m.lp.SetAccountData(acBotKey, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRoom config
|
// GetRoom config
|
||||||
@@ -54,12 +54,12 @@ func (m *Manager) GetRoom(roomID id.RoomID) (Room, error) {
|
|||||||
config = make(Room, 0)
|
config = make(Room, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, utils.UnwrapError(err)
|
return config, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetRoom config
|
// SetRoom config
|
||||||
func (m *Manager) SetRoom(roomID id.RoomID, cfg Room) error {
|
func (m *Manager) SetRoom(roomID id.RoomID, cfg Room) error {
|
||||||
return utils.UnwrapError(m.lp.SetRoomAccountData(roomID, acRoomKey, cfg))
|
return m.lp.SetRoomAccountData(roomID, acRoomKey, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBanlist config
|
// GetBanlist config
|
||||||
@@ -72,7 +72,7 @@ func (m *Manager) GetBanlist() List {
|
|||||||
defer m.mu.Unlock("banlist")
|
defer m.mu.Unlock("banlist")
|
||||||
config, err := m.lp.GetAccountData(acBanlistKey)
|
config, err := m.lp.GetAccountData(acBanlistKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.log.Error().Err(utils.UnwrapError(err)).Msg("cannot get banlist")
|
m.log.Error().Err(err).Msg("cannot get banlist")
|
||||||
}
|
}
|
||||||
if config == nil {
|
if config == nil {
|
||||||
config = make(List, 0)
|
config = make(List, 0)
|
||||||
@@ -93,14 +93,14 @@ func (m *Manager) SetBanlist(cfg List) error {
|
|||||||
cfg = make(List, 0)
|
cfg = make(List, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
return utils.UnwrapError(m.lp.SetAccountData(acBanlistKey, cfg))
|
return m.lp.SetAccountData(acBanlistKey, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetGreylist config
|
// GetGreylist config
|
||||||
func (m *Manager) GetGreylist() List {
|
func (m *Manager) GetGreylist() List {
|
||||||
config, err := m.lp.GetAccountData(acGreylistKey)
|
config, err := m.lp.GetAccountData(acGreylistKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.log.Error().Err(utils.UnwrapError(err)).Msg("cannot get banlist")
|
m.log.Error().Err(err).Msg("cannot get banlist")
|
||||||
}
|
}
|
||||||
if config == nil {
|
if config == nil {
|
||||||
config = make(List, 0)
|
config = make(List, 0)
|
||||||
@@ -112,5 +112,5 @@ func (m *Manager) GetGreylist() List {
|
|||||||
|
|
||||||
// SetGreylist config
|
// SetGreylist config
|
||||||
func (m *Manager) SetGreylist(cfg List) error {
|
func (m *Manager) SetGreylist(cfg List) error {
|
||||||
return utils.UnwrapError(m.lp.SetAccountData(acGreylistKey, cfg))
|
return m.lp.SetAccountData(acGreylistKey, cfg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,26 +14,31 @@ type Room map[string]string
|
|||||||
|
|
||||||
// option keys
|
// option keys
|
||||||
const (
|
const (
|
||||||
RoomActive = ".active"
|
RoomActive = ".active"
|
||||||
RoomOwner = "owner"
|
RoomOwner = "owner"
|
||||||
RoomMailbox = "mailbox"
|
RoomMailbox = "mailbox"
|
||||||
RoomDomain = "domain"
|
RoomDomain = "domain"
|
||||||
RoomNoSend = "nosend"
|
RoomPassword = "password"
|
||||||
RoomNoReplies = "noreplies"
|
RoomSignature = "signature"
|
||||||
RoomNoCC = "nocc"
|
RoomAutoreply = "autoreply"
|
||||||
RoomNoSender = "nosender"
|
|
||||||
RoomNoRecipient = "norecipient"
|
RoomNoCC = "nocc"
|
||||||
RoomNoSubject = "nosubject"
|
RoomNoFiles = "nofiles"
|
||||||
RoomNoHTML = "nohtml"
|
RoomNoHTML = "nohtml"
|
||||||
RoomNoThreads = "nothreads"
|
RoomNoInlines = "noinlines"
|
||||||
RoomNoFiles = "nofiles"
|
RoomNoRecipient = "norecipient"
|
||||||
RoomNoInlines = "noinlines"
|
RoomNoReplies = "noreplies"
|
||||||
RoomPassword = "password"
|
RoomNoSend = "nosend"
|
||||||
|
RoomNoSender = "nosender"
|
||||||
|
RoomNoSubject = "nosubject"
|
||||||
|
RoomNoThreads = "nothreads"
|
||||||
|
|
||||||
RoomSpamcheckDKIM = "spamcheck:dkim"
|
RoomSpamcheckDKIM = "spamcheck:dkim"
|
||||||
|
RoomSpamcheckMX = "spamcheck:mx"
|
||||||
RoomSpamcheckSMTP = "spamcheck:smtp"
|
RoomSpamcheckSMTP = "spamcheck:smtp"
|
||||||
RoomSpamcheckSPF = "spamcheck:spf"
|
RoomSpamcheckSPF = "spamcheck:spf"
|
||||||
RoomSpamcheckMX = "spamcheck:mx"
|
|
||||||
RoomSpamlist = "spamlist"
|
RoomSpamlist = "spamlist"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get option
|
// Get option
|
||||||
@@ -66,6 +71,14 @@ func (s Room) Password() string {
|
|||||||
return s.Get(RoomPassword)
|
return s.Get(RoomPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s Room) Signature() string {
|
||||||
|
return s.Get(RoomSignature)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Room) Autoreply() string {
|
||||||
|
return s.Get(RoomAutoreply)
|
||||||
|
}
|
||||||
|
|
||||||
func (s Room) NoSend() bool {
|
func (s Room) NoSend() bool {
|
||||||
return utils.Bool(s.Get(RoomNoSend))
|
return utils.Bool(s.Get(RoomNoSend))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import (
|
|||||||
|
|
||||||
"github.com/getsentry/sentry-go"
|
"github.com/getsentry/sentry-go"
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ctxkey int
|
type ctxkey int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ctxEvent ctxkey = iota
|
ctxEvent ctxkey = iota
|
||||||
|
ctxThreadID ctxkey = iota
|
||||||
)
|
)
|
||||||
|
|
||||||
func newContext(evt *event.Event) context.Context {
|
func newContext(evt *event.Event) context.Context {
|
||||||
@@ -49,3 +51,21 @@ func eventToContext(ctx context.Context, evt *event.Event) context.Context {
|
|||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func threadIDToContext(ctx context.Context, threadID id.EventID) context.Context {
|
||||||
|
return context.WithValue(ctx, ctxThreadID, threadID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func threadIDFromContext(ctx context.Context) id.EventID {
|
||||||
|
v := ctx.Value(ctxThreadID)
|
||||||
|
if v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
threadID, ok := v.(id.EventID)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return threadID
|
||||||
|
}
|
||||||
|
|||||||
165
bot/email.go
165
bot/email.go
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gitlab.com/etke.cc/linkpearl"
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
"maunium.net/go/mautrix/format"
|
"maunium.net/go/mautrix/format"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
@@ -111,6 +112,8 @@ func (b *Bot) GetIFOptions(roomID id.RoomID) email.IncomingFilteringOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IncomingEmail sends incoming email to matrix room
|
// IncomingEmail sends incoming email to matrix room
|
||||||
|
//
|
||||||
|
//nolint:gocognit // TODO
|
||||||
func (b *Bot) IncomingEmail(ctx context.Context, email *email.Email) error {
|
func (b *Bot) IncomingEmail(ctx context.Context, email *email.Email) error {
|
||||||
roomID, ok := b.GetMapping(email.Mailbox(true))
|
roomID, ok := b.GetMapping(email.Mailbox(true))
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -118,16 +121,19 @@ func (b *Bot) IncomingEmail(ctx context.Context, email *email.Email) error {
|
|||||||
}
|
}
|
||||||
cfg, err := b.cfg.GetRoom(roomID)
|
cfg, err := b.cfg.GetRoom(roomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, roomID, "cannot get settings: %v", err)
|
b.Error(ctx, "cannot get settings: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.mu.Lock(roomID.String())
|
b.mu.Lock(roomID.String())
|
||||||
defer b.mu.Unlock(roomID.String())
|
defer b.mu.Unlock(roomID.String())
|
||||||
|
|
||||||
var threadID id.EventID
|
var threadID id.EventID
|
||||||
|
newThread := true
|
||||||
if email.InReplyTo != "" || email.References != "" {
|
if email.InReplyTo != "" || email.References != "" {
|
||||||
threadID = b.getThreadID(roomID, email.InReplyTo, email.References)
|
threadID = b.getThreadID(roomID, email.InReplyTo, email.References)
|
||||||
if threadID != "" {
|
if threadID != "" {
|
||||||
|
newThread = false
|
||||||
|
ctx = threadIDToContext(ctx, threadID)
|
||||||
b.setThreadID(roomID, email.MessageID, threadID)
|
b.setThreadID(roomID, email.MessageID, threadID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,12 +141,14 @@ func (b *Bot) IncomingEmail(ctx context.Context, email *email.Email) error {
|
|||||||
eventID, serr := b.lp.Send(roomID, content)
|
eventID, serr := b.lp.Send(roomID, content)
|
||||||
if serr != nil {
|
if serr != nil {
|
||||||
if !strings.Contains(serr.Error(), "M_UNKNOWN") { // if it's not an unknown event event error
|
if !strings.Contains(serr.Error(), "M_UNKNOWN") { // if it's not an unknown event event error
|
||||||
return utils.UnwrapError(serr)
|
return serr
|
||||||
}
|
}
|
||||||
threadID = "" // unknown event edge case - remove existing thread ID to avoid complications
|
threadID = "" // unknown event edge case - remove existing thread ID to avoid complications
|
||||||
|
newThread = true
|
||||||
}
|
}
|
||||||
if threadID == "" {
|
if threadID == "" {
|
||||||
threadID = eventID
|
threadID = eventID
|
||||||
|
ctx = threadIDToContext(ctx, threadID)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.setThreadID(roomID, email.MessageID, threadID)
|
b.setThreadID(roomID, email.MessageID, threadID)
|
||||||
@@ -154,26 +162,119 @@ func (b *Bot) IncomingEmail(ctx context.Context, email *email.Email) error {
|
|||||||
b.sendFiles(ctx, roomID, email.Files, cfg.NoThreads(), threadID)
|
b.sendFiles(ctx, roomID, email.Files, cfg.NoThreads(), threadID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if newThread && cfg.Autoreply() != "" {
|
||||||
|
b.sendAutoreply(roomID, threadID)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendEmailReply sends replies from matrix thread to email thread
|
//nolint:gocognit // TODO
|
||||||
func (b *Bot) SendEmailReply(ctx context.Context) {
|
func (b *Bot) sendAutoreply(roomID id.RoomID, threadID id.EventID) {
|
||||||
evt := eventFromContext(ctx)
|
cfg, err := b.cfg.GetRoom(roomID)
|
||||||
if !b.allowSend(evt.Sender, evt.RoomID) {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !b.allowReply(evt.Sender, evt.RoomID) {
|
|
||||||
|
text := cfg.Autoreply()
|
||||||
|
if text == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
threadEvt, err := b.lp.GetClient().GetEvent(roomID, threadID)
|
||||||
|
if err != nil {
|
||||||
|
b.log.Error().Err(err).Msg("cannot get thread event for autoreply")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
evt := &event.Event{
|
||||||
|
ID: threadID + "-autoreply",
|
||||||
|
RoomID: roomID,
|
||||||
|
Content: event.Content{
|
||||||
|
Parsed: &event.MessageEventContent{
|
||||||
|
RelatesTo: &event.RelatesTo{
|
||||||
|
Type: event.RelThread,
|
||||||
|
EventID: threadID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
meta := b.getParentEmail(evt, cfg.Mailbox())
|
||||||
|
|
||||||
|
if meta.To == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if meta.ThreadID == "" {
|
||||||
|
meta.ThreadID = threadID
|
||||||
|
}
|
||||||
|
if meta.Subject == "" {
|
||||||
|
meta.Subject = "Automatic response"
|
||||||
|
}
|
||||||
|
content := format.RenderMarkdown(text, true, true)
|
||||||
|
signature := format.RenderMarkdown(cfg.Signature(), true, true)
|
||||||
|
body := content.Body
|
||||||
|
if signature.Body != "" {
|
||||||
|
body += "\n\n---\n" + signature.Body
|
||||||
|
}
|
||||||
|
var htmlBody string
|
||||||
|
if !cfg.NoHTML() {
|
||||||
|
htmlBody = content.FormattedBody
|
||||||
|
if htmlBody != "" && signature.FormattedBody != "" {
|
||||||
|
htmlBody += "<br><hr><br>" + signature.FormattedBody
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
meta.MessageID = email.MessageID(evt.ID, meta.FromDomain)
|
||||||
|
meta.References = meta.References + " " + meta.MessageID
|
||||||
|
b.log.Info().Any("meta", meta).Msg("sending automatic reply")
|
||||||
|
eml := email.New(meta.MessageID, meta.InReplyTo, meta.References, meta.Subject, meta.From, meta.To, meta.RcptTo, meta.CC, body, htmlBody, nil, nil)
|
||||||
|
data := eml.Compose(b.cfg.GetBot().DKIMPrivateKey())
|
||||||
|
if data == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var queued bool
|
||||||
|
ctx := newContext(threadEvt)
|
||||||
|
recipients := meta.Recipients
|
||||||
|
for _, to := range recipients {
|
||||||
|
queued, err = b.Sendmail(evt.ID, meta.From, to, data)
|
||||||
|
if queued {
|
||||||
|
b.log.Info().Err(err).Str("from", meta.From).Str("to", to).Msg("email has been queued")
|
||||||
|
b.saveSentMetadata(ctx, queued, meta.ThreadID, recipients, eml, cfg, "Autoreply has been sent (queued)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
b.Error(ctx, "cannot send email: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.saveSentMetadata(ctx, queued, meta.ThreadID, recipients, eml, cfg, "Autoreply has been sent")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) canReply(sender id.UserID, roomID id.RoomID) bool {
|
||||||
|
return b.allowSend(sender, roomID) && b.allowReply(sender, roomID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendEmailReply sends replies from matrix thread to email thread
|
||||||
|
//
|
||||||
|
//nolint:gocognit // TODO
|
||||||
|
func (b *Bot) SendEmailReply(ctx context.Context) {
|
||||||
|
evt := eventFromContext(ctx)
|
||||||
|
if !b.canReply(evt.Sender, evt.RoomID) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cfg, err := b.cfg.GetRoom(evt.RoomID)
|
cfg, err := b.cfg.GetRoom(evt.RoomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot retrieve room settings: %v", err)
|
b.Error(ctx, "cannot retrieve room settings: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mailbox := cfg.Mailbox()
|
mailbox := cfg.Mailbox()
|
||||||
if mailbox == "" {
|
if mailbox == "" {
|
||||||
b.Error(ctx, evt.RoomID, "mailbox is not configured, kupo")
|
b.Error(ctx, "mailbox is not configured, kupo")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,21 +284,29 @@ func (b *Bot) SendEmailReply(ctx context.Context) {
|
|||||||
meta := b.getParentEmail(evt, mailbox)
|
meta := b.getParentEmail(evt, mailbox)
|
||||||
|
|
||||||
if meta.To == "" {
|
if meta.To == "" {
|
||||||
b.Error(ctx, evt.RoomID, "cannot find parent email and continue the thread. Please, start a new email thread")
|
b.Error(ctx, "cannot find parent email and continue the thread. Please, start a new email thread")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if meta.ThreadID == "" {
|
if meta.ThreadID == "" {
|
||||||
meta.ThreadID = b.getThreadID(evt.RoomID, meta.InReplyTo, meta.References)
|
meta.ThreadID = b.getThreadID(evt.RoomID, meta.InReplyTo, meta.References)
|
||||||
|
ctx = threadIDToContext(ctx, meta.ThreadID)
|
||||||
}
|
}
|
||||||
content := evt.Content.AsMessage()
|
content := evt.Content.AsMessage()
|
||||||
if meta.Subject == "" {
|
if meta.Subject == "" {
|
||||||
meta.Subject = strings.SplitN(content.Body, "\n", 1)[0]
|
meta.Subject = strings.SplitN(content.Body, "\n", 1)[0]
|
||||||
}
|
}
|
||||||
|
signature := format.RenderMarkdown(cfg.Signature(), true, true)
|
||||||
body := content.Body
|
body := content.Body
|
||||||
|
if signature.Body != "" {
|
||||||
|
body += "\n\n---\n" + signature.Body
|
||||||
|
}
|
||||||
var htmlBody string
|
var htmlBody string
|
||||||
if !cfg.NoHTML() {
|
if !cfg.NoHTML() {
|
||||||
htmlBody = content.FormattedBody
|
htmlBody = content.FormattedBody
|
||||||
|
if htmlBody != "" && signature.FormattedBody != "" {
|
||||||
|
htmlBody += "<br><hr><br>" + signature.FormattedBody
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
meta.MessageID = email.MessageID(evt.ID, meta.FromDomain)
|
meta.MessageID = email.MessageID(evt.ID, meta.FromDomain)
|
||||||
@@ -206,7 +315,7 @@ func (b *Bot) SendEmailReply(ctx context.Context) {
|
|||||||
eml := email.New(meta.MessageID, meta.InReplyTo, meta.References, meta.Subject, meta.From, meta.To, meta.RcptTo, meta.CC, body, htmlBody, nil, nil)
|
eml := email.New(meta.MessageID, meta.InReplyTo, meta.References, meta.Subject, meta.From, meta.To, meta.RcptTo, meta.CC, body, htmlBody, nil, nil)
|
||||||
data := eml.Compose(b.cfg.GetBot().DKIMPrivateKey())
|
data := eml.Compose(b.cfg.GetBot().DKIMPrivateKey())
|
||||||
if data == "" {
|
if data == "" {
|
||||||
b.SendError(ctx, evt.RoomID, "email body is empty")
|
b.lp.SendNotice(evt.RoomID, "email body is empty", linkpearl.RelatesTo(meta.ThreadID, cfg.NoThreads()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +330,7 @@ func (b *Bot) SendEmailReply(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot send email: %v", err)
|
b.Error(ctx, "cannot send email: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -320,7 +429,7 @@ func (e *parentEmail) calculateRecipients(from string, forwardedFrom []string) {
|
|||||||
|
|
||||||
func (b *Bot) getParentEvent(evt *event.Event) (id.EventID, *event.Event) {
|
func (b *Bot) getParentEvent(evt *event.Event) (id.EventID, *event.Event) {
|
||||||
content := evt.Content.AsMessage()
|
content := evt.Content.AsMessage()
|
||||||
threadID := utils.EventParent(evt.ID, content)
|
threadID := linkpearl.EventParent(evt.ID, content)
|
||||||
b.log.Debug().Str("eventID", evt.ID.String()).Str("threadID", threadID.String()).Msg("looking up for the parent event within thread")
|
b.log.Debug().Str("eventID", evt.ID.String()).Str("threadID", threadID.String()).Msg("looking up for the parent event within thread")
|
||||||
if threadID == evt.ID {
|
if threadID == evt.ID {
|
||||||
b.log.Debug().Str("eventID", evt.ID.String()).Msg("event is the thread itself")
|
b.log.Debug().Str("eventID", evt.ID.String()).Msg("event is the thread itself")
|
||||||
@@ -336,7 +445,7 @@ func (b *Bot) getParentEvent(evt *event.Event) (id.EventID, *event.Event) {
|
|||||||
b.log.Error().Err(err).Msg("cannot get parent event")
|
b.log.Error().Err(err).Msg("cannot get parent event")
|
||||||
return threadID, nil
|
return threadID, nil
|
||||||
}
|
}
|
||||||
utils.ParseContent(parentEvt, parentEvt.Type)
|
linkpearl.ParseContent(parentEvt, parentEvt.Type, b.log)
|
||||||
|
|
||||||
if !b.lp.GetMachine().StateStore.IsEncrypted(evt.RoomID) {
|
if !b.lp.GetMachine().StateStore.IsEncrypted(evt.RoomID) {
|
||||||
return threadID, parentEvt
|
return threadID, parentEvt
|
||||||
@@ -362,12 +471,12 @@ func (b *Bot) getParentEmail(evt *event.Event, newFromMailbox string) *parentEma
|
|||||||
return parent
|
return parent
|
||||||
}
|
}
|
||||||
|
|
||||||
parent.From = utils.EventField[string](&parentEvt.Content, eventFromKey)
|
parent.From = linkpearl.EventField[string](&parentEvt.Content, eventFromKey)
|
||||||
parent.To = utils.EventField[string](&parentEvt.Content, eventToKey)
|
parent.To = linkpearl.EventField[string](&parentEvt.Content, eventToKey)
|
||||||
parent.CC = utils.EventField[string](&parentEvt.Content, eventCcKey)
|
parent.CC = linkpearl.EventField[string](&parentEvt.Content, eventCcKey)
|
||||||
parent.RcptTo = utils.EventField[string](&parentEvt.Content, eventRcptToKey)
|
parent.RcptTo = linkpearl.EventField[string](&parentEvt.Content, eventRcptToKey)
|
||||||
parent.InReplyTo = utils.EventField[string](&parentEvt.Content, eventMessageIDkey)
|
parent.InReplyTo = linkpearl.EventField[string](&parentEvt.Content, eventMessageIDkey)
|
||||||
parent.References = utils.EventField[string](&parentEvt.Content, eventReferencesKey)
|
parent.References = linkpearl.EventField[string](&parentEvt.Content, eventReferencesKey)
|
||||||
senderEmail := parent.fixtofrom(newFromMailbox, b.domains)
|
senderEmail := parent.fixtofrom(newFromMailbox, b.domains)
|
||||||
parent.calculateRecipients(senderEmail, b.mbxc.Forwarded)
|
parent.calculateRecipients(senderEmail, b.mbxc.Forwarded)
|
||||||
parent.MessageID = email.MessageID(parentEvt.ID, parent.FromDomain)
|
parent.MessageID = email.MessageID(parentEvt.ID, parent.FromDomain)
|
||||||
@@ -378,7 +487,7 @@ func (b *Bot) getParentEmail(evt *event.Event, newFromMailbox string) *parentEma
|
|||||||
parent.References = " " + parent.MessageID
|
parent.References = " " + parent.MessageID
|
||||||
}
|
}
|
||||||
|
|
||||||
parent.Subject = utils.EventField[string](&parentEvt.Content, eventSubjectKey)
|
parent.Subject = linkpearl.EventField[string](&parentEvt.Content, eventSubjectKey)
|
||||||
if parent.Subject != "" {
|
if parent.Subject != "" {
|
||||||
parent.Subject = "Re: " + parent.Subject
|
parent.Subject = "Re: " + parent.Subject
|
||||||
} else {
|
} else {
|
||||||
@@ -390,28 +499,32 @@ 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
|
// 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
|
// 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 config.Room) {
|
func (b *Bot) saveSentMetadata(ctx context.Context, queued bool, threadID id.EventID, recipients []string, eml *email.Email, cfg config.Room, textOverride ...string) {
|
||||||
addrs := strings.Join(recipients, ", ")
|
addrs := strings.Join(recipients, ", ")
|
||||||
text := "Email has been sent to " + addrs
|
text := "Email has been sent to " + addrs
|
||||||
if queued {
|
if queued {
|
||||||
text = "Email to " + addrs + " has been queued"
|
text = "Email to " + addrs + " has been queued"
|
||||||
}
|
}
|
||||||
|
if len(textOverride) > 0 {
|
||||||
|
text = textOverride[0]
|
||||||
|
}
|
||||||
|
|
||||||
evt := eventFromContext(ctx)
|
evt := eventFromContext(ctx)
|
||||||
content := eml.Content(threadID, cfg.ContentOptions())
|
content := eml.Content(threadID, cfg.ContentOptions())
|
||||||
notice := format.RenderMarkdown(text, true, true)
|
notice := format.RenderMarkdown(text, true, true)
|
||||||
msgContent, ok := content.Parsed.(*event.MessageEventContent)
|
msgContent, ok := content.Parsed.(*event.MessageEventContent)
|
||||||
if !ok {
|
if !ok {
|
||||||
b.Error(ctx, evt.RoomID, "cannot parse message")
|
b.Error(ctx, "cannot parse message")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
msgContent.MsgType = event.MsgNotice
|
msgContent.MsgType = event.MsgNotice
|
||||||
msgContent.Body = notice.Body
|
msgContent.Body = notice.Body
|
||||||
msgContent.FormattedBody = notice.FormattedBody
|
msgContent.FormattedBody = notice.FormattedBody
|
||||||
|
msgContent.RelatesTo = linkpearl.RelatesTo(threadID, cfg.NoThreads())
|
||||||
content.Parsed = msgContent
|
content.Parsed = msgContent
|
||||||
msgID, err := b.lp.Send(evt.RoomID, content)
|
msgID, err := b.lp.Send(evt.RoomID, content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot send notice: %v", err)
|
b.Error(ctx, "cannot send notice: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
domain := utils.SanitizeDomain(cfg.Domain())
|
domain := utils.SanitizeDomain(cfg.Domain())
|
||||||
@@ -423,9 +536,9 @@ func (b *Bot) saveSentMetadata(ctx context.Context, queued bool, threadID id.Eve
|
|||||||
func (b *Bot) sendFiles(ctx context.Context, roomID id.RoomID, files []*utils.File, noThreads bool, parentID id.EventID) {
|
func (b *Bot) sendFiles(ctx context.Context, roomID id.RoomID, files []*utils.File, noThreads bool, parentID id.EventID) {
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
req := file.Convert()
|
req := file.Convert()
|
||||||
err := b.lp.SendFile(roomID, req, file.MsgType, utils.RelatesTo(!noThreads, parentID))
|
err := b.lp.SendFile(roomID, req, file.MsgType, linkpearl.RelatesTo(parentID, noThreads))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, roomID, "cannot upload file %s: %v", req.FileName, err)
|
b.Error(ctx, "cannot upload file %s: %v", req.FileName, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
44
bot/reaction.go
Normal file
44
bot/reaction.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitlab.com/etke.cc/linkpearl"
|
||||||
|
"maunium.net/go/mautrix/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
var supportedReactions = map[string]string{
|
||||||
|
"⛔️": commandSpamlistAdd,
|
||||||
|
"🛑": commandSpamlistAdd,
|
||||||
|
"🚫": commandSpamlistAdd,
|
||||||
|
"spam": commandSpamlistAdd,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) handleReaction(ctx context.Context) {
|
||||||
|
evt := eventFromContext(ctx)
|
||||||
|
content := evt.Content.AsReaction()
|
||||||
|
action, ok := supportedReactions[content.GetRelatesTo().Key]
|
||||||
|
if !ok { // cannot do anything with it
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
srcID := content.GetRelatesTo().EventID
|
||||||
|
srcEvt, err := b.lp.GetClient().GetEvent(evt.RoomID, srcID)
|
||||||
|
if err != nil {
|
||||||
|
b.Error(ctx, "cannot find event %s: %v", srcID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
threadID := linkpearl.EventParent(srcID, srcEvt.Content.AsMessage())
|
||||||
|
ctx = threadIDToContext(ctx, threadID)
|
||||||
|
linkpearl.ParseContent(evt, event.EventMessage, b.log)
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case commandSpamlistAdd:
|
||||||
|
sender := linkpearl.EventField[string](&srcEvt.Content, eventFromKey)
|
||||||
|
if sender == "" {
|
||||||
|
b.Error(ctx, "cannot get sender of the email")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.runSpamlistAdd(ctx, []string{commandSpamlistAdd, linkpearl.EventField[string](&srcEvt.Content, eventFromKey)})
|
||||||
|
}
|
||||||
|
}
|
||||||
27
bot/sync.go
27
bot/sync.go
@@ -21,7 +21,14 @@ func (b *Bot) initSync() {
|
|||||||
event.EventMessage,
|
event.EventMessage,
|
||||||
func(_ mautrix.EventSource, evt *event.Event) {
|
func(_ mautrix.EventSource, evt *event.Event) {
|
||||||
go b.onMessage(evt)
|
go b.onMessage(evt)
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
b.lp.OnEventType(
|
||||||
|
event.EventReaction,
|
||||||
|
func(_ mautrix.EventSource, evt *event.Event) {
|
||||||
|
go b.onReaction(evt)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// joinPermit is called by linkpearl when processing "invite" events and deciding if rooms should be auto-joined or not
|
// joinPermit is called by linkpearl when processing "invite" events and deciding if rooms should be auto-joined or not
|
||||||
@@ -69,6 +76,20 @@ func (b *Bot) onMessage(evt *event.Event) {
|
|||||||
b.handle(ctx)
|
b.handle(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Bot) onReaction(evt *event.Event) {
|
||||||
|
// ignore own messages
|
||||||
|
if evt.Sender == b.lp.GetClient().UserID {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// mautrix 0.15.x migration
|
||||||
|
if b.ignoreBefore >= evt.Timestamp {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := newContext(evt)
|
||||||
|
b.handleReaction(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
// onBotJoin handles the "bot joined the room" event
|
// onBotJoin handles the "bot joined the room" event
|
||||||
func (b *Bot) onBotJoin(ctx context.Context) {
|
func (b *Bot) onBotJoin(ctx context.Context) {
|
||||||
evt := eventFromContext(ctx)
|
evt := eventFromContext(ctx)
|
||||||
@@ -80,7 +101,7 @@ func (b *Bot) onBotJoin(ctx context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b.sendIntroduction(ctx, evt.RoomID)
|
b.sendIntroduction(evt.RoomID)
|
||||||
b.sendHelp(ctx)
|
b.sendHelp(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +124,7 @@ func (b *Bot) onLeave(ctx context.Context) {
|
|||||||
b.runStop(ctx)
|
b.runStop(ctx)
|
||||||
_, err := b.lp.GetClient().LeaveRoom(evt.RoomID)
|
_, err := b.lp.GetClient().LeaveRoom(evt.RoomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot leave empty room: %v", err)
|
b.Error(ctx, "cannot leave empty room: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ func main() {
|
|||||||
|
|
||||||
cfg := config.New()
|
cfg := config.New()
|
||||||
initLog(cfg)
|
initLog(cfg)
|
||||||
utils.SetLogger(&log)
|
|
||||||
utils.SetDomains(cfg.Domains)
|
utils.SetDomains(cfg.Domains)
|
||||||
|
|
||||||
log.Info().Msg("#############################")
|
log.Info().Msg("#############################")
|
||||||
|
|||||||
2
e2e/send
2
e2e/send
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
ssmtp -v test@localhost < $1
|
ssmtp -v test+sub@localhost < $1
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ Content-Type: multipart/alternative; boundary="Apple-Mail=_E091454E-BCFA-43B4-99
|
|||||||
Subject: MIME test 1
|
Subject: MIME test 1
|
||||||
Date: Sat, 13 Oct 2012 15:33:07 -0700
|
Date: Sat, 13 Oct 2012 15:33:07 -0700
|
||||||
Message-Id: <4E2E5A48-1A2C-4450-8663-D41B451DA93A@makita.skynet>
|
Message-Id: <4E2E5A48-1A2C-4450-8663-D41B451DA93A@makita.skynet>
|
||||||
To: test@localhost
|
To: test+sub@localhost
|
||||||
Mime-Version: 1.0 (Apple Message framework v1283)
|
Mime-Version: 1.0 (Apple Message framework v1283)
|
||||||
X-Mailer: Apple Mail (2.1283)
|
X-Mailer: Apple Mail (2.1283)
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/emersion/go-msgauth/dkim"
|
"github.com/emersion/go-msgauth/dkim"
|
||||||
"github.com/jhillyerd/enmime"
|
"github.com/jhillyerd/enmime"
|
||||||
|
"gitlab.com/etke.cc/linkpearl"
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
"maunium.net/go/mautrix/format"
|
"maunium.net/go/mautrix/format"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
@@ -107,15 +108,21 @@ func (e *Email) Mailbox(incoming bool) string {
|
|||||||
return utils.Mailbox(e.From)
|
return utils.Mailbox(e.From)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content converts the email object to a Matrix event content
|
func (e *Email) contentHeader(threadID id.EventID, text *strings.Builder, options *ContentOptions) {
|
||||||
func (e *Email) Content(threadID id.EventID, options *ContentOptions) *event.Content {
|
|
||||||
var text strings.Builder
|
|
||||||
if options.Sender {
|
if options.Sender {
|
||||||
text.WriteString(e.From)
|
text.WriteString(e.From)
|
||||||
}
|
}
|
||||||
if options.Recipient {
|
if options.Recipient {
|
||||||
|
mailbox, sub, host := utils.EmailParts(e.To)
|
||||||
text.WriteString(" ➡️ ")
|
text.WriteString(" ➡️ ")
|
||||||
text.WriteString(e.To)
|
text.WriteString(mailbox)
|
||||||
|
text.WriteString("@")
|
||||||
|
text.WriteString(host)
|
||||||
|
if sub != "" {
|
||||||
|
text.WriteString(" (")
|
||||||
|
text.WriteString(sub)
|
||||||
|
text.WriteString(")")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if options.CC && len(e.CC) > 0 {
|
if options.CC && len(e.CC) > 0 {
|
||||||
text.WriteString("\ncc: ")
|
text.WriteString("\ncc: ")
|
||||||
@@ -129,6 +136,14 @@ func (e *Email) Content(threadID id.EventID, options *ContentOptions) *event.Con
|
|||||||
text.WriteString(e.Subject)
|
text.WriteString(e.Subject)
|
||||||
text.WriteString("\n\n")
|
text.WriteString("\n\n")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content converts the email object to a Matrix event content
|
||||||
|
func (e *Email) Content(threadID id.EventID, options *ContentOptions) *event.Content {
|
||||||
|
var text strings.Builder
|
||||||
|
|
||||||
|
e.contentHeader(threadID, &text, options)
|
||||||
|
|
||||||
if e.HTML != "" && options.HTML {
|
if e.HTML != "" && options.HTML {
|
||||||
text.WriteString(format.HTMLToMarkdown(e.HTML))
|
text.WriteString(format.HTMLToMarkdown(e.HTML))
|
||||||
} else {
|
} else {
|
||||||
@@ -136,7 +151,7 @@ func (e *Email) Content(threadID id.EventID, options *ContentOptions) *event.Con
|
|||||||
}
|
}
|
||||||
|
|
||||||
parsed := format.RenderMarkdown(text.String(), true, true)
|
parsed := format.RenderMarkdown(text.String(), true, true)
|
||||||
parsed.RelatesTo = utils.RelatesTo(options.Threads, threadID)
|
parsed.RelatesTo = linkpearl.RelatesTo(threadID, !options.Threads)
|
||||||
|
|
||||||
var cc string
|
var cc string
|
||||||
if len(e.CC) > 0 {
|
if len(e.CC) > 0 {
|
||||||
|
|||||||
3
go.mod
3
go.mod
@@ -15,6 +15,7 @@ require (
|
|||||||
github.com/jhillyerd/enmime v0.10.0
|
github.com/jhillyerd/enmime v0.10.0
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
github.com/mattn/go-sqlite3 v1.14.17
|
github.com/mattn/go-sqlite3 v1.14.17
|
||||||
|
github.com/mcnijman/go-emailaddress v1.1.0
|
||||||
github.com/mileusna/crontab v1.2.0
|
github.com/mileusna/crontab v1.2.0
|
||||||
github.com/raja/argon2pw v1.0.2-0.20210910183755-a391af63bd39
|
github.com/raja/argon2pw v1.0.2-0.20210910183755-a391af63bd39
|
||||||
github.com/rs/zerolog v1.30.0
|
github.com/rs/zerolog v1.30.0
|
||||||
@@ -25,7 +26,7 @@ require (
|
|||||||
gitlab.com/etke.cc/go/secgen v1.1.1
|
gitlab.com/etke.cc/go/secgen v1.1.1
|
||||||
gitlab.com/etke.cc/go/trysmtp v1.1.3
|
gitlab.com/etke.cc/go/trysmtp v1.1.3
|
||||||
gitlab.com/etke.cc/go/validator v1.0.6
|
gitlab.com/etke.cc/go/validator v1.0.6
|
||||||
gitlab.com/etke.cc/linkpearl v0.0.0-20230920071429-25fe33ba08d0
|
gitlab.com/etke.cc/linkpearl v0.0.0-20230928120707-1e99315dc616
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
|
||||||
maunium.net/go/mautrix v0.16.1
|
maunium.net/go/mautrix v0.16.1
|
||||||
)
|
)
|
||||||
|
|||||||
7
go.sum
7
go.sum
@@ -58,6 +58,8 @@ github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxm
|
|||||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
|
github.com/mcnijman/go-emailaddress v1.1.0 h1:7/Uxgn9pXwXmvXsFSgORo6XoRTrttj7AGmmB2yFArAg=
|
||||||
|
github.com/mcnijman/go-emailaddress v1.1.0/go.mod h1:m+aauxGmv31sB5zZ1I8ICcMoa9ZHOA9RiurCijfvkhI=
|
||||||
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE=
|
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE=
|
||||||
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ=
|
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ=
|
||||||
github.com/mileusna/crontab v1.2.0 h1:x9ZmE2A4p6CDqMEGQ+GbqsNtnmbdmWMQYShdQu8LvrU=
|
github.com/mileusna/crontab v1.2.0 h1:x9ZmE2A4p6CDqMEGQ+GbqsNtnmbdmWMQYShdQu8LvrU=
|
||||||
@@ -109,8 +111,8 @@ gitlab.com/etke.cc/go/trysmtp v1.1.3 h1:e2EHond77onMaecqCg6mWumffTSEf+ycgj88nbee
|
|||||||
gitlab.com/etke.cc/go/trysmtp v1.1.3/go.mod h1:lOO7tTdAE0a3ETV3wN3GJ7I1Tqewu7YTpPWaOmTteV0=
|
gitlab.com/etke.cc/go/trysmtp v1.1.3/go.mod h1:lOO7tTdAE0a3ETV3wN3GJ7I1Tqewu7YTpPWaOmTteV0=
|
||||||
gitlab.com/etke.cc/go/validator v1.0.6 h1:w0Muxf9Pqw7xvF7NaaswE6d7r9U3nB2t2l5PnFMrecQ=
|
gitlab.com/etke.cc/go/validator v1.0.6 h1:w0Muxf9Pqw7xvF7NaaswE6d7r9U3nB2t2l5PnFMrecQ=
|
||||||
gitlab.com/etke.cc/go/validator v1.0.6/go.mod h1:Id0SxRj0J3IPhiKlj0w1plxVLZfHlkwipn7HfRZsDts=
|
gitlab.com/etke.cc/go/validator v1.0.6/go.mod h1:Id0SxRj0J3IPhiKlj0w1plxVLZfHlkwipn7HfRZsDts=
|
||||||
gitlab.com/etke.cc/linkpearl v0.0.0-20230920071429-25fe33ba08d0 h1:7fx8afCUluCzJISPUr6j8przpwdcCCXqqPHWvPRmzhA=
|
gitlab.com/etke.cc/linkpearl v0.0.0-20230928120707-1e99315dc616 h1:Gvhmq84VmAJN1xRzRBK79XJVObAvVcx9Q3s6K+Zo644=
|
||||||
gitlab.com/etke.cc/linkpearl v0.0.0-20230920071429-25fe33ba08d0/go.mod h1:IZ0TE+ZnIdJLb538owDMxhtpWH7blfW+oR7e5XRXxNY=
|
gitlab.com/etke.cc/linkpearl v0.0.0-20230928120707-1e99315dc616/go.mod h1:IZ0TE+ZnIdJLb538owDMxhtpWH7blfW+oR7e5XRXxNY=
|
||||||
go.mau.fi/util v0.1.0 h1:BwIFWIOEeO7lsiI2eWKFkWTfc5yQmoe+0FYyOFVyaoE=
|
go.mau.fi/util v0.1.0 h1:BwIFWIOEeO7lsiI2eWKFkWTfc5yQmoe+0FYyOFVyaoE=
|
||||||
go.mau.fi/util v0.1.0/go.mod h1:AxuJUMCxpzgJ5eV9JbPWKRH8aAJJidxetNdUj7qcb84=
|
go.mau.fi/util v0.1.0/go.mod h1:AxuJUMCxpzgJ5eV9JbPWKRH8aAJJidxetNdUj7qcb84=
|
||||||
golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
@@ -118,6 +120,7 @@ golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
|||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||||
|
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20210501142056-aec3718b3fa0/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
golang.org/x/net v0.0.0-20210501142056-aec3718b3fa0/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
|||||||
@@ -64,7 +64,8 @@ type matrixbot interface {
|
|||||||
IsGreylisted(net.Addr) bool
|
IsGreylisted(net.Addr) bool
|
||||||
IsBanned(net.Addr) bool
|
IsBanned(net.Addr) bool
|
||||||
IsTrusted(net.Addr) bool
|
IsTrusted(net.Addr) bool
|
||||||
Ban(net.Addr)
|
BanAuto(net.Addr)
|
||||||
|
BanAuth(net.Addr)
|
||||||
GetMapping(string) (id.RoomID, bool)
|
GetMapping(string) (id.RoomID, bool)
|
||||||
GetIFOptions(id.RoomID) email.IncomingFilteringOptions
|
GetIFOptions(id.RoomID) email.IncomingFilteringOptions
|
||||||
IncomingEmail(context.Context, *email.Email) error
|
IncomingEmail(context.Context, *email.Email) error
|
||||||
|
|||||||
@@ -41,14 +41,14 @@ func (m *mailServer) Login(state *smtp.ConnectionState, username, password strin
|
|||||||
|
|
||||||
if !email.AddressValid(username) {
|
if !email.AddressValid(username) {
|
||||||
m.log.Debug().Str("address", username).Msg("address is invalid")
|
m.log.Debug().Str("address", username).Msg("address is invalid")
|
||||||
m.bot.Ban(state.RemoteAddr)
|
m.bot.BanAuth(state.RemoteAddr)
|
||||||
return nil, ErrBanned
|
return nil, ErrBanned
|
||||||
}
|
}
|
||||||
|
|
||||||
roomID, allow := m.bot.AllowAuth(username, password)
|
roomID, allow := m.bot.AllowAuth(username, password)
|
||||||
if !allow {
|
if !allow {
|
||||||
m.log.Debug().Str("username", username).Msg("username or password is invalid")
|
m.log.Debug().Str("username", username).Msg("username or password is invalid")
|
||||||
m.bot.Ban(state.RemoteAddr)
|
m.bot.BanAuth(state.RemoteAddr)
|
||||||
return nil, ErrBanned
|
return nil, ErrBanned
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ func (m *mailServer) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session,
|
|||||||
getRoomID: m.bot.GetMapping,
|
getRoomID: m.bot.GetMapping,
|
||||||
getFilters: m.bot.GetIFOptions,
|
getFilters: m.bot.GetIFOptions,
|
||||||
receiveEmail: m.ReceiveEmail,
|
receiveEmail: m.ReceiveEmail,
|
||||||
ban: m.bot.Ban,
|
ban: m.bot.BanAuto,
|
||||||
greylisted: m.bot.IsGreylisted,
|
greylisted: m.bot.IsGreylisted,
|
||||||
trusted: m.bot.IsTrusted,
|
trusted: m.bot.IsTrusted,
|
||||||
log: m.log,
|
log: m.log,
|
||||||
|
|||||||
@@ -1,14 +1,56 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mcnijman/go-emailaddress"
|
||||||
|
)
|
||||||
|
|
||||||
// Mailbox returns mailbox part from email address
|
// Mailbox returns mailbox part from email address
|
||||||
func Mailbox(email string) string {
|
func Mailbox(email string) string {
|
||||||
index := strings.LastIndex(email, "@")
|
mailbox, _, _ := EmailParts(email)
|
||||||
if index == -1 {
|
return mailbox
|
||||||
return email
|
}
|
||||||
|
|
||||||
|
// Subaddress returns sub address part form email address
|
||||||
|
func Subaddress(email string) string {
|
||||||
|
_, sub, _ := EmailParts(email)
|
||||||
|
return sub
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hostname returns hostname part from email address
|
||||||
|
func Hostname(email string) string {
|
||||||
|
_, _, hostname := EmailParts(email)
|
||||||
|
return hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmailParts parses email address into mailbox, subaddress, and hostname
|
||||||
|
func EmailParts(email string) (string, string, string) {
|
||||||
|
var mailbox, hostname string
|
||||||
|
address, err := emailaddress.Parse(email)
|
||||||
|
if err == nil {
|
||||||
|
mailbox = address.LocalPart
|
||||||
|
hostname = address.Domain
|
||||||
|
} else {
|
||||||
|
mailbox = email
|
||||||
|
hostname = email
|
||||||
|
mIdx := strings.Index(email, "@")
|
||||||
|
hIdx := strings.LastIndex(email, "@")
|
||||||
|
if mIdx != -1 {
|
||||||
|
mailbox = email[:mIdx]
|
||||||
|
}
|
||||||
|
if hIdx != -1 {
|
||||||
|
hostname = email[hIdx+1:]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return email[:index]
|
|
||||||
|
var sub string
|
||||||
|
idx := strings.Index(mailbox, "+")
|
||||||
|
if idx != -1 {
|
||||||
|
sub = strings.ReplaceAll(mailbox[idx:], "+", "")
|
||||||
|
mailbox = strings.ReplaceAll(mailbox[:idx], "+", "")
|
||||||
|
}
|
||||||
|
return mailbox, sub, hostname
|
||||||
}
|
}
|
||||||
|
|
||||||
// EmailsList returns human-readable list of mailbox's emails for all available domains
|
// EmailsList returns human-readable list of mailbox's emails for all available domains
|
||||||
@@ -34,8 +76,3 @@ func EmailsList(mailbox string, domain string) string {
|
|||||||
|
|
||||||
return msg.String()
|
return msg.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hostname returns hostname part from email address
|
|
||||||
func Hostname(email string) string {
|
|
||||||
return email[strings.LastIndex(email, "@")+1:]
|
|
||||||
}
|
|
||||||
|
|||||||
70
utils/mail_test.go
Normal file
70
utils/mail_test.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestMailbox(t *testing.T) {
|
||||||
|
tests := map[string]string{
|
||||||
|
"mailbox@example.com": "mailbox",
|
||||||
|
"mail-box@example.com": "mail-box",
|
||||||
|
"mailbox": "mailbox",
|
||||||
|
"mail@box@example.com": "mail",
|
||||||
|
"mailbox+@example.com": "mailbox",
|
||||||
|
"mailbox+sub@example.com": "mailbox",
|
||||||
|
"mailbox+++sub@example.com": "mailbox",
|
||||||
|
}
|
||||||
|
|
||||||
|
for in, expected := range tests {
|
||||||
|
t.Run(in, func(t *testing.T) {
|
||||||
|
output := Mailbox(in)
|
||||||
|
if output != expected {
|
||||||
|
t.Error(expected, "!=", output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubaddress(t *testing.T) {
|
||||||
|
tests := map[string]string{
|
||||||
|
"mailbox@example@example.com": "",
|
||||||
|
"mail-box@example.com": "",
|
||||||
|
"mailbox+": "",
|
||||||
|
"mailbox+sub@example.com": "sub",
|
||||||
|
"mailbox+++sub@example.com": "sub",
|
||||||
|
}
|
||||||
|
|
||||||
|
for in, expected := range tests {
|
||||||
|
t.Run(in, func(t *testing.T) {
|
||||||
|
output := Subaddress(in)
|
||||||
|
if output != expected {
|
||||||
|
t.Error(expected, "!=", output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHostname(t *testing.T) {
|
||||||
|
tests := map[string]string{
|
||||||
|
"mailbox@example.com": "example.com",
|
||||||
|
"mailbox": "mailbox",
|
||||||
|
"mail@box@example.com": "example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
for in, expected := range tests {
|
||||||
|
t.Run(in, func(t *testing.T) {
|
||||||
|
output := Hostname(in)
|
||||||
|
if output != expected {
|
||||||
|
t.Error(expected, "!=", output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmailList(t *testing.T) {
|
||||||
|
domains = []string{"example.com", "example.org"}
|
||||||
|
expected := "test@example.org, test@example.com"
|
||||||
|
|
||||||
|
actual := EmailsList("test", "example.org")
|
||||||
|
if actual != expected {
|
||||||
|
t.Error(expected, "!=", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,19 +5,9 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var domains []string
|
||||||
log *zerolog.Logger
|
|
||||||
domains []string
|
|
||||||
)
|
|
||||||
|
|
||||||
// SetLogger for utils
|
|
||||||
func SetLogger(loggerInstance *zerolog.Logger) {
|
|
||||||
log = loggerInstance
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDomains for later use
|
// SetDomains for later use
|
||||||
func SetDomains(slice []string) {
|
func SetDomains(slice []string) {
|
||||||
@@ -134,3 +124,23 @@ func StringSlice(str string) []string {
|
|||||||
func SanitizeStringSlice(str string) string {
|
func SanitizeStringSlice(str string) string {
|
||||||
return SliceString(StringSlice(str))
|
return SliceString(StringSlice(str))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MapKeys returns sorted keys of the map
|
||||||
|
func MapKeys[V any](data map[string]V) []string {
|
||||||
|
keys := make([]string, 0, len(data))
|
||||||
|
for k := range data {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chunks divides slice by chunks with specified size
|
||||||
|
func Chunks[T any](slice []T, chunkSize int) [][]T {
|
||||||
|
chunks := make([][]T, 0, (len(slice)+chunkSize-1)/chunkSize)
|
||||||
|
|
||||||
|
for chunkSize < len(slice) {
|
||||||
|
slice, chunks = slice[chunkSize:], append(chunks, slice[0:chunkSize:chunkSize])
|
||||||
|
}
|
||||||
|
return append(chunks, slice)
|
||||||
|
}
|
||||||
|
|||||||
2
vendor/github.com/mcnijman/go-emailaddress/.gitignore
generated
vendored
Normal file
2
vendor/github.com/mcnijman/go-emailaddress/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
emailaddress
|
||||||
|
coverage.*
|
||||||
25
vendor/github.com/mcnijman/go-emailaddress/.travis.yml
generated
vendored
Normal file
25
vendor/github.com/mcnijman/go-emailaddress/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
sudo: false
|
||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- "1.11.x"
|
||||||
|
- "1.10.x"
|
||||||
|
- "1.9.x"
|
||||||
|
- tip
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
- GO111MODULE=on
|
||||||
|
matrix:
|
||||||
|
allow_failures:
|
||||||
|
- go: tip
|
||||||
|
fast_finish: true
|
||||||
|
install:
|
||||||
|
- go get golang.org/x/tools/cmd/cover
|
||||||
|
- go get github.com/mattn/goveralls
|
||||||
|
- GO111MODULE=off go get -u github.com/securego/gosec/cmd/gosec/...
|
||||||
|
script:
|
||||||
|
- go get -t -v ./...
|
||||||
|
- diff -u <(echo -n) <(gofmt -d -s .)
|
||||||
|
- go tool vet .
|
||||||
|
- $GOPATH/bin/gosec ./...
|
||||||
|
- go test -race -covermode=atomic -coverprofile=coverage.txt ./...
|
||||||
|
- $GOPATH/bin/goveralls -coverprofile=coverage.txt -service=travis-ci
|
||||||
21
vendor/github.com/mcnijman/go-emailaddress/LICENCE
generated
vendored
Normal file
21
vendor/github.com/mcnijman/go-emailaddress/LICENCE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 Thijs Nijman
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
102
vendor/github.com/mcnijman/go-emailaddress/README.md
generated
vendored
Normal file
102
vendor/github.com/mcnijman/go-emailaddress/README.md
generated
vendored
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# go-emailaddress #
|
||||||
|
|
||||||
|
[](https://godoc.org/github.com/mcnijman/go-emailaddress) [](https://travis-ci.org/mcnijman/go-emailaddress) [](https://coveralls.io/github/mcnijman/go-emailaddress?branch=master) [](https://goreportcard.com/report/github.com/mcnijman/go-emailaddress)
|
||||||
|
|
||||||
|
go-emailaddress is a tiny Go library for finding, parsing and validating email addresses. This
|
||||||
|
library is tested for Go v1.9 and above.
|
||||||
|
|
||||||
|
Note that there is no such thing as perfect email address validation other than sending an actual
|
||||||
|
email (ie. with a confirmation token). This library however checks if the email format conforms to
|
||||||
|
the spec and if the host (domain) is actually able to receive emails. You can also use this library
|
||||||
|
to find emails in a byte array. This package was created as similar packages don't seem to be
|
||||||
|
maintained anymore (ie contain bugs with pull requests still open), and/or use wrong local
|
||||||
|
validation.
|
||||||
|
|
||||||
|
## Usage ##
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get -u github.com/mcnijman/go-emailaddress
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parsing and local validation ###
|
||||||
|
|
||||||
|
Parse and validate the email locally using RFC 5322 regex, note that when `err == nil` it doesn't
|
||||||
|
necessarily mean the email address actually exists.
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/mcnijman/go-emailaddress"
|
||||||
|
|
||||||
|
email, err := emailaddress.Parse("foo@bar.com")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("invalid email")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(email.LocalPart) // foo
|
||||||
|
fmt.Println(email.Domain) // bar.com
|
||||||
|
fmt.Println(email) // foo@bar.com
|
||||||
|
fmt.Println(email.String()) // foo@bar.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validating the host ###
|
||||||
|
|
||||||
|
Host validation will first attempt to resolve the domain and then verify if we can start a mail
|
||||||
|
transaction with the host. This is relatively slow as it will contact the host several times.
|
||||||
|
Note that when `err == nil` it doesn't necessarily mean the email address actually exists.
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/mcnijman/go-emailaddress"
|
||||||
|
|
||||||
|
email, err := emailaddress.Parse("foo@bar.com")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("invalid email")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := email.ValidateHost()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("invalid host")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Finding emails ###
|
||||||
|
|
||||||
|
This will look for emails in a byte array (ie text or an html response).
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/mcnijman/go-emailaddress"
|
||||||
|
|
||||||
|
text := []byte(`Send me an email at foo@bar.com.`)
|
||||||
|
validateHost := false
|
||||||
|
|
||||||
|
emails := emailaddress.Find(text, validateHost)
|
||||||
|
|
||||||
|
for _, e := range emails {
|
||||||
|
fmt.Println(e)
|
||||||
|
}
|
||||||
|
// foo@bar.com
|
||||||
|
```
|
||||||
|
|
||||||
|
As RFC 5322 is really broad this method will likely match images and urls that contain
|
||||||
|
the '@' character (ie. !--logo@2x.png). For more reliable results, you can use the following method.
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/mcnijman/go-emailaddress"
|
||||||
|
|
||||||
|
text := []byte(`Send me an email at foo@bar.com or fake@domain.foobar.`)
|
||||||
|
validateHost := false
|
||||||
|
|
||||||
|
emails := emailaddress.FindWithIcannSuffix(text, validateHost)
|
||||||
|
|
||||||
|
for _, e := range emails {
|
||||||
|
fmt.Println(e)
|
||||||
|
}
|
||||||
|
// foo@bar.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Versioning ##
|
||||||
|
|
||||||
|
This library uses [semantic versioning 2.0.0](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## License ##
|
||||||
|
|
||||||
|
This library is distributed under the MIT license found in the [LICENSE](./LICENSE)
|
||||||
|
file.
|
||||||
224
vendor/github.com/mcnijman/go-emailaddress/emailaddress.go
generated
vendored
Normal file
224
vendor/github.com/mcnijman/go-emailaddress/emailaddress.go
generated
vendored
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
// Copyright 2018 The go-emailaddress AUTHORS. All rights reserved.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by a MIT
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package emailaddress provides a tiny library for finding, parsing and validation of email
|
||||||
|
addresses. This library is tested for Go v1.9 and above.
|
||||||
|
|
||||||
|
go get -u github.com/mcnijman/go-emailaddress
|
||||||
|
|
||||||
|
Local validation
|
||||||
|
|
||||||
|
Parse and validate the email locally using RFC 5322 regex, note that when err == nil it doesn't
|
||||||
|
necessarily mean the email address actually exists.
|
||||||
|
|
||||||
|
import "github.com/mcnijman/go-emailaddress"
|
||||||
|
|
||||||
|
email, err := emailaddress.Parse("foo@bar.com")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("invalid email")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(email.LocalPart) // foo
|
||||||
|
fmt.Println(email.Domain) // bar.com
|
||||||
|
fmt.Println(email) // foo@bar.com
|
||||||
|
fmt.Println(email.String()) // foo@bar.com
|
||||||
|
|
||||||
|
Host validation
|
||||||
|
|
||||||
|
Host validation will first attempt to resolve the domain and then verify if we can start a mail
|
||||||
|
transaction with the host. This is relatively slow as it will contact the host several times.
|
||||||
|
Note that when err == nil it doesn't necessarily mean the email address actually exists.
|
||||||
|
|
||||||
|
import "github.com/mcnijman/go-emailaddress"
|
||||||
|
|
||||||
|
email, err := emailaddress.Parse("foo@bar.com")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("invalid email")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := email.ValidateHost()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("invalid host")
|
||||||
|
}
|
||||||
|
|
||||||
|
Finding emails
|
||||||
|
|
||||||
|
This will look for emails in a byte array (ie text or an html response).
|
||||||
|
|
||||||
|
import "github.com/mcnijman/go-emailaddress"
|
||||||
|
|
||||||
|
text := []byte(`Send me an email at foo@bar.com.`)
|
||||||
|
validateHost := false
|
||||||
|
|
||||||
|
emails := emailaddress.Find(text, validateHost)
|
||||||
|
|
||||||
|
for _, e := range emails {
|
||||||
|
fmt.Println(e)
|
||||||
|
}
|
||||||
|
// foo@bar.com
|
||||||
|
|
||||||
|
As RFC 5322 is really broad this method will likely match images and urls that contain
|
||||||
|
the '@' character (ie. !--logo@2x.png). For more reliable results, you can use the following method.
|
||||||
|
|
||||||
|
import "github.com/mcnijman/go-emailaddress"
|
||||||
|
|
||||||
|
text := []byte(`Send me an email at foo@bar.com or fake@domain.foobar.`)
|
||||||
|
validateHost := false
|
||||||
|
|
||||||
|
emails := emailaddress.FindWithIcannSuffix(text, validateHost)
|
||||||
|
|
||||||
|
for _, e := range emails {
|
||||||
|
fmt.Println(e)
|
||||||
|
}
|
||||||
|
// foo@bar.com
|
||||||
|
|
||||||
|
*/
|
||||||
|
package emailaddress
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/smtp"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/publicsuffix"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// rfc5322 is a RFC 5322 regex, as per: https://stackoverflow.com/a/201378/5405453.
|
||||||
|
// Note that this can't verify that the address is an actual working email address.
|
||||||
|
// Use ValidateHost as a starter and/or send them one :-).
|
||||||
|
rfc5322 = "(?i)(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])"
|
||||||
|
validEmailRegexp = regexp.MustCompile(fmt.Sprintf("^%s*$", rfc5322))
|
||||||
|
findEmailRegexp = regexp.MustCompile(rfc5322)
|
||||||
|
)
|
||||||
|
|
||||||
|
// EmailAddress is a structure that stores the address local-part@domain parts.
|
||||||
|
type EmailAddress struct {
|
||||||
|
// LocalPart usually the username of an email address.
|
||||||
|
LocalPart string
|
||||||
|
|
||||||
|
// Domain is the part of the email address after the last @.
|
||||||
|
// This should be DNS resolvable to an email server.
|
||||||
|
Domain string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e EmailAddress) String() string {
|
||||||
|
if e.LocalPart == "" || e.Domain == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s@%s", e.LocalPart, e.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateHost will test if the email address is actually reachable. It will first try to resolve
|
||||||
|
// the host and then start a mail transaction.
|
||||||
|
func (e EmailAddress) ValidateHost() error {
|
||||||
|
host, err := lookupHost(e.Domain)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tryHost(host, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateIcanSuffix will test if the public suffix of the domain is managed by ICANN using
|
||||||
|
// the golang.org/x/net/publicsuffix package. If not it will return an error. Note that if this
|
||||||
|
// method returns an error it does not necessarily mean that the email address is invalid. Also the
|
||||||
|
// suffix list in the standard package is embedded and thereby not up to date.
|
||||||
|
func (e EmailAddress) ValidateIcanSuffix() error {
|
||||||
|
d := strings.ToLower(e.Domain)
|
||||||
|
if s, icann := publicsuffix.PublicSuffix(d); !icann {
|
||||||
|
return fmt.Errorf("public suffix is not managed by ICANN, got %s", s)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find uses the RFC 5322 regex to match, parse and validate any email addresses found in a string.
|
||||||
|
// If the validateHost boolean is true it will call the validate host for every email address
|
||||||
|
// encounterd. As RFC 5322 is really broad this method will likely match images and urls that
|
||||||
|
// contain the '@' character.
|
||||||
|
func Find(haystack []byte, validateHost bool) (emails []*EmailAddress) {
|
||||||
|
results := findEmailRegexp.FindAll(haystack, -1)
|
||||||
|
for _, r := range results {
|
||||||
|
if e, err := Parse(string(r)); err == nil {
|
||||||
|
if validateHost {
|
||||||
|
if err := e.ValidateHost(); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emails = append(emails, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return emails
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindWithIcannSuffix uses the RFC 5322 regex to match, parse and validate any email addresses
|
||||||
|
// found in a string. It will return emails if its eTLD is managed by the ICANN organization.
|
||||||
|
// If the validateHost boolean is true it will call the validate host for every email address
|
||||||
|
// encounterd. As RFC 5322 is really broad this method will likely match images and urls that
|
||||||
|
// contain the '@' character.
|
||||||
|
func FindWithIcannSuffix(haystack []byte, validateHost bool) (emails []*EmailAddress) {
|
||||||
|
results := Find(haystack, false)
|
||||||
|
for _, e := range results {
|
||||||
|
if err := e.ValidateIcanSuffix(); err == nil {
|
||||||
|
if validateHost {
|
||||||
|
if err := e.ValidateHost(); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emails = append(emails, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return emails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse will parse the input and validate the email locally. If you want to validate the host of
|
||||||
|
// this email address remotely call the ValidateHost method.
|
||||||
|
func Parse(email string) (*EmailAddress, error) {
|
||||||
|
if !validEmailRegexp.MatchString(email) {
|
||||||
|
return nil, fmt.Errorf("format is incorrect for %s", email)
|
||||||
|
}
|
||||||
|
|
||||||
|
i := strings.LastIndexByte(email, '@')
|
||||||
|
e := &EmailAddress{
|
||||||
|
LocalPart: email[:i],
|
||||||
|
Domain: email[i+1:],
|
||||||
|
}
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookupHost first checks if any MX records are available and if not, it will check
|
||||||
|
// if A records are available as they can resolve email server hosts. An error indicates
|
||||||
|
// that non of the A or MX records are available.
|
||||||
|
func lookupHost(domain string) (string, error) {
|
||||||
|
if mx, err := net.LookupMX(domain); err == nil {
|
||||||
|
return mx[0].Host, nil
|
||||||
|
}
|
||||||
|
if ips, err := net.LookupIP(domain); err == nil {
|
||||||
|
return ips[0].String(), nil // randomly returns IPv4 or IPv6 (when available)
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("failed finding MX and A records for domain %s", domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// tryHost will verify if we can start a mail transaction with the host.
|
||||||
|
func tryHost(host string, e EmailAddress) error {
|
||||||
|
client, err := smtp.Dial(fmt.Sprintf("%s:%d", host, 25))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
if err = client.Hello(e.Domain); err == nil {
|
||||||
|
if err = client.Mail(fmt.Sprintf("hello@%s", e.Domain)); err == nil {
|
||||||
|
if err = client.Rcpt(e.String()); err == nil {
|
||||||
|
client.Reset() // #nosec
|
||||||
|
client.Quit() // #nosec
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
8
vendor/gitlab.com/etke.cc/linkpearl/accountdata.go
generated
vendored
8
vendor/gitlab.com/etke.cc/linkpearl/accountdata.go
generated
vendored
@@ -24,7 +24,7 @@ func (l *Linkpearl) GetAccountData(name string) (map[string]string, error) {
|
|||||||
l.acc.Add(name, data)
|
l.acc.Add(name, data)
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
return data, err
|
return data, UnwrapError(err)
|
||||||
}
|
}
|
||||||
data = l.decryptAccountData(data)
|
data = l.decryptAccountData(data)
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ func (l *Linkpearl) SetAccountData(name string, data map[string]string) error {
|
|||||||
l.acc.Add(name, data)
|
l.acc.Add(name, data)
|
||||||
|
|
||||||
data = l.encryptAccountData(data)
|
data = l.encryptAccountData(data)
|
||||||
return l.GetClient().SetAccountData(name, data)
|
return UnwrapError(l.GetClient().SetAccountData(name, data))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRoomAccountData of the room (from cache and API, with encryption support)
|
// GetRoomAccountData of the room (from cache and API, with encryption support)
|
||||||
@@ -59,7 +59,7 @@ func (l *Linkpearl) GetRoomAccountData(roomID id.RoomID, name string) (map[strin
|
|||||||
l.acc.Add(key, data)
|
l.acc.Add(key, data)
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
return data, err
|
return data, UnwrapError(err)
|
||||||
}
|
}
|
||||||
data = l.decryptAccountData(data)
|
data = l.decryptAccountData(data)
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ func (l *Linkpearl) SetRoomAccountData(roomID id.RoomID, name string, data map[s
|
|||||||
l.acc.Add(key, data)
|
l.acc.Add(key, data)
|
||||||
|
|
||||||
data = l.encryptAccountData(data)
|
data = l.encryptAccountData(data)
|
||||||
return l.GetClient().SetRoomAccountData(roomID, name, data)
|
return UnwrapError(l.GetClient().SetRoomAccountData(roomID, name, data))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Linkpearl) encryptAccountData(data map[string]string) map[string]string {
|
func (l *Linkpearl) encryptAccountData(data map[string]string) map[string]string {
|
||||||
|
|||||||
2
vendor/gitlab.com/etke.cc/linkpearl/justfile
generated
vendored
2
vendor/gitlab.com/etke.cc/linkpearl/justfile
generated
vendored
@@ -21,6 +21,6 @@ vuln:
|
|||||||
|
|
||||||
# run unit tests
|
# run unit tests
|
||||||
test:
|
test:
|
||||||
@go test ${BUILDFLAGS} -coverprofile=cover.out ./...
|
@go test -coverprofile=cover.out ./...
|
||||||
@go tool cover -func=cover.out
|
@go tool cover -func=cover.out
|
||||||
-@rm -f cover.out
|
-@rm -f cover.out
|
||||||
|
|||||||
31
vendor/gitlab.com/etke.cc/linkpearl/send.go
generated
vendored
31
vendor/gitlab.com/etke.cc/linkpearl/send.go
generated
vendored
@@ -1,8 +1,6 @@
|
|||||||
package linkpearl
|
package linkpearl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"maunium.net/go/mautrix"
|
"maunium.net/go/mautrix"
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
"maunium.net/go/mautrix/format"
|
"maunium.net/go/mautrix/format"
|
||||||
@@ -16,24 +14,31 @@ func (l *Linkpearl) Send(roomID id.RoomID, content interface{}) (id.EventID, err
|
|||||||
l.log.Debug().Str("roomID", roomID.String()).Any("content", content).Msg("sending event")
|
l.log.Debug().Str("roomID", roomID.String()).Any("content", content).Msg("sending event")
|
||||||
resp, err := l.api.SendMessageEvent(roomID, event.EventMessage, content)
|
resp, err := l.api.SendMessageEvent(roomID, event.EventMessage, content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", UnwrapError(err)
|
||||||
}
|
}
|
||||||
return resp.EventID, nil
|
return resp.EventID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendNotice to a room with optional thread relation
|
// SendNotice to a room with optional relations, markdown supported
|
||||||
func (l *Linkpearl) SendNotice(roomID id.RoomID, threadID id.EventID, message string, args ...interface{}) {
|
func (l *Linkpearl) SendNotice(roomID id.RoomID, message string, relates ...*event.RelatesTo) {
|
||||||
content := format.RenderMarkdown(fmt.Sprintf(message, args...), true, true)
|
var withRelatesTo bool
|
||||||
if threadID != "" {
|
content := format.RenderMarkdown(message, true, true)
|
||||||
content.RelatesTo = &event.RelatesTo{
|
content.MsgType = event.MsgNotice
|
||||||
Type: event.RelThread,
|
if len(relates) > 0 {
|
||||||
EventID: threadID,
|
withRelatesTo = true
|
||||||
}
|
content.RelatesTo = relates[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := l.Send(roomID, &content)
|
_, err := l.Send(roomID, &content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.log.Error().Err(err).Str("roomID", roomID.String()).Msg("cannot send a notice int the room")
|
l.log.Error().Err(UnwrapError(err)).Str("roomID", roomID.String()).Str("retries", "1/2").Msg("cannot send a notice into the room")
|
||||||
|
if withRelatesTo {
|
||||||
|
content.RelatesTo = nil
|
||||||
|
_, err = l.Send(roomID, &content)
|
||||||
|
if err != nil {
|
||||||
|
l.log.Error().Err(UnwrapError(err)).Str("roomID", roomID.String()).Str("retries", "2/2").Msg("cannot send a notice into the room even without relations")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +46,7 @@ func (l *Linkpearl) SendNotice(roomID id.RoomID, threadID id.EventID, message st
|
|||||||
func (l *Linkpearl) SendFile(roomID id.RoomID, req *mautrix.ReqUploadMedia, msgtype event.MessageType, relation *event.RelatesTo) error {
|
func (l *Linkpearl) SendFile(roomID id.RoomID, req *mautrix.ReqUploadMedia, msgtype event.MessageType, relation *event.RelatesTo) error {
|
||||||
resp, err := l.GetClient().UploadMedia(*req)
|
resp, err := l.GetClient().UploadMedia(*req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
err = UnwrapError(err)
|
||||||
l.log.Error().Err(err).Str("file", req.FileName).Msg("cannot upload file")
|
l.log.Error().Err(err).Str("file", req.FileName).Msg("cannot upload file")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -52,6 +58,7 @@ func (l *Linkpearl) SendFile(roomID id.RoomID, req *mautrix.ReqUploadMedia, msgt
|
|||||||
RelatesTo: relation,
|
RelatesTo: relation,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
err = UnwrapError(err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.log.Error().Err(err).Str("file", req.FileName).Msg("cannot send uploaded file")
|
l.log.Error().Err(err).Str("file", req.FileName).Msg("cannot send uploaded file")
|
||||||
}
|
}
|
||||||
|
|||||||
24
utils/matrix.go → vendor/gitlab.com/etke.cc/linkpearl/utils.go
generated
vendored
24
utils/matrix.go → vendor/gitlab.com/etke.cc/linkpearl/utils.go
generated
vendored
@@ -1,28 +1,38 @@
|
|||||||
package utils
|
package linkpearl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/rs/zerolog"
|
||||||
"maunium.net/go/mautrix"
|
"maunium.net/go/mautrix"
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RelatesTo returns relation object of a matrix event (either threads or reply-to)
|
// RelatesTo returns relation object of a matrix event (either threads with reply-to fallback or plain reply-to)
|
||||||
func RelatesTo(threads bool, parentID id.EventID) *event.RelatesTo {
|
func RelatesTo(parentID id.EventID, noThreads ...bool) *event.RelatesTo {
|
||||||
if parentID == "" {
|
if parentID == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if threads {
|
var nothreads bool
|
||||||
|
if len(noThreads) > 0 {
|
||||||
|
nothreads = noThreads[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if nothreads {
|
||||||
return &event.RelatesTo{
|
return &event.RelatesTo{
|
||||||
Type: event.RelThread,
|
InReplyTo: &event.InReplyTo{
|
||||||
EventID: parentID,
|
EventID: parentID,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &event.RelatesTo{
|
return &event.RelatesTo{
|
||||||
|
Type: event.RelThread,
|
||||||
|
EventID: parentID,
|
||||||
InReplyTo: &event.InReplyTo{
|
InReplyTo: &event.InReplyTo{
|
||||||
EventID: parentID,
|
EventID: parentID,
|
||||||
},
|
},
|
||||||
|
IsFallingBack: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +76,7 @@ func EventField[T any](content *event.Content, field string) T {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseContent(evt *event.Event, eventType event.Type) {
|
func ParseContent(evt *event.Event, eventType event.Type, log *zerolog.Logger) {
|
||||||
if evt.Content.Parsed != nil {
|
if evt.Content.Parsed != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
5
vendor/modules.txt
vendored
5
vendor/modules.txt
vendored
@@ -71,6 +71,9 @@ github.com/mattn/go-runewidth
|
|||||||
# github.com/mattn/go-sqlite3 v1.14.17
|
# github.com/mattn/go-sqlite3 v1.14.17
|
||||||
## explicit; go 1.16
|
## explicit; go 1.16
|
||||||
github.com/mattn/go-sqlite3
|
github.com/mattn/go-sqlite3
|
||||||
|
# github.com/mcnijman/go-emailaddress v1.1.0
|
||||||
|
## explicit
|
||||||
|
github.com/mcnijman/go-emailaddress
|
||||||
# github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a
|
# github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a
|
||||||
## explicit
|
## explicit
|
||||||
github.com/mikesmitty/edkey
|
github.com/mikesmitty/edkey
|
||||||
@@ -141,7 +144,7 @@ gitlab.com/etke.cc/go/trysmtp
|
|||||||
# gitlab.com/etke.cc/go/validator v1.0.6
|
# gitlab.com/etke.cc/go/validator v1.0.6
|
||||||
## explicit; go 1.18
|
## explicit; go 1.18
|
||||||
gitlab.com/etke.cc/go/validator
|
gitlab.com/etke.cc/go/validator
|
||||||
# gitlab.com/etke.cc/linkpearl v0.0.0-20230920071429-25fe33ba08d0
|
# gitlab.com/etke.cc/linkpearl v0.0.0-20230928120707-1e99315dc616
|
||||||
## explicit; go 1.18
|
## explicit; go 1.18
|
||||||
gitlab.com/etke.cc/linkpearl
|
gitlab.com/etke.cc/linkpearl
|
||||||
# go.mau.fi/util v0.1.0
|
# go.mau.fi/util v0.1.0
|
||||||
|
|||||||
Reference in New Issue
Block a user