subaddressing support, closes #61

This commit is contained in:
Aine
2023-09-25 23:20:17 +03:00
parent 18f1113d33
commit 6be4891165
14 changed files with 519 additions and 16 deletions

View File

@@ -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] Receive emails to matrix rooms
- [x] Receive attachments
- [x] Subaddressing support
- [x] Catch-all mailbox
- [x] Map email threads to matrix threads
- [x] Multi-domain support

View File

@@ -1,3 +1,3 @@
#!/bin/bash
ssmtp -v test@localhost < $1
ssmtp -v test+sub@localhost < $1

View File

@@ -3,7 +3,7 @@ Content-Type: multipart/alternative; boundary="Apple-Mail=_E091454E-BCFA-43B4-99
Subject: MIME test 1
Date: Sat, 13 Oct 2012 15:33:07 -0700
Message-Id: <4E2E5A48-1A2C-4450-8663-D41B451DA93A@makita.skynet>
To: test@localhost
To: test+sub@localhost
Mime-Version: 1.0 (Apple Message framework v1283)
X-Mailer: Apple Mail (2.1283)

View File

@@ -107,15 +107,21 @@ func (e *Email) Mailbox(incoming bool) string {
return utils.Mailbox(e.From)
}
// 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
func (e *Email) contentHeader(threadID id.EventID, text *strings.Builder, options *ContentOptions) {
if options.Sender {
text.WriteString(e.From)
}
if options.Recipient {
mailbox, sub, host := utils.EmailParts(e.To)
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 {
text.WriteString("\ncc: ")
@@ -129,6 +135,14 @@ func (e *Email) Content(threadID id.EventID, options *ContentOptions) *event.Con
text.WriteString(e.Subject)
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 {
text.WriteString(format.HTMLToMarkdown(e.HTML))
} else {

1
go.mod
View File

@@ -15,6 +15,7 @@ require (
github.com/jhillyerd/enmime v0.10.0
github.com/lib/pq v1.10.9
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/raja/argon2pw v1.0.2-0.20210910183755-a391af63bd39
github.com/rs/zerolog v1.30.0

3
go.sum
View File

@@ -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-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
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/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ=
github.com/mileusna/crontab v1.2.0 h1:x9ZmE2A4p6CDqMEGQ+GbqsNtnmbdmWMQYShdQu8LvrU=
@@ -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/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/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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=

View File

@@ -1,14 +1,56 @@
package utils
import "strings"
import (
"strings"
"github.com/mcnijman/go-emailaddress"
)
// Mailbox returns mailbox part from email address
func Mailbox(email string) string {
index := strings.LastIndex(email, "@")
if index == -1 {
return email
mailbox, _, _ := EmailParts(email)
return mailbox
}
// 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
@@ -34,8 +76,3 @@ func EmailsList(mailbox string, domain string) 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
View 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)
}
}

View File

@@ -0,0 +1,2 @@
emailaddress
coverage.*

25
vendor/github.com/mcnijman/go-emailaddress/.travis.yml generated vendored Normal file
View 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
View 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
View File

@@ -0,0 +1,102 @@
# go-emailaddress #
[![GoDoc](https://godoc.org/github.com/mcnijman/go-emailaddress?status.svg)](https://godoc.org/github.com/mcnijman/go-emailaddress) [![Build Status](https://travis-ci.org/mcnijman/go-emailaddress.svg?branch=master)](https://travis-ci.org/mcnijman/go-emailaddress) [![Test Coverage](https://coveralls.io/repos/github/mcnijman/go-emailaddress/badge.svg?branch=master)](https://coveralls.io/github/mcnijman/go-emailaddress?branch=master) [![go report](https://goreportcard.com/badge/github.com/mcnijman/go-emailaddress)](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.

View 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
}

3
vendor/modules.txt vendored
View File

@@ -71,6 +71,9 @@ github.com/mattn/go-runewidth
# github.com/mattn/go-sqlite3 v1.14.17
## explicit; go 1.16
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
## explicit
github.com/mikesmitty/edkey