diff --git a/README.md b/README.md
index 2cc34a2..2636f9d 100644
--- a/README.md
+++ b/README.md
@@ -56,7 +56,7 @@ You can find default values in [config/defaults.go](config/defaults.go)
The following configuration needed only if you want to send emails using postmoogle
-First, add new DMARC DNS record of `TXT` type for subdomain `_dmarc` with a proper policy, the easiest one is: `v=DMARC1; p=quarantine;`.
+**First**, add new DMARC DNS record of `TXT` type for subdomain `_dmarc` with a proper policy, the easiest one is: `v=DMARC1; p=quarantine;`.
Example
@@ -86,7 +86,7 @@ _dmarc.DOMAIN. 1799 IN TXT "v=DMARC1; p=quarantine;"
-Second, add new SPF DNS record of `TXT` type for your domain that will be used with postmoogle, with format: `v=spf1 ip4:SERVER_IP -all`
+**Second**, add new SPF DNS record of `TXT` type for your domain that will be used with postmoogle, with format: `v=spf1 ip4:SERVER_IP -all`
Example
@@ -116,6 +116,52 @@ DOMAIN. 1799 IN TXT "v=spf1 ip4:111.111.111.111 -all"
+**Third**, add new DKIM DNS record of `TXT` type for subdomain `postmoogle._domainkey` that will be used with postmoogle.
+
+You can get that signature using the `!pm dkim` command:
+
+
+!pm dkim
+DKIM signature is: `v=DKIM1; k=ed25519; p=OcVzOwAONDfgbJX/5vwzlXOs9gUDO0YKlXHaDnBJtXw=`.
+You need to add it to your DNS records (if not already):
+Add new DNS record with type = `TXT`, key (subdomain/from): `postmoogle._domainkey` and value (to):
+
+```
+v=DKIM1; k=ed25519; p=OcVzOwAONDfgbJX/5vwzlXOs9gUDO0YKlXHaDnBJtXw=
+```
+
+Without that record other email servers may reject your emails as spam, kupo.
+
+
+
+
+Example
+
+```bash
+$ dig TXT postmoogle._domainkey.DOMAIN
+
+; <<>> DiG 9.18.6 <<>> TXT postmoogle._domainkey.DOMAIN
+;; global options: +cmd
+;; Got answer:
+;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 59014
+;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
+
+;; OPT PSEUDOSECTION:
+; EDNS: version: 0, flags:; udp: 1232
+;; QUESTION SECTION:
+;postmoogle._domainkey.DOMAIN. IN TXT
+
+;; ANSWER SECTION:
+postmoogle._domainkey.DOMAIN. 600 IN TXT "v=DKIM1; k=ed25519; p=OcVzOwAONDfgbJX/5vwzlXOs9gUDO0YKlXHaDnBJtXw="
+
+;; Query time: 90 msec
+;; SERVER: 1.1.1.1#53(1.1.1.1) (UDP)
+;; WHEN: Mon Sep 05 16:16:21 EEST 2022
+;; MSG SIZE rcvd: 525
+```
+
+
+
## Usage
### How to start
@@ -147,8 +193,9 @@ If you want to change them - check available options in the help message (`!pm h
---
-* **!pm mailboxes** - Show the list of all mailboxes
+* **!pm dkim** - Get DKIM signature
* **!pm users** - Get or set allowed users patterns
+* **!pm mailboxes** - Show the list of all mailboxes
* **!pm delete** <mailbox> - Delete specific mailbox
diff --git a/bot/command.go b/bot/command.go
index 892cee1..de96e60 100644
--- a/bot/command.go
+++ b/bot/command.go
@@ -15,6 +15,7 @@ const (
commandHelp = "help"
commandStop = "stop"
commandSend = "send"
+ commandDKIM = "dkim"
commandUsers = botOptionUsers
commandDelete = "delete"
commandMailboxes = "mailboxes"
@@ -132,6 +133,11 @@ func (b *Bot) initCommands() commandList {
description: "Get or set allowed users",
allowed: b.allowAdmin,
},
+ {
+ key: commandDKIM,
+ description: "Get DKIM signature",
+ allowed: b.allowAdmin,
+ },
{
key: commandMailboxes,
description: "Show the list of all mailboxes",
@@ -163,6 +169,8 @@ func (b *Bot) handleCommand(ctx context.Context, evt *event.Event, commandSlice
b.runStop(ctx)
case commandSend:
b.runSend(ctx, commandSlice)
+ case commandDKIM:
+ b.runDKIM(ctx)
case commandUsers:
b.runUsers(ctx, commandSlice)
case commandDelete:
@@ -272,7 +280,6 @@ func (b *Bot) runSend(ctx context.Context, commandSlice []string) {
subject := lines[1]
body := strings.Join(lines[2:], "\n")
- b.log.Debug("to=%s subject=%s body=%s", to, subject, body)
err := b.Send2Email(ctx, to, subject, body)
if err != nil {
b.Error(ctx, evt.RoomID, "cannot send email: %v", err)
diff --git a/bot/command_admin.go b/bot/command_admin.go
index 978d8eb..c5a9f6c 100644
--- a/bot/command_admin.go
+++ b/bot/command_admin.go
@@ -6,6 +6,7 @@ import (
"sort"
"strings"
+ "gitlab.com/etke.cc/go/secgen"
"maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/utils"
@@ -130,3 +131,32 @@ func (b *Bot) runUsers(ctx context.Context, commandSlice []string) {
b.allowedUsers = allowedUsers
b.SendNotice(ctx, evt.RoomID, "allowed users updated")
}
+
+func (b *Bot) runDKIM(ctx context.Context) {
+ evt := eventFromContext(ctx)
+ cfg := b.getBotSettings()
+ signature := cfg.DKIMSignature()
+ if signature == "" {
+ var private string
+ var derr error
+ signature, private, derr = secgen.DKIM()
+ if derr != nil {
+ b.Error(ctx, evt.RoomID, "cannot generate DKIM signature: %v", derr)
+ return
+ }
+ cfg.Set(botOptionDKIMSignature, signature)
+ cfg.Set(botOptionDKIMPrivateKey, private)
+ err := b.setBotSettings(cfg)
+ if err != nil {
+ b.Error(ctx, evt.RoomID, "cannot save bot options: %v", err)
+ return
+ }
+ }
+
+ b.SendNotice(ctx, evt.RoomID, fmt.Sprintf(
+ "DKIM signature is: `%s`.\n"+
+ "You need to add it to your DNS records (if not already):\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.",
+ signature, signature))
+}
diff --git a/bot/email.go b/bot/email.go
index 2342199..030edc6 100644
--- a/bot/email.go
+++ b/bot/email.go
@@ -2,11 +2,15 @@ package bot
import (
"context"
+ "crypto"
+ "crypto/x509"
+ "encoding/pem"
"errors"
"fmt"
"strings"
"time"
+ "github.com/emersion/go-msgauth/dkim"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
@@ -234,9 +238,45 @@ func (b *Bot) Send2Email(ctx context.Context, to, subject, body string) error {
msg.WriteString(body)
msg.WriteString("\r\n")
+ msg = b.signDKIM(msg)
+
return b.mta.Send(from, to, msg.String())
}
+func (b *Bot) signDKIM(body strings.Builder) strings.Builder {
+ privkey := b.getBotSettings().DKIMPrivateKey()
+ if privkey == "" {
+ b.log.Warn("DKIM private key not found, email will be sent unsigned")
+ return body
+ }
+ pemblock, _ := pem.Decode([]byte(privkey))
+ if pemblock == nil {
+ b.log.Error("cannot decode DKIM private key")
+ return body
+ }
+ parsedkey, err := x509.ParsePKCS8PrivateKey(pemblock.Bytes)
+ if err != nil {
+ b.log.Error("cannot parse PKCS8 private key: %v", err)
+ return body
+ }
+ signer := parsedkey.(crypto.Signer)
+
+ options := &dkim.SignOptions{
+ Domain: b.domain,
+ Selector: "postmoogle",
+ Signer: signer,
+ }
+
+ var msg strings.Builder
+ err = dkim.Sign(&msg, strings.NewReader(body.String()), options)
+ if err != nil {
+ b.log.Error("cannot sign email: %v", err)
+ return body
+ }
+
+ return msg
+}
+
func (b *Bot) sendFiles(ctx context.Context, roomID id.RoomID, files []*utils.File, noThreads bool, parentID id.EventID) {
for _, file := range files {
req := file.Convert()
diff --git a/bot/settings_bot.go b/bot/settings_bot.go
index 23ba72a..d45cbe4 100644
--- a/bot/settings_bot.go
+++ b/bot/settings_bot.go
@@ -11,7 +11,9 @@ const acBotSettingsKey = "cc.etke.postmoogle.config"
// bot options keys
const (
- botOptionUsers = "users"
+ botOptionUsers = "users"
+ botOptionDKIMSignature = "dkim.pub"
+ botOptionDKIMPrivateKey = "dkim.pem"
)
type botSettings map[string]string
@@ -40,6 +42,16 @@ func (s botSettings) Users() []string {
return []string{value}
}
+// DKIMSignature (DNS TXT record)
+func (s botSettings) DKIMSignature() string {
+ return s.Get(botOptionDKIMSignature)
+}
+
+// DKIMPrivateKey keep it secret
+func (s botSettings) DKIMPrivateKey() string {
+ return s.Get(botOptionDKIMPrivateKey)
+}
+
func (b *Bot) initBotUsers() ([]string, error) {
config := b.getBotSettings()
cfgUsers := config.Users()
diff --git a/go.mod b/go.mod
index 7aee223..68631fa 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.18
require (
git.sr.ht/~xn/cache/v2 v2.0.0
+ github.com/emersion/go-msgauth v0.6.6
github.com/emersion/go-smtp v0.15.0
github.com/getsentry/sentry-go v0.13.0
github.com/jhillyerd/enmime v0.10.0
@@ -11,6 +12,7 @@ require (
github.com/mattn/go-sqlite3 v1.14.14
gitlab.com/etke.cc/go/env v1.0.0
gitlab.com/etke.cc/go/logger v1.1.0
+ gitlab.com/etke.cc/go/secgen v1.1.0
gitlab.com/etke.cc/linkpearl v0.0.0-20220831124140-598117f26c77
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b
maunium.net/go/mautrix v0.12.0
@@ -27,6 +29,7 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.12 // indirect
+ github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
diff --git a/go.sum b/go.sum
index f7a2f5c..b8768be 100644
--- a/go.sum
+++ b/go.sum
@@ -6,10 +6,18 @@ github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXO
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/emersion/go-message v0.11.2/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
+github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
+github.com/emersion/go-milter v0.3.3/go.mod h1:ablHK0pbLB83kMFBznp/Rj8aV+Kc3jw8cxzzmCNLIOY=
+github.com/emersion/go-msgauth v0.6.6 h1:buv5lL8v/3v4RpHnQFS2IPhE3nxSRX+AxnrEJbDbHhA=
+github.com/emersion/go-msgauth v0.6.6/go.mod h1:A+/zaz9bzukLM6tRWRgJ3BdrBi+TFKTvQ3fGMFOI9SM=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8=
github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
+github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
+github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/getsentry/sentry-go v0.13.0 h1:20dgTiUSfxRB/EhMPtxcL9ZEbM1ZdR+W/7f7NWD+xWo=
github.com/getsentry/sentry-go v0.13.0/go.mod h1:EOsfu5ZdvKPfeHYV6pTVQnsjfp30+XA7//UooKNumH0=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
@@ -30,6 +38,7 @@ github.com/jhillyerd/enmime v0.10.0 h1:DZEzhptPRBesvN3gf7K1BOh4rfpqdsdrEoxW1Edr/
github.com/jhillyerd/enmime v0.10.0/go.mod h1:Qpe8EEemJMFAF8+NZoWdpXvK2Yb9dRF0k/z6mkcDHsA=
github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@@ -41,6 +50,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-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw=
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+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/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
@@ -57,6 +68,7 @@ github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6us
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -74,21 +86,28 @@ gitlab.com/etke.cc/go/env v1.0.0 h1:J98BwzOuELnjsVPFvz5wa79L7IoRV9CmrS41xLYXtSw=
gitlab.com/etke.cc/go/env v1.0.0/go.mod h1:e1l4RM5MA1sc0R1w/RBDAESWRwgo5cOG9gx8BKUn2C4=
gitlab.com/etke.cc/go/logger v1.1.0 h1:Yngp/DDLmJ0jJNLvLXrfan5Gi5QV+r7z6kCczTv8t4U=
gitlab.com/etke.cc/go/logger v1.1.0/go.mod h1:8Vw5HFXlZQ5XeqvUs5zan+GnhrQyYtm/xe+yj8H/0zk=
+gitlab.com/etke.cc/go/secgen v1.1.0 h1:KFjFEXNlSPtY19ichNL+lQF2Q0vP3/9O2rVGZzVrqq0=
+gitlab.com/etke.cc/go/secgen v1.1.0/go.mod h1:3pJqRGeWApzx7qXjABqz2o2SMCNpKSZao/gXVdasqE8=
gitlab.com/etke.cc/linkpearl v0.0.0-20220831124140-598117f26c77 h1:O9t4Sw/nu0JDUX+3KYjaqBi887opyNZ0imE+i2sV+q8=
gitlab.com/etke.cc/linkpearl v0.0.0-20220831124140-598117f26c77/go.mod h1:CqwzwxVogKG6gDWTPTen3NyWbTESg42jxoTfXXwDGKQ=
+golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
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-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=