From 6be4891165c88c381a2c61825520c12fda7401b4 Mon Sep 17 00:00:00 2001 From: Aine Date: Mon, 25 Sep 2023 23:20:17 +0300 Subject: [PATCH] subaddressing support, closes #61 --- README.md | 1 + e2e/send | 2 +- e2e/simple.eml | 2 +- email/email.go | 22 +- go.mod | 1 + go.sum | 3 + utils/mail.go | 57 ++++- utils/mail_test.go | 70 ++++++ .../mcnijman/go-emailaddress/.gitignore | 2 + .../mcnijman/go-emailaddress/.travis.yml | 25 ++ .../mcnijman/go-emailaddress/LICENCE | 21 ++ .../mcnijman/go-emailaddress/README.md | 102 ++++++++ .../mcnijman/go-emailaddress/emailaddress.go | 224 ++++++++++++++++++ vendor/modules.txt | 3 + 14 files changed, 519 insertions(+), 16 deletions(-) create mode 100644 utils/mail_test.go create mode 100644 vendor/github.com/mcnijman/go-emailaddress/.gitignore create mode 100644 vendor/github.com/mcnijman/go-emailaddress/.travis.yml create mode 100644 vendor/github.com/mcnijman/go-emailaddress/LICENCE create mode 100644 vendor/github.com/mcnijman/go-emailaddress/README.md create mode 100644 vendor/github.com/mcnijman/go-emailaddress/emailaddress.go diff --git a/README.md b/README.md index a2a40e8..b210a94 100644 --- a/README.md +++ b/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] 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 diff --git a/e2e/send b/e2e/send index 2aeeab9..c8b8be0 100755 --- a/e2e/send +++ b/e2e/send @@ -1,3 +1,3 @@ #!/bin/bash -ssmtp -v test@localhost < $1 +ssmtp -v test+sub@localhost < $1 diff --git a/e2e/simple.eml b/e2e/simple.eml index 56eb757..03d0194 100644 --- a/e2e/simple.eml +++ b/e2e/simple.eml @@ -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) diff --git a/email/email.go b/email/email.go index cbe792e..9d2b226 100644 --- a/email/email.go +++ b/email/email.go @@ -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 { diff --git a/go.mod b/go.mod index 484e132..54c6077 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index b87e519..d0b4bda 100644 --- a/go.sum +++ b/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-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= diff --git a/utils/mail.go b/utils/mail.go index 3be1b25..79ef9d8 100644 --- a/utils/mail.go +++ b/utils/mail.go @@ -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:] -} diff --git a/utils/mail_test.go b/utils/mail_test.go new file mode 100644 index 0000000..8ee4b21 --- /dev/null +++ b/utils/mail_test.go @@ -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) + } +} diff --git a/vendor/github.com/mcnijman/go-emailaddress/.gitignore b/vendor/github.com/mcnijman/go-emailaddress/.gitignore new file mode 100644 index 0000000..c0613ec --- /dev/null +++ b/vendor/github.com/mcnijman/go-emailaddress/.gitignore @@ -0,0 +1,2 @@ +emailaddress +coverage.* \ No newline at end of file diff --git a/vendor/github.com/mcnijman/go-emailaddress/.travis.yml b/vendor/github.com/mcnijman/go-emailaddress/.travis.yml new file mode 100644 index 0000000..e0799ba --- /dev/null +++ b/vendor/github.com/mcnijman/go-emailaddress/.travis.yml @@ -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 diff --git a/vendor/github.com/mcnijman/go-emailaddress/LICENCE b/vendor/github.com/mcnijman/go-emailaddress/LICENCE new file mode 100644 index 0000000..4dfcb58 --- /dev/null +++ b/vendor/github.com/mcnijman/go-emailaddress/LICENCE @@ -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. diff --git a/vendor/github.com/mcnijman/go-emailaddress/README.md b/vendor/github.com/mcnijman/go-emailaddress/README.md new file mode 100644 index 0000000..525cc43 --- /dev/null +++ b/vendor/github.com/mcnijman/go-emailaddress/README.md @@ -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. \ No newline at end of file diff --git a/vendor/github.com/mcnijman/go-emailaddress/emailaddress.go b/vendor/github.com/mcnijman/go-emailaddress/emailaddress.go new file mode 100644 index 0000000..a78ae6a --- /dev/null +++ b/vendor/github.com/mcnijman/go-emailaddress/emailaddress.go @@ -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 +} diff --git a/vendor/modules.txt b/vendor/modules.txt index a32e6c9..69f772d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -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