Skip to content

Commit

Permalink
feat: support setting custom sender domain (#79)
Browse files Browse the repository at this point in the history
* feat: support setting custom sender domain

Some SMTP servers, such as the Gmail SMTP-relay, requires a proper
domain name in the inital EHLO/HELO exchange and will reject attempts to
use localhost.

* ci: replace deprecated linters

Switch deprecated linters to their suggested replacements.

* fix: handle HELO errors when using localname

Check the return value of the HELO when specifying a sender domain name.

---------

Co-authored-by: Dom Dwyer <[email protected]>
  • Loading branch information
taisph and domodwyer authored Feb 25, 2023
1 parent ca22342 commit 8cabd8d
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 7 deletions.
4 changes: 2 additions & 2 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ output:

linters:
enable:
- golint
- revive
- gosec
- unconvert
- gocyclo
- goimports
- nakedret
- scopelint
- exportloopref
- exhaustive
- exportloopref

Expand Down
7 changes: 7 additions & 0 deletions mailyak.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type MailYak struct {
html BodyPart
plain BodyPart

localName string
toAddrs []string
ccAddrs []string
bccAddrs []string
Expand Down Expand Up @@ -170,6 +171,12 @@ func (m *MailYak) Plain() *BodyPart {
return &m.plain
}

// getLocalName should return the sender domain to be used in the EHLO/HELO
// command.
func (m *MailYak) getLocalName() string {
return m.localName
}

// getToAddrs should return a slice of email addresses to be added to the
// RCPT TO command.
func (m *MailYak) getToAddrs() []string {
Expand Down
10 changes: 10 additions & 0 deletions sender.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ type emailSender interface {

// sendableMail provides a set of methods to describe an email to a SMTP server.
type sendableMail interface {
// getLocalName should return the sender domain to be used in the EHLO/HELO
// command.
getLocalName() string

// getToAddrs should return a slice of email addresses to be added to the
// RCPT TO command.
getToAddrs() []string
Expand Down Expand Up @@ -46,6 +50,12 @@ func smtpExchange(m sendableMail, conn net.Conn, serverName string, tryTLSUpgrad
}
defer func() { _ = c.Quit() }()

if localName := m.getLocalName(); localName != "" {
if err := c.Hello(localName); err != nil {
return err
}
}

if tryTLSUpgrade {
if ok, _ := c.Extension("STARTTLS"); ok {
//nolint:gosec
Expand Down
59 changes: 54 additions & 5 deletions sender_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,17 @@ func newConnAsserts(c net.Conn, t *testing.T) *connAsserts {
// mockMail provides the methods for a sendableMail, allowing for deterministic
// MIME content in tests.
type mockMail struct {
toAddrs []string
fromAddr string
auth smtp.Auth
mime string
localName string
toAddrs []string
fromAddr string
auth smtp.Auth
mime string
}

// getLocalName should return the sender domain to be used in the EHLO/HELO
// command.
func (m *mockMail) getLocalName() string {
return m.localName
}

// toAddrs should return a slice of email addresses to be added to the RCPT
Expand Down Expand Up @@ -262,6 +269,48 @@ func TestSMTPProtocolExchange(t *testing.T) {
wantTLSErr: nil,
wantPlaintextErr: nil,
},
{
name: "with localname",
mail: &mockMail{
toAddrs: []string{
"[email protected]",
"[email protected]",
"Dom <[email protected]>",
},
fromAddr: "[email protected]",
mime: "bananas",
localName: "example.com",
},
connFn: func(c *connAsserts) {
c.Respond("220 localhost ESMTP bananas\r\n")

c.Expect("EHLO example.com\r\n")
c.Respond("250-example.com Hola\r\n")
c.Respond("250 AUTH LOGIN PLAIN\r\n")

c.Expect("MAIL FROM:<[email protected]>\r\n")
c.Respond("250 OK\r\n")

c.Expect("RCPT TO:<[email protected]>\r\n")
c.Respond("250 OK\r\n")

c.Expect("RCPT TO:<[email protected]>\r\n")
c.Respond("250 OK\r\n")

c.Expect("RCPT TO:<[email protected]>\r\n")
c.Respond("250 OK\r\n")

c.Expect("DATA\r\n")
c.Respond("354 OK\r\n")
c.Expect("bananas\r\n.\r\n")
c.Respond("250 Will do friend\r\n")

c.Expect("QUIT\r\n")
c.Respond("221 Adios\r\n")
},
wantTLSErr: nil,
wantPlaintextErr: nil,
},
}

// handleConn provides the accept loop for both the TLS server, and the
Expand All @@ -282,7 +331,7 @@ func TestSMTPProtocolExchange(t *testing.T) {
}

for _, tt := range tests {
var tt = tt
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

Expand Down
10 changes: 10 additions & 0 deletions setters.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,13 @@ func (m *MailYak) Subject(sub string) {
func (m *MailYak) AddHeader(name, value string) {
m.headers[m.trimRegex.ReplaceAllString(name, "")] = mime.QEncoding.Encode("UTF-8", m.trimRegex.ReplaceAllString(value, ""))
}

// LocalName sets the sender domain name.
//
// If set, it is used in the EHLO/HELO command instead of the default domain
// (localhost, see [smtp.NewClient]). Some SMTP servers, such as the Gmail
// SMTP-relay, requires a proper domain name and will reject attempts to use
// localhost.
func (m *MailYak) LocalName(name string) {
m.localName = m.trimRegex.ReplaceAllString(name, "")
}
41 changes: 41 additions & 0 deletions setters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,3 +301,44 @@ func TestMailYakAddHeader(t *testing.T) {
})
}
}

func TestMailYakLocalName(t *testing.T) {
t.Parallel()

tests := []struct {
// Test description.
name string
// Parameters.
from string
// Want
want string
}{
{
"empty",
"",
"",
},
{
"ASCII",
"example.com\r\n",
"example.com",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

m := &MailYak{
headers: map[string]string{},
trimRegex: regexp.MustCompile("\r?\n"),
}

m.LocalName(tt.from)

if !reflect.DeepEqual(m.localName, tt.want) {
t.Errorf("%q. MailYak.LocalName() = %v, want %v", tt.name, m.localName, tt.want)
}
})
}
}

0 comments on commit 8cabd8d

Please sign in to comment.