Skip to content

Commit

Permalink
fix: support SMTP over TLS (#498)
Browse files Browse the repository at this point in the history
  • Loading branch information
dbarrosop authored Apr 4, 2024
1 parent ed8edf3 commit e08950c
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 61 deletions.
1 change: 1 addition & 0 deletions go/cmd/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ func getSMTPEmailer(cCtx *cli.Context, logger *slog.Logger) (*notifications.Emai
return notifications.NewEmail(
cCtx.String(flagSMTPHost),
uint16(cCtx.Uint(flagSMTPPort)),
cCtx.Bool(flagSMTPSecure),
auth,
cCtx.String(flagSMTPSender),
headers,
Expand Down
7 changes: 7 additions & 0 deletions go/cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const (
flagEmailSigninEmailVerifiedRequired = "email-verification-required"
flagSMTPHost = "smtp-host"
flagSMTPPort = "smtp-port"
flagSMTPSecure = "smtp-secure"
flagSMTPUser = "smtp-user"
flagSMTPPassword = "smtp-password"
flagSMTPSender = "smtp-sender"
Expand Down Expand Up @@ -251,6 +252,12 @@ func CommandServe() *cli.Command { //nolint:funlen,maintidx
Value: 587, //nolint:gomnd
EnvVars: []string{"AUTH_SMTP_PORT"},
},
&cli.BoolFlag{ //nolint: exhaustruct
Name: flagSMTPSecure,
Usage: "Connect over TLS. Deprecated: It is recommended to use port 587 with STARTTLS instead of this option.",
Category: "smtp",
EnvVars: []string{"AUTH_SMTP_SECURE"},
},
&cli.StringFlag{ //nolint: exhaustruct
Name: flagSMTPUser,
Usage: "SMTP user",
Expand Down
33 changes: 19 additions & 14 deletions go/notifications/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,32 @@ import (
)

type Email struct {
from string
address string
extraHeaders map[string]string
auth smtp.Auth
templates *Templates
from string
host string
port uint16
useTLSConnection bool
extraHeaders map[string]string
auth smtp.Auth
templates *Templates
}

func NewEmail(
host string,
port uint16,
useTLSConnection bool,
auth smtp.Auth,
from string,
extraHeaders map[string]string,
templates *Templates,
) *Email {
address := fmt.Sprintf("%s:%d", host, port)

return &Email{
from: from,
address: address,
extraHeaders: extraHeaders,
auth: auth,
templates: templates,
from: from,
host: host,
port: port,
useTLSConnection: useTLSConnection,
extraHeaders: extraHeaders,
auth: auth,
templates: templates,
}
}

Expand All @@ -49,8 +52,10 @@ func (sm *Email) Send(to, subject, contents string, headers map[string]string) e
buf.WriteString("\r\n")
buf.WriteString(contents + "\r\n")

if err := smtp.SendMail(
sm.address,
if err := sendMail(
sm.host,
sm.port,
sm.useTLSConnection,
sm.auth,
sm.from,
[]string{to},
Expand Down
2 changes: 2 additions & 0 deletions go/notifications/email_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func TestEmailSend(t *testing.T) {
mail := notifications.NewEmail(
"localhost",
1025,
false,
smtp.PlainAuth("", "user", "password", "localhost"),
"admin@localhost",
map[string]string{
Expand Down Expand Up @@ -59,6 +60,7 @@ func TestEmailSendEmailVerify(t *testing.T) {
mail := notifications.NewEmail(
"localhost",
1025,
false,
smtp.PlainAuth("", "user", "password", "localhost"),
"admin@localhost",
map[string]string{
Expand Down
47 changes: 0 additions & 47 deletions go/notifications/smtp_auth.go

This file was deleted.

147 changes: 147 additions & 0 deletions go/notifications/stdlib.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// The contents of this files are modified libraris from the Go standard library.
// The original code can be found at https://cs.opensource.google/go/go/+/refs/tags/go1.22.2:src/net/smtp/smtp.go;l=321
//
// Copyright belongs to the Go authors.
package notifications

import (
"crypto/tls"
"errors"
"fmt"
"net"
"net/smtp"
"strings"
)

const TLSPort = 465

func validateLine(line string) error {
if strings.ContainsAny(line, "\n\r") {
return errors.New("smtp: A line must not contain CR or LF") //nolint
}
return nil
}

func sendMail( //nolint:funlen,cyclop
host string,
port uint16,
useTLSConnection bool,
a smtp.Auth,
from string,
to []string,
msg []byte,
) error {
if err := validateLine(from); err != nil {
return err
}
for _, recp := range to {
if err := validateLine(recp); err != nil {
return err
}
}

addr := fmt.Sprintf("%s:%d", host, port)
var conn net.Conn
var err error
if useTLSConnection {
tlsconfig := &tls.Config{ //nolint:gosec,exhaustruct
InsecureSkipVerify: false,
ServerName: host,
}

conn, err = tls.Dial("tcp", addr, tlsconfig)
if err != nil {
return err //nolint:wrapcheck
}
} else {
conn, err = net.Dial("tcp", addr)
if err != nil {
return err //nolint:wrapcheck
}
}

c, err := smtp.NewClient(conn, host)
if err != nil {
return err //nolint:wrapcheck
}
defer c.Close()

if err = c.Hello("hasura-auth"); err != nil {
return err //nolint:wrapcheck
}
if ok, _ := c.Extension("STARTTLS"); ok {
config := &tls.Config{ServerName: addr} //nolint:gosec,exhaustruct
if err = c.StartTLS(config); err != nil {
return err //nolint:wrapcheck
}
}

if err = c.Auth(a); err != nil {
return err //nolint:wrapcheck
}

if err = c.Mail(from); err != nil {
return err //nolint:wrapcheck
}
for _, addr := range to {
if err = c.Rcpt(addr); err != nil {
return err //nolint:wrapcheck
}
}
w, err := c.Data()
if err != nil {
return err //nolint:wrapcheck
}
_, err = w.Write(msg)
if err != nil {
return err //nolint:wrapcheck
}
err = w.Close()
if err != nil {
return err //nolint:wrapcheck
}
return c.Quit() //nolint:wrapcheck
}

// This is a copy of the smtp.PlainAuth function from the Go standard library.
// It is copied here because we want to allow mailhog to be used as a mail server
// without requiring TLS. The standard library's smtp.PlainAuth function requires
// TLS to be enabled unless the server is localhost.
//
// Copyright belongs to the Go authors.
type plainAuth struct {
identity, username, password string
host string
}

func PlainAuth(identity, username, password, host string) smtp.Auth {
return &plainAuth{identity, username, password, host}
}

func isLocalhost(name string) bool {
return name == "mailhog" || name == "localhost" || name == "127.0.0.1" || name == "::1"
}

func (a *plainAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
// Must have TLS, or else localhost server.
// Note: If TLS is not true, then we can't trust ANYTHING in ServerInfo.
// In particular, it doesn't matter if the server advertises PLAIN auth.
// That might just be the attacker saying
// "it's ok, you can trust me with your password."
if !server.TLS && !isLocalhost(server.Name) {
return "", nil, errors.New("unencrypted connection") //nolint:goerr113
}
if server.Name != a.host {
return "", nil, errors.New("wrong host name") //nolint:goerr113
}
resp := []byte(a.identity + "\x00" + a.username + "\x00" + a.password)
return "PLAIN", resp, nil
}

func (a *plainAuth) Next(_ []byte, more bool) ([]byte, error) {
if more {
// We've already sent everything.
return nil, errors.New("unexpected server challenge") //nolint:goerr113
}
return nil, nil
}

0 comments on commit e08950c

Please sign in to comment.