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=