real multi-domain support
This commit is contained in:
155
README.md
155
README.md
@@ -19,7 +19,7 @@ so you can use it to send emails from your apps and scripts as well.
|
|||||||
- [x] Receive attachments
|
- [x] Receive attachments
|
||||||
- [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 aliases
|
- [x] Multi-domain support
|
||||||
|
|
||||||
#### deep dive
|
#### deep dive
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ env vars
|
|||||||
* **POSTMOOGLE_HOMESERVER** - homeserver url, eg: `https://matrix.example.com`
|
* **POSTMOOGLE_HOMESERVER** - homeserver url, eg: `https://matrix.example.com`
|
||||||
* **POSTMOOGLE_LOGIN** - user login/localpart, eg: `moogle`
|
* **POSTMOOGLE_LOGIN** - user login/localpart, eg: `moogle`
|
||||||
* **POSTMOOGLE_PASSWORD** - user password
|
* **POSTMOOGLE_PASSWORD** - user password
|
||||||
* **POSTMOOGLE_DOMAINS** - space separated list of SMTP domains to listen for new emails. The first domain acts as the main (actual) domain, all other as aliases
|
* **POSTMOOGLE_DOMAINS** - space separated list of SMTP domains to listen for new emails. The first domain acts as the default domain, all other as aliases
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>other optional config parameters</summary>
|
<summary>other optional config parameters</summary>
|
||||||
@@ -72,155 +72,7 @@ You can find default values in [config/defaults.go](config/defaults.go)
|
|||||||
|
|
||||||
### 2. DNS (optional)
|
### 2. DNS (optional)
|
||||||
|
|
||||||
The following configuration is needed only if you want to send outgoing emails via Postmoogle (it's not necessary if you only want to receive emails).
|
Follow the [docs/dns](docs/dns.md)
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>TL;DR</summary>
|
|
||||||
|
|
||||||
1. Configure DMARC record
|
|
||||||
2. Configure SPF record
|
|
||||||
3. Configure MX record
|
|
||||||
4. Configure DKIM record (use `!pm dkim`)
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
**First**, add a new DMARC DNS record of the `TXT` type for subdomain `_dmarc` with a proper policy. The simplest policy you can use is: `v=DMARC1; p=quarantine;`.
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Example</summary>
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ dig txt _dmarc.example.com
|
|
||||||
|
|
||||||
; <<>> DiG 9.18.6 <<>> txt _dmarc.example.com
|
|
||||||
;; global options: +cmd
|
|
||||||
;; Got answer:
|
|
||||||
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 57306
|
|
||||||
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
|
|
||||||
|
|
||||||
;; OPT PSEUDOSECTION:
|
|
||||||
; EDNS: version: 0, flags:; udp: 1232
|
|
||||||
;; QUESTION SECTION:
|
|
||||||
;_dmarc.example.com. IN TXT
|
|
||||||
|
|
||||||
;; ANSWER SECTION:
|
|
||||||
_dmarc.example.com. 1799 IN TXT "v=DMARC1; p=quarantine;"
|
|
||||||
|
|
||||||
;; Query time: 46 msec
|
|
||||||
;; SERVER: 1.1.1.1#53(1.1.1.1) (UDP)
|
|
||||||
;; WHEN: Sun Sep 04 21:31:30 EEST 2022
|
|
||||||
;; MSG SIZE rcvd: 79
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
**Second**, add a new SPF DNS record of the `TXT` type for your domain that will be used with Postmoogle, with format: `v=spf1 ip4:SERVER_IP -all` (replace `SERVER_IP` with your server's IP address)
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Example</summary>
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ dig txt example.com
|
|
||||||
|
|
||||||
; <<>> DiG 9.18.6 <<>> txt example.com
|
|
||||||
;; global options: +cmd
|
|
||||||
;; Got answer:
|
|
||||||
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 24796
|
|
||||||
;; flags: qr rd ra; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 1
|
|
||||||
|
|
||||||
;; OPT PSEUDOSECTION:
|
|
||||||
; EDNS: version: 0, flags:; udp: 1232
|
|
||||||
;; QUESTION SECTION:
|
|
||||||
;example.com. IN TXT
|
|
||||||
|
|
||||||
;; ANSWER SECTION:
|
|
||||||
example.com. 1799 IN TXT "v=spf1 ip4:111.111.111.111 -all"
|
|
||||||
|
|
||||||
;; Query time: 36 msec
|
|
||||||
;; SERVER: 1.1.1.1#53(1.1.1.1) (UDP)
|
|
||||||
;; WHEN: Sun Sep 04 21:35:04 EEST 2022
|
|
||||||
;; MSG SIZE rcvd: 255
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
**Third**, add a new MX DNS record of the `MX` type for your domain that will be used with postmoogle. It should point to the same (sub-)domain.
|
|
||||||
Looks odd, but some mail servers will refuse to interact with your mail server (and Postmoogle is already a mail server) without MX records.
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Example</summary>
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dig MX example.com
|
|
||||||
|
|
||||||
; <<>> DiG 9.18.6 <<>> MX example.com
|
|
||||||
;; global options: +cmd
|
|
||||||
;; Got answer:
|
|
||||||
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12688
|
|
||||||
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
|
|
||||||
|
|
||||||
;; OPT PSEUDOSECTION:
|
|
||||||
; EDNS: version: 0, flags:; udp: 1232
|
|
||||||
;; QUESTION SECTION:
|
|
||||||
;example.com. IN MX
|
|
||||||
|
|
||||||
;; ANSWER SECTION:
|
|
||||||
example.com. 1799 IN MX 10 example.com.
|
|
||||||
|
|
||||||
;; Query time: 40 msec
|
|
||||||
;; SERVER: 1.1.1.1#53(1.1.1.1) (UDP)
|
|
||||||
;; WHEN: Tue Sep 06 16:44:47 EEST 2022
|
|
||||||
;; MSG SIZE rcvd: 59
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
**Fourth** (and the last one), 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:
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>!pm dkim</summary>
|
|
||||||
|
|
||||||
DKIM signature is: `v=DKIM1; k=rsa; p=MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAxJVqmBHhK9FY93q1o3WEaP2GKMh/3LNMyvi1uSjOKxIyfWv685KxX1EUrbHakQRCTtUM7efKEsXsXBh+DQru2TE32yFpL9afA5BbHj3KePGFY8KJ2m0sQxbQcvn2KjJC0IQ15mk0rninPhtphU/2zLsd6e7Rl1m3L+9Osk320GbfDgSKjRPcSiwVMbLJpSOP0H0F3cIu+c1fHZHfmWy0O+us42C3HTLTlD779LTnQnKlAOQD/+DYYqz6TGGxEwUG2BRQ8O8w7/wXEkg/6a/MxNtPnc59g29CpqRsDkuYiR3UIpqzLDoqHlaoKNbYy34R+4aIjfNpmZyR5kIumws+3MJtJt9UhBTMloqd8lZDPaPmX2NEDqbcSTkHMQrphk+EWSCc7OvbKRaXZ0SyJLpLjxRwKrpeO0JAI0ZpnAFS11uBEe9GSS8uzIIFNYVD1vHloAFKvUJEhyuVyz9/SyqTnArN3ZTiC5cqD1MB86q5QPrKqZfp1dAnv7xAJThL0AP/AgMBAAE=`.
|
|
||||||
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=rsa; p=MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAxJVqmBHhK9FY93q1o3WEaP2GKMh/3LNMyvi1uSjOKxIyfWv685KxX1EUrbHakQRCTtUM7efKEsXsXBh+DQru2TE32yFpL9afA5BbHj3KePGFY8KJ2m0sQxbQcvn2KjJC0IQ15mk0rninPhtphU/2zLsd6e7Rl1m3L+9Osk320GbfDgSKjRPcSiwVMbLJpSOP0H0F3cIu+c1fHZHfmWy0O+us42C3HTLTlD779LTnQnKlAOQD/+DYYqz6TGGxEwUG2BRQ8O8w7/wXEkg/6a/MxNtPnc59g29CpqRsDkuYiR3UIpqzLDoqHlaoKNbYy34R+4aIjfNpmZyR5kIumws+3MJtJt9UhBTMloqd8lZDPaPmX2NEDqbcSTkHMQrphk+EWSCc7OvbKRaXZ0SyJLpLjxRwKrpeO0JAI0ZpnAFS11uBEe9GSS8uzIIFNYVD1vHloAFKvUJEhyuVyz9/SyqTnArN3ZTiC5cqD1MB86q5QPrKqZfp1dAnv7xAJThL0AP/AgMBAAE=
|
|
||||||
```
|
|
||||||
|
|
||||||
Without that record other email servers may reject your emails as spam, kupo.
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Example</summary>
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ dig TXT postmoogle._domainkey.example.com
|
|
||||||
|
|
||||||
; <<>> DiG 9.18.6 <<>> TXT postmoogle._domainkey.example.com
|
|
||||||
;; 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.example.com. IN TXT
|
|
||||||
|
|
||||||
;; ANSWER SECTION:
|
|
||||||
postmoogle._domainkey.example.com. 600 IN TXT "v=DKIM1; k=rsa; p=MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAxJVqmBHhK9FY93q1o3WEaP2GKMh/3LNMyvi1uSjOKxIyfWv685KxX1EUrbHakQRCTtUM7efKEsXsXBh+DQru2TE32yFpL9afA5BbHj3KePGFY8KJ2m0sQxbQcvn2KjJC0IQ15mk0rninPhtphU/2zLsd6e7Rl1m3L+9Osk320GbfDgSKjRPcSiwVMbLJpSOP0H0F3cIu+c1fHZHfmWy0O+us42C3HTLTlD779LTnQnKlAOQD/+DYYqz6TGGxEwUG2BRQ8O8w7/wXEkg/6a/MxNtPnc59g29CpqRsDkuYiR3UIpqzLDoqHlaoKNbYy34R+4aIjfNpmZyR5kIumws+3MJtJt9UhBTMloqd8lZDPaPmX2NEDqbcSTkHMQrphk+EWSCc7OvbKRaXZ0SyJLpLjxRwKrpeO0JAI0ZpnAFS11uBEe9GSS8uzIIFNYVD1vHloAFKvUJEhyuVyz9/SyqTnArN3ZTiC5cqD1MB86q5QPrKqZfp1dAnv7xAJThL0AP/AgMBAAE="
|
|
||||||
|
|
||||||
;; 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
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -241,6 +93,7 @@ If you want to change them - check available options in the help message (`!pm h
|
|||||||
---
|
---
|
||||||
|
|
||||||
* **!pm mailbox** - Get or set mailbox of the room
|
* **!pm mailbox** - Get or set mailbox of the room
|
||||||
|
* **!pm domain** - Get or set default domain of the room
|
||||||
* **!pm owner** - Get or set owner of the room
|
* **!pm owner** - Get or set owner of the room
|
||||||
* **!pm password** - Get or set SMTP password of the room's mailbox
|
* **!pm password** - Get or set SMTP password of the room's mailbox
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,12 @@ func (b *Bot) initCommands() commandList {
|
|||||||
sanitizer: utils.Mailbox,
|
sanitizer: utils.Mailbox,
|
||||||
allowed: b.allowOwner,
|
allowed: b.allowOwner,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: roomOptionDomain,
|
||||||
|
description: "Get or set default domain of the room",
|
||||||
|
sanitizer: utils.SanitizeDomain,
|
||||||
|
allowed: b.allowOwner,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: roomOptionOwner,
|
key: roomOptionOwner,
|
||||||
description: "Get or set owner of the room",
|
description: "Get or set owner of the room",
|
||||||
@@ -276,7 +282,7 @@ func (b *Bot) sendIntroduction(ctx context.Context, roomID id.RoomID) {
|
|||||||
msg.WriteString(" SOME_INBOX` command.\n")
|
msg.WriteString(" SOME_INBOX` command.\n")
|
||||||
|
|
||||||
msg.WriteString("You will then be able to send emails to ")
|
msg.WriteString("You will then be able to send emails to ")
|
||||||
msg.WriteString(utils.EmailsList("SOME_INBOX", b.domains))
|
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.SendNotice(ctx, roomID, msg.String())
|
||||||
@@ -315,7 +321,7 @@ func (b *Bot) sendHelp(ctx context.Context) {
|
|||||||
msg.WriteString(value)
|
msg.WriteString(value)
|
||||||
if cmd.key == roomOptionMailbox {
|
if cmd.key == roomOptionMailbox {
|
||||||
msg.WriteString(" (")
|
msg.WriteString(" (")
|
||||||
msg.WriteString(utils.EmailsList(value, b.domains))
|
msg.WriteString(utils.EmailsList(value, cfg.Domain()))
|
||||||
msg.WriteString(")")
|
msg.WriteString(")")
|
||||||
}
|
}
|
||||||
msg.WriteString("`)")
|
msg.WriteString("`)")
|
||||||
@@ -376,8 +382,9 @@ func (b *Bot) runSend(ctx context.Context) {
|
|||||||
b.lock(evt.RoomID.String())
|
b.lock(evt.RoomID.String())
|
||||||
defer b.unlock(evt.RoomID.String())
|
defer b.unlock(evt.RoomID.String())
|
||||||
|
|
||||||
from := mailbox + "@" + b.domains[0]
|
domain := utils.SanitizeDomain(cfg.Domain())
|
||||||
ID := utils.MessageID(evt.ID, b.domains[0])
|
from := mailbox + "@" + domain
|
||||||
|
ID := utils.MessageID(evt.ID, domain)
|
||||||
for _, to := range tos {
|
for _, to := range tos {
|
||||||
email := utils.NewEmail(ID, "", " "+ID, subject, from, to, body, "", nil)
|
email := utils.NewEmail(ID, "", " "+ID, subject, from, to, body, "", nil)
|
||||||
data := email.Compose(b.getBotSettings().DKIMPrivateKey())
|
data := email.Compose(b.getBotSettings().DKIMPrivateKey())
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func (b *Bot) sendMailboxes(ctx context.Context) {
|
|||||||
for _, mailbox := range slice {
|
for _, mailbox := range slice {
|
||||||
cfg := mailboxes[mailbox]
|
cfg := mailboxes[mailbox]
|
||||||
msg.WriteString("* `")
|
msg.WriteString("* `")
|
||||||
msg.WriteString(utils.EmailsList(mailbox, b.domains))
|
msg.WriteString(utils.EmailsList(mailbox, cfg.Domain()))
|
||||||
msg.WriteString("` by ")
|
msg.WriteString("` by ")
|
||||||
msg.WriteString(cfg.Owner())
|
msg.WriteString(cfg.Owner())
|
||||||
msg.WriteString("\n")
|
msg.WriteString("\n")
|
||||||
@@ -174,7 +174,7 @@ func (b *Bot) runCatchAll(ctx context.Context, commandSlice []string) {
|
|||||||
if cfg.CatchAll() != "" {
|
if cfg.CatchAll() != "" {
|
||||||
msg.WriteString(cfg.CatchAll())
|
msg.WriteString(cfg.CatchAll())
|
||||||
msg.WriteString(" (")
|
msg.WriteString(" (")
|
||||||
msg.WriteString(utils.EmailsList(cfg.CatchAll(), b.domains))
|
msg.WriteString(utils.EmailsList(cfg.CatchAll(), ""))
|
||||||
msg.WriteString(")")
|
msg.WriteString(")")
|
||||||
} else {
|
} else {
|
||||||
msg.WriteString("not set")
|
msg.WriteString("not set")
|
||||||
@@ -203,5 +203,5 @@ func (b *Bot) runCatchAll(ctx context.Context, commandSlice []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Catch-all is set to: `%s` (%s).", mailbox, utils.EmailsList(mailbox, b.domains)))
|
b.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Catch-all is set to: `%s` (%s).", mailbox, utils.EmailsList(mailbox, "")))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ func (b *Bot) getOption(ctx context.Context, name string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if name == roomOptionMailbox {
|
if name == roomOptionMailbox {
|
||||||
value = utils.EmailsList(value, b.domains)
|
value = utils.EmailsList(value, cfg.Domain())
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := fmt.Sprintf("`%s` of this room is `%s`\n"+
|
msg := fmt.Sprintf("`%s` of this room is `%s`\n"+
|
||||||
@@ -87,7 +87,7 @@ func (b *Bot) setOption(ctx context.Context, name, value string) {
|
|||||||
if name == roomOptionMailbox {
|
if name == roomOptionMailbox {
|
||||||
existingID, ok := b.getMapping(value)
|
existingID, ok := b.getMapping(value)
|
||||||
if ok && existingID != "" && existingID != evt.RoomID {
|
if ok && existingID != "" && existingID != evt.RoomID {
|
||||||
b.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Mailbox `%s` (%s) already taken, kupo", value, utils.EmailsList(value, b.domains)))
|
b.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Mailbox `%s` (%s) already taken, kupo", value, utils.EmailsList(value, "")))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +116,7 @@ func (b *Bot) setOption(ctx context.Context, name, value string) {
|
|||||||
b.rooms.Delete(old)
|
b.rooms.Delete(old)
|
||||||
}
|
}
|
||||||
b.rooms.Store(value, evt.RoomID)
|
b.rooms.Store(value, evt.RoomID)
|
||||||
value = fmt.Sprintf("%s@%s", value, b.domains[0])
|
value = fmt.Sprintf("%s@%s", value, utils.SanitizeDomain(cfg.Domain()))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = b.setRoomSettings(evt.RoomID, cfg)
|
err = b.setRoomSettings(evt.RoomID, cfg)
|
||||||
|
|||||||
16
bot/email.go
16
bot/email.go
@@ -147,12 +147,13 @@ func (b *Bot) SendEmailReply(ctx context.Context) {
|
|||||||
b.Error(ctx, evt.RoomID, "mailbox is not configured, kupo")
|
b.Error(ctx, evt.RoomID, "mailbox is not configured, kupo")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
domain := utils.SanitizeDomain(cfg.Domain())
|
||||||
|
|
||||||
b.lock(evt.RoomID.String())
|
b.lock(evt.RoomID.String())
|
||||||
defer b.unlock(evt.RoomID.String())
|
defer b.unlock(evt.RoomID.String())
|
||||||
|
|
||||||
fromMailbox := mailbox + "@" + b.domains[0]
|
fromMailbox := mailbox + "@" + domain
|
||||||
meta := b.getParentEmail(evt)
|
meta := b.getParentEmail(evt, domain)
|
||||||
// when email was sent from matrix and reply was sent from matrix again
|
// when email was sent from matrix and reply was sent from matrix again
|
||||||
if fromMailbox != meta.From {
|
if fromMailbox != meta.From {
|
||||||
meta.To = meta.From
|
meta.To = meta.From
|
||||||
@@ -173,7 +174,7 @@ func (b *Bot) SendEmailReply(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
body := content.Body
|
body := content.Body
|
||||||
|
|
||||||
meta.MessageID = utils.MessageID(evt.ID, b.domains[0])
|
meta.MessageID = utils.MessageID(evt.ID, domain)
|
||||||
meta.References = meta.References + " " + meta.MessageID
|
meta.References = meta.References + " " + meta.MessageID
|
||||||
b.log.Debug("send email reply: %+v", meta)
|
b.log.Debug("send email reply: %+v", meta)
|
||||||
email := utils.NewEmail(meta.MessageID, meta.InReplyTo, meta.References, meta.Subject, meta.From, meta.To, body, "", nil)
|
email := utils.NewEmail(meta.MessageID, meta.InReplyTo, meta.References, meta.Subject, meta.From, meta.To, body, "", nil)
|
||||||
@@ -240,7 +241,7 @@ func (b *Bot) getParentEvent(evt *event.Event) (id.EventID, *event.Event) {
|
|||||||
return threadID, decrypted
|
return threadID, decrypted
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) getParentEmail(evt *event.Event) parentEmail {
|
func (b *Bot) getParentEmail(evt *event.Event, domain string) parentEmail {
|
||||||
var parent parentEmail
|
var parent parentEmail
|
||||||
threadID, parentEvt := b.getParentEvent(evt)
|
threadID, parentEvt := b.getParentEvent(evt)
|
||||||
parent.ThreadID = threadID
|
parent.ThreadID = threadID
|
||||||
@@ -251,7 +252,7 @@ func (b *Bot) getParentEmail(evt *event.Event) parentEmail {
|
|||||||
return parent
|
return parent
|
||||||
}
|
}
|
||||||
|
|
||||||
parent.MessageID = utils.MessageID(parentEvt.ID, b.domains[0])
|
parent.MessageID = utils.MessageID(parentEvt.ID, domain)
|
||||||
parent.From = utils.EventField[string](&parentEvt.Content, eventFromKey)
|
parent.From = utils.EventField[string](&parentEvt.Content, eventFromKey)
|
||||||
parent.To = utils.EventField[string](&parentEvt.Content, eventToKey)
|
parent.To = utils.EventField[string](&parentEvt.Content, eventToKey)
|
||||||
parent.InReplyTo = utils.EventField[string](&parentEvt.Content, eventMessageIDkey)
|
parent.InReplyTo = utils.EventField[string](&parentEvt.Content, eventMessageIDkey)
|
||||||
@@ -298,8 +299,9 @@ func (b *Bot) saveSentMetadata(ctx context.Context, queued bool, threadID id.Eve
|
|||||||
b.Error(ctx, evt.RoomID, "cannot send notice: %v", err)
|
b.Error(ctx, evt.RoomID, "cannot send notice: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
b.setThreadID(evt.RoomID, utils.MessageID(evt.ID, b.domains[0]), threadID)
|
domain := utils.SanitizeDomain(cfg.Domain())
|
||||||
b.setThreadID(evt.RoomID, utils.MessageID(msgID, b.domains[0]), threadID)
|
b.setThreadID(evt.RoomID, utils.MessageID(evt.ID, domain), threadID)
|
||||||
|
b.setThreadID(evt.RoomID, utils.MessageID(msgID, domain), threadID)
|
||||||
b.setLastEventID(evt.RoomID, threadID, msgID)
|
b.setLastEventID(evt.RoomID, threadID, msgID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const acRoomSettingsKey = "cc.etke.postmoogle.settings"
|
|||||||
const (
|
const (
|
||||||
roomOptionOwner = "owner"
|
roomOptionOwner = "owner"
|
||||||
roomOptionMailbox = "mailbox"
|
roomOptionMailbox = "mailbox"
|
||||||
|
roomOptionDomain = "domain"
|
||||||
roomOptionNoSend = "nosend"
|
roomOptionNoSend = "nosend"
|
||||||
roomOptionNoSender = "nosender"
|
roomOptionNoSender = "nosender"
|
||||||
roomOptionNoRecipient = "norecipient"
|
roomOptionNoRecipient = "norecipient"
|
||||||
@@ -44,6 +45,10 @@ func (s roomSettings) Mailbox() string {
|
|||||||
return s.Get(roomOptionMailbox)
|
return s.Get(roomOptionMailbox)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s roomSettings) Domain() string {
|
||||||
|
return s.Get(roomOptionDomain)
|
||||||
|
}
|
||||||
|
|
||||||
func (s roomSettings) Owner() string {
|
func (s roomSettings) Owner() string {
|
||||||
return s.Get(roomOptionOwner)
|
return s.Get(roomOptionOwner)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ func main() {
|
|||||||
cfg := config.New()
|
cfg := config.New()
|
||||||
log = logger.New("postmoogle.", cfg.LogLevel)
|
log = logger.New("postmoogle.", cfg.LogLevel)
|
||||||
utils.SetLogger(log)
|
utils.SetLogger(log)
|
||||||
|
utils.SetDomains(cfg.Domains)
|
||||||
|
|
||||||
log.Info("#############################")
|
log.Info("#############################")
|
||||||
log.Info("Postmoogle")
|
log.Info("Postmoogle")
|
||||||
|
|||||||
159
docs/dns.md
Normal file
159
docs/dns.md
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
# DNS configuration
|
||||||
|
|
||||||
|
the following configuration is required only if you want to send emails from Postmoogle
|
||||||
|
|
||||||
|
# MX
|
||||||
|
|
||||||
|
Add a new MX DNS record of the `MX` type for your domain that will be used with postmoogle.
|
||||||
|
It should point to the same (sub-)domain.
|
||||||
|
Looks odd, but some mail servers will refuse to interact with your mail server
|
||||||
|
(and Postmoogle is already a mail server) without MX records.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Example</summary>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dig MX example.com
|
||||||
|
|
||||||
|
; <<>> DiG 9.18.6 <<>> MX example.com
|
||||||
|
;; global options: +cmd
|
||||||
|
;; Got answer:
|
||||||
|
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12688
|
||||||
|
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
|
||||||
|
|
||||||
|
;; OPT PSEUDOSECTION:
|
||||||
|
; EDNS: version: 0, flags:; udp: 1232
|
||||||
|
;; QUESTION SECTION:
|
||||||
|
;example.com. IN MX
|
||||||
|
|
||||||
|
;; ANSWER SECTION:
|
||||||
|
example.com. 1799 IN MX 10 example.com.
|
||||||
|
|
||||||
|
;; Query time: 40 msec
|
||||||
|
;; SERVER: 1.1.1.1#53(1.1.1.1) (UDP)
|
||||||
|
;; WHEN: Tue Sep 06 16:44:47 EEST 2022
|
||||||
|
;; MSG SIZE rcvd: 59
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
# SPF
|
||||||
|
|
||||||
|
Aadd a new SPF DNS record of the `TXT` type for your domain that will be used with Postmoogle,
|
||||||
|
with format: `v=spf1 ip4:SERVER_IP4 -all` (replace `SERVER_IP4` with your server's IP address),
|
||||||
|
for servers with IPv6: `v=spf1 ip6:SERVER_IP6 -all` (you may use both `ip4` and `ip6` in one TXT record).
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Example</summary>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ dig txt example.com
|
||||||
|
|
||||||
|
; <<>> DiG 9.18.6 <<>> txt example.com
|
||||||
|
;; global options: +cmd
|
||||||
|
;; Got answer:
|
||||||
|
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 24796
|
||||||
|
;; flags: qr rd ra; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 1
|
||||||
|
|
||||||
|
;; OPT PSEUDOSECTION:
|
||||||
|
; EDNS: version: 0, flags:; udp: 1232
|
||||||
|
;; QUESTION SECTION:
|
||||||
|
;example.com. IN TXT
|
||||||
|
|
||||||
|
;; ANSWER SECTION:
|
||||||
|
example.com. 1799 IN TXT "v=spf1 ip4:111.111.111.111 -all"
|
||||||
|
|
||||||
|
;; Query time: 36 msec
|
||||||
|
;; SERVER: 1.1.1.1#53(1.1.1.1) (UDP)
|
||||||
|
;; WHEN: Sun Sep 04 21:35:04 EEST 2022
|
||||||
|
;; MSG SIZE rcvd: 255
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
# DMARC
|
||||||
|
|
||||||
|
Add a new DMARC DNS record of the `TXT` type for subdomain `_dmarc` with a proper policy.
|
||||||
|
The simplest policy you can use is: `v=DMARC1; p=quarantine;`.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Example</summary>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ dig txt _dmarc.example.com
|
||||||
|
|
||||||
|
; <<>> DiG 9.18.6 <<>> txt _dmarc.example.com
|
||||||
|
;; global options: +cmd
|
||||||
|
;; Got answer:
|
||||||
|
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 57306
|
||||||
|
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
|
||||||
|
|
||||||
|
;; OPT PSEUDOSECTION:
|
||||||
|
; EDNS: version: 0, flags:; udp: 1232
|
||||||
|
;; QUESTION SECTION:
|
||||||
|
;_dmarc.example.com. IN TXT
|
||||||
|
|
||||||
|
;; ANSWER SECTION:
|
||||||
|
_dmarc.example.com. 1799 IN TXT "v=DMARC1; p=quarantine;"
|
||||||
|
|
||||||
|
;; Query time: 46 msec
|
||||||
|
;; SERVER: 1.1.1.1#53(1.1.1.1) (UDP)
|
||||||
|
;; WHEN: Sun Sep 04 21:31:30 EEST 2022
|
||||||
|
;; MSG SIZE rcvd: 79
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
# DKIM
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>!pm dkim</summary>
|
||||||
|
|
||||||
|
DKIM signature is: `v=DKIM1; k=rsa; p=MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAxJVqmBHhK9FY93q1o3WEaP2GKMh/3LNMyvi1uSjOKxIyfWv685KxX1EUrbHakQRCTtUM7efKEsXsXBh+DQru2TE32yFpL9afA5BbHj3KePGFY8KJ2m0sQxbQcvn2KjJC0IQ15mk0rninPhtphU/2zLsd6e7Rl1m3L+9Osk320GbfDgSKjRPcSiwVMbLJpSOP0H0F3cIu+c1fHZHfmWy0O+us42C3HTLTlD779LTnQnKlAOQD/+DYYqz6TGGxEwUG2BRQ8O8w7/wXEkg/6a/MxNtPnc59g29CpqRsDkuYiR3UIpqzLDoqHlaoKNbYy34R+4aIjfNpmZyR5kIumws+3MJtJt9UhBTMloqd8lZDPaPmX2NEDqbcSTkHMQrphk+EWSCc7OvbKRaXZ0SyJLpLjxRwKrpeO0JAI0ZpnAFS11uBEe9GSS8uzIIFNYVD1vHloAFKvUJEhyuVyz9/SyqTnArN3ZTiC5cqD1MB86q5QPrKqZfp1dAnv7xAJThL0AP/AgMBAAE=`.
|
||||||
|
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=rsa; p=MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAxJVqmBHhK9FY93q1o3WEaP2GKMh/3LNMyvi1uSjOKxIyfWv685KxX1EUrbHakQRCTtUM7efKEsXsXBh+DQru2TE32yFpL9afA5BbHj3KePGFY8KJ2m0sQxbQcvn2KjJC0IQ15mk0rninPhtphU/2zLsd6e7Rl1m3L+9Osk320GbfDgSKjRPcSiwVMbLJpSOP0H0F3cIu+c1fHZHfmWy0O+us42C3HTLTlD779LTnQnKlAOQD/+DYYqz6TGGxEwUG2BRQ8O8w7/wXEkg/6a/MxNtPnc59g29CpqRsDkuYiR3UIpqzLDoqHlaoKNbYy34R+4aIjfNpmZyR5kIumws+3MJtJt9UhBTMloqd8lZDPaPmX2NEDqbcSTkHMQrphk+EWSCc7OvbKRaXZ0SyJLpLjxRwKrpeO0JAI0ZpnAFS11uBEe9GSS8uzIIFNYVD1vHloAFKvUJEhyuVyz9/SyqTnArN3ZTiC5cqD1MB86q5QPrKqZfp1dAnv7xAJThL0AP/AgMBAAE=
|
||||||
|
```
|
||||||
|
|
||||||
|
Without that record other email servers may reject your emails as spam, kupo.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Example</summary>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ dig TXT postmoogle._domainkey.example.com
|
||||||
|
|
||||||
|
; <<>> DiG 9.18.6 <<>> TXT postmoogle._domainkey.example.com
|
||||||
|
;; 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.example.com. IN TXT
|
||||||
|
|
||||||
|
;; ANSWER SECTION:
|
||||||
|
postmoogle._domainkey.example.com. 600 IN TXT "v=DKIM1; k=rsa; p=MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAxJVqmBHhK9FY93q1o3WEaP2GKMh/3LNMyvi1uSjOKxIyfWv685KxX1EUrbHakQRCTtUM7efKEsXsXBh+DQru2TE32yFpL9afA5BbHj3KePGFY8KJ2m0sQxbQcvn2KjJC0IQ15mk0rninPhtphU/2zLsd6e7Rl1m3L+9Osk320GbfDgSKjRPcSiwVMbLJpSOP0H0F3cIu+c1fHZHfmWy0O+us42C3HTLTlD779LTnQnKlAOQD/+DYYqz6TGGxEwUG2BRQ8O8w7/wXEkg/6a/MxNtPnc59g29CpqRsDkuYiR3UIpqzLDoqHlaoKNbYy34R+4aIjfNpmZyR5kIumws+3MJtJt9UhBTMloqd8lZDPaPmX2NEDqbcSTkHMQrphk+EWSCc7OvbKRaXZ0SyJLpLjxRwKrpeO0JAI0ZpnAFS11uBEe9GSS8uzIIFNYVD1vHloAFKvUJEhyuVyz9/SyqTnArN3ZTiC5cqD1MB86q5QPrKqZfp1dAnv7xAJThL0AP/AgMBAAE="
|
||||||
|
|
||||||
|
;; 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
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
# rDNS
|
||||||
|
|
||||||
|
> additional PTR record will help you to get better spam score
|
||||||
|
|
||||||
|
Configure Reverse DNS of your server. Unfortunately, rDNS is provider-specific, so you have to find out how to configure it with your hosting provider. Search for something like: `PROVIDER configure "rdns"` (where `PROVIDER` is your hosting provider name)
|
||||||
@@ -7,13 +7,21 @@ import (
|
|||||||
"gitlab.com/etke.cc/go/logger"
|
"gitlab.com/etke.cc/go/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
var log *logger.Logger
|
var (
|
||||||
|
log *logger.Logger
|
||||||
|
domains []string
|
||||||
|
)
|
||||||
|
|
||||||
// SetLogger for utils
|
// SetLogger for utils
|
||||||
func SetLogger(loggerInstance *logger.Logger) {
|
func SetLogger(loggerInstance *logger.Logger) {
|
||||||
log = loggerInstance
|
log = loggerInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetDomains for later use
|
||||||
|
func SetDomains(slice []string) {
|
||||||
|
domains = slice
|
||||||
|
}
|
||||||
|
|
||||||
// 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, "@")
|
index := strings.LastIndex(email, "@")
|
||||||
@@ -24,16 +32,24 @@ func Mailbox(email string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
func EmailsList(mailbox string, domains []string) string {
|
func EmailsList(mailbox string, domain string) string {
|
||||||
var msg strings.Builder
|
var msg strings.Builder
|
||||||
|
domain = SanitizeDomain(domain)
|
||||||
|
msg.WriteString(mailbox)
|
||||||
|
msg.WriteString("@")
|
||||||
|
msg.WriteString(domain)
|
||||||
|
|
||||||
count := len(domains) - 1
|
count := len(domains) - 1
|
||||||
for i, domain := range domains {
|
for i, aliasDomain := range domains {
|
||||||
msg.WriteString(mailbox)
|
|
||||||
msg.WriteString("@")
|
|
||||||
msg.WriteString(domain)
|
|
||||||
if i < count {
|
if i < count {
|
||||||
msg.WriteString(", ")
|
msg.WriteString(", ")
|
||||||
}
|
}
|
||||||
|
if aliasDomain == domain {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
msg.WriteString(mailbox)
|
||||||
|
msg.WriteString("@")
|
||||||
|
msg.WriteString(aliasDomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
return msg.String()
|
return msg.String()
|
||||||
@@ -44,6 +60,22 @@ func Hostname(email string) string {
|
|||||||
return email[strings.LastIndex(email, "@")+1:]
|
return email[strings.LastIndex(email, "@")+1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SanitizeDomain checks that input domain is available for use
|
||||||
|
func SanitizeDomain(domain string) string {
|
||||||
|
domain = strings.TrimSpace(domain)
|
||||||
|
if domain == "" {
|
||||||
|
return domains[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, allowed := range domains {
|
||||||
|
if domain == allowed {
|
||||||
|
return domain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return domains[0]
|
||||||
|
}
|
||||||
|
|
||||||
// Bool converts string to boolean
|
// Bool converts string to boolean
|
||||||
func Bool(str string) bool {
|
func Bool(str string) bool {
|
||||||
str = strings.ToLower(str)
|
str = strings.ToLower(str)
|
||||||
|
|||||||
Reference in New Issue
Block a user