diff --git a/go/cmd/email.go b/go/cmd/email.go index 08daff401..d6abe42f6 100644 --- a/go/cmd/email.go +++ b/go/cmd/email.go @@ -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, diff --git a/go/cmd/serve.go b/go/cmd/serve.go index f05d7f038..9374c429b 100644 --- a/go/cmd/serve.go +++ b/go/cmd/serve.go @@ -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" @@ -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", diff --git a/go/notifications/email.go b/go/notifications/email.go index 3c1453308..29d68e488 100644 --- a/go/notifications/email.go +++ b/go/notifications/email.go @@ -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, } } @@ -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}, diff --git a/go/notifications/email_test.go b/go/notifications/email_test.go index 49a070370..c0a0e12f6 100644 --- a/go/notifications/email_test.go +++ b/go/notifications/email_test.go @@ -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{ @@ -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{ diff --git a/go/notifications/smtp_auth.go b/go/notifications/smtp_auth.go deleted file mode 100644 index 50c5d59e2..000000000 --- a/go/notifications/smtp_auth.go +++ /dev/null @@ -1,47 +0,0 @@ -package notifications - -import ( - "errors" - "net/smtp" -) - -// 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. -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 -} diff --git a/go/notifications/stdlib.go b/go/notifications/stdlib.go new file mode 100644 index 000000000..2cac8041d --- /dev/null +++ b/go/notifications/stdlib.go @@ -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 +}