fix: support SMTP STARTTLS mode and NTLM auth (#5426)
* fix: support SMTP STARTTLS mode and NTLM auth Add explicit SMTP STARTTLS configuration for 587-style connections and keep SSL/TLS as the implicit TLS mode. Prefer PLAIN when advertised, keep LOGIN compatibility, and add NTLM as a fallback for Exchange SMTP servers that require it after STARTTLS. * fix: respect explicit SMTP encryption mode * fix: preserve SMTP TLS compatibility
This commit is contained in:
@@ -120,6 +120,8 @@ var InsecureTLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
var SMTPServer = ""
|
||||
var SMTPPort = 587
|
||||
var SMTPSSLEnabled = false
|
||||
var SMTPStartTLSEnabled = false
|
||||
var SMTPInsecureSkipVerify = false
|
||||
var SMTPForceAuthLogin = false
|
||||
var SMTPAccount = ""
|
||||
var SMTPFrom = ""
|
||||
|
||||
+72
-40
@@ -27,10 +27,52 @@ func shouldUseSMTPLoginAuth() bool {
|
||||
}
|
||||
|
||||
func getSMTPAuth() smtp.Auth {
|
||||
if shouldUseSMTPLoginAuth() {
|
||||
return LoginAuth(SMTPAccount, SMTPToken)
|
||||
return AutoSMTPAuth(SMTPAccount, SMTPToken)
|
||||
}
|
||||
|
||||
func shouldAuthenticateSMTP() bool {
|
||||
return SMTPAccount != "" && SMTPToken != ""
|
||||
}
|
||||
|
||||
func smtpTLSConfig() *tls.Config {
|
||||
return &tls.Config{
|
||||
ServerName: SMTPServer,
|
||||
InsecureSkipVerify: SMTPInsecureSkipVerify, // #nosec G402 -- admin-controlled SMTP compatibility option.
|
||||
}
|
||||
return smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer)
|
||||
}
|
||||
|
||||
func newSMTPClient(addr string) (*smtp.Client, error) {
|
||||
if SMTPSSLEnabled || (SMTPPort == 465 && !SMTPStartTLSEnabled) {
|
||||
conn, err := tls.Dial("tcp", addr, smtpTLSConfig())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err := smtp.NewClient(conn, SMTPServer)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
client, err := smtp.Dial(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if SMTPStartTLSEnabled {
|
||||
startTLSSupported, _ := client.Extension("STARTTLS")
|
||||
if !startTLSSupported {
|
||||
_ = client.Close()
|
||||
return nil, fmt.Errorf("SMTP server does not support STARTTLS")
|
||||
}
|
||||
if err := client.StartTLS(smtpTLSConfig()); err != nil {
|
||||
_ = client.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func SendEmail(subject string, receiver string, content string) error {
|
||||
@@ -56,47 +98,37 @@ func SendEmail(subject string, receiver string, content string) error {
|
||||
addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort)
|
||||
to := strings.Split(receiver, ";")
|
||||
var err error
|
||||
if SMTPPort == 465 || SMTPSSLEnabled {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: SMTPServer,
|
||||
}
|
||||
conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", SMTPServer, SMTPPort), tlsConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := smtp.NewClient(conn, SMTPServer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
client, err := newSMTPClient(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
if shouldAuthenticateSMTP() {
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = client.Mail(SMTPFrom); err != nil {
|
||||
return err
|
||||
}
|
||||
receiverEmails := strings.Split(receiver, ";")
|
||||
for _, receiver := range receiverEmails {
|
||||
if err = client.Rcpt(receiver); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(mail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
err = smtp.SendMail(addr, auth, SMTPFrom, to, mail)
|
||||
}
|
||||
if err = client.Mail(SMTPFrom); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, receiver := range to {
|
||||
if err = client.Rcpt(receiver); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(mail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = client.Quit()
|
||||
if err != nil {
|
||||
SysError(fmt.Sprintf("failed to send email to %s: %v", receiver, err))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
|
||||
ntlmssp "github.com/Azure/go-ntlmssp"
|
||||
)
|
||||
|
||||
type smtpAutoAuth struct {
|
||||
username string
|
||||
password string
|
||||
mech string
|
||||
}
|
||||
|
||||
func AutoSMTPAuth(username, password string) smtp.Auth {
|
||||
return &smtpAutoAuth{username: username, password: password}
|
||||
}
|
||||
|
||||
func (a *smtpAutoAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
|
||||
useLoginAuth := SMTPForceAuthLogin
|
||||
if !useLoginAuth && shouldUseSMTPLoginAuth() {
|
||||
useLoginAuth = !(server != nil && len(server.Auth) == 1 && smtpServerSupportsAuth(server, "NTLM"))
|
||||
}
|
||||
if useLoginAuth {
|
||||
a.mech = "LOGIN"
|
||||
return "LOGIN", []byte{}, nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case smtpServerSupportsAuth(server, "PLAIN"):
|
||||
a.mech = "PLAIN"
|
||||
return "PLAIN", []byte("\x00" + a.username + "\x00" + a.password), nil
|
||||
case smtpServerSupportsAuth(server, "LOGIN"):
|
||||
a.mech = "LOGIN"
|
||||
return "LOGIN", []byte{}, nil
|
||||
case smtpServerSupportsAuth(server, "NTLM"):
|
||||
a.mech = "NTLM"
|
||||
negotiateMessage, err := ntlmssp.NewNegotiateMessage("", "")
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return "NTLM", negotiateMessage, nil
|
||||
default:
|
||||
a.mech = "PLAIN"
|
||||
return "PLAIN", []byte("\x00" + a.username + "\x00" + a.password), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (a *smtpAutoAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||
if !more {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch a.mech {
|
||||
case "LOGIN":
|
||||
switch string(fromServer) {
|
||||
case "Username:":
|
||||
return []byte(a.username), nil
|
||||
case "Password:":
|
||||
return []byte(a.password), nil
|
||||
default:
|
||||
return nil, errors.New("unknown SMTP AUTH LOGIN challenge")
|
||||
}
|
||||
case "NTLM":
|
||||
return ntlmssp.NewAuthenticateMessage(fromServer, a.username, a.password, nil)
|
||||
default:
|
||||
return nil, errors.New("unexpected SMTP auth challenge")
|
||||
}
|
||||
}
|
||||
|
||||
func smtpServerSupportsAuth(server *smtp.ServerInfo, mechanism string) bool {
|
||||
if server == nil {
|
||||
return false
|
||||
}
|
||||
for _, auth := range server.Auth {
|
||||
if strings.EqualFold(auth, mechanism) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,531 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type fakeSMTPServer struct {
|
||||
listener net.Listener
|
||||
host string
|
||||
port int
|
||||
cert tls.Certificate
|
||||
advertiseSTARTTLS bool
|
||||
authMechanisms []string
|
||||
messages chan string
|
||||
authCommands chan string
|
||||
startTLSCommands chan string
|
||||
}
|
||||
|
||||
func newFakeSMTPServer(t *testing.T) *fakeSMTPServer {
|
||||
return newFakeSMTPServerWithSTARTTLSAdvertisement(t, true)
|
||||
}
|
||||
|
||||
func newFakeSMTPServerWithSTARTTLSAdvertisement(t *testing.T, advertiseSTARTTLS bool) *fakeSMTPServer {
|
||||
t.Helper()
|
||||
|
||||
cert, err := newTestTLSCertificate()
|
||||
require.NoError(t, err)
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
host, portText, err := net.SplitHostPort(listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
port, err := strconv.Atoi(portText)
|
||||
require.NoError(t, err)
|
||||
|
||||
server := &fakeSMTPServer{
|
||||
listener: listener,
|
||||
host: host,
|
||||
port: port,
|
||||
cert: cert,
|
||||
advertiseSTARTTLS: advertiseSTARTTLS,
|
||||
authMechanisms: []string{"PLAIN", "LOGIN"},
|
||||
messages: make(chan string, 1),
|
||||
authCommands: make(chan string, 1),
|
||||
startTLSCommands: make(chan string, 1),
|
||||
}
|
||||
go server.serve()
|
||||
return server
|
||||
}
|
||||
|
||||
func newFakeImplicitTLSSMTPServer(t *testing.T) *fakeSMTPServer {
|
||||
t.Helper()
|
||||
|
||||
cert, err := newTestTLSCertificate()
|
||||
require.NoError(t, err)
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
host, portText, err := net.SplitHostPort(listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
port, err := strconv.Atoi(portText)
|
||||
require.NoError(t, err)
|
||||
|
||||
server := &fakeSMTPServer{
|
||||
listener: tls.NewListener(listener, &tls.Config{Certificates: []tls.Certificate{cert}}),
|
||||
host: host,
|
||||
port: port,
|
||||
cert: cert,
|
||||
advertiseSTARTTLS: false,
|
||||
authMechanisms: []string{"PLAIN", "LOGIN"},
|
||||
messages: make(chan string, 1),
|
||||
authCommands: make(chan string, 1),
|
||||
startTLSCommands: make(chan string, 1),
|
||||
}
|
||||
go server.serve()
|
||||
return server
|
||||
}
|
||||
|
||||
func (s *fakeSMTPServer) close() {
|
||||
_ = s.listener.Close()
|
||||
}
|
||||
|
||||
func (s *fakeSMTPServer) serve() {
|
||||
conn, err := s.listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
_ = conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
|
||||
rw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
|
||||
if err := writeSMTPLine(rw, "220 fake.smtp.local ESMTP"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
encrypted := false
|
||||
for {
|
||||
line, err := rw.ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
command := strings.TrimRight(line, "\r\n")
|
||||
upperCommand := strings.ToUpper(command)
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(upperCommand, "EHLO"):
|
||||
if err := writeSMTPLine(rw, "250-fake.smtp.local"); err != nil {
|
||||
return
|
||||
}
|
||||
if !encrypted && s.advertiseSTARTTLS {
|
||||
if err := writeSMTPLine(rw, "250-STARTTLS"); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(s.authMechanisms) > 0 {
|
||||
if err := writeSMTPLine(rw, "250 AUTH "+strings.Join(s.authMechanisms, " ")); err != nil {
|
||||
return
|
||||
}
|
||||
} else if err := writeSMTPLine(rw, "250 8BITMIME"); err != nil {
|
||||
return
|
||||
}
|
||||
case upperCommand == "STARTTLS":
|
||||
if encrypted || !s.advertiseSTARTTLS {
|
||||
if err := writeSMTPLine(rw, "502 5.5.1 STARTTLS not supported"); err != nil {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case s.startTLSCommands <- command:
|
||||
default:
|
||||
}
|
||||
if err := writeSMTPLine(rw, "220 2.0.0 Ready to start TLS"); err != nil {
|
||||
return
|
||||
}
|
||||
tlsConn := tls.Server(conn, &tls.Config{Certificates: []tls.Certificate{s.cert}})
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
return
|
||||
}
|
||||
conn = tlsConn
|
||||
rw = bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
|
||||
encrypted = true
|
||||
case strings.HasPrefix(upperCommand, "AUTH"):
|
||||
select {
|
||||
case s.authCommands <- command:
|
||||
default:
|
||||
}
|
||||
if err := writeSMTPLine(rw, "235 2.7.0 Authentication successful"); err != nil {
|
||||
return
|
||||
}
|
||||
case strings.HasPrefix(upperCommand, "MAIL FROM:"):
|
||||
if err := writeSMTPLine(rw, "250 2.1.0 Sender OK"); err != nil {
|
||||
return
|
||||
}
|
||||
case strings.HasPrefix(upperCommand, "RCPT TO:"):
|
||||
if err := writeSMTPLine(rw, "250 2.1.5 Recipient OK"); err != nil {
|
||||
return
|
||||
}
|
||||
case upperCommand == "DATA":
|
||||
if err := writeSMTPLine(rw, "354 End data with <CR><LF>.<CR><LF>"); err != nil {
|
||||
return
|
||||
}
|
||||
var data strings.Builder
|
||||
for {
|
||||
dataLine, err := rw.ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if strings.TrimRight(dataLine, "\r\n") == "." {
|
||||
break
|
||||
}
|
||||
data.WriteString(dataLine)
|
||||
}
|
||||
s.messages <- data.String()
|
||||
if err := writeSMTPLine(rw, "250 2.0.0 Queued"); err != nil {
|
||||
return
|
||||
}
|
||||
case upperCommand == "QUIT":
|
||||
_ = writeSMTPLine(rw, "221 2.0.0 Bye")
|
||||
return
|
||||
default:
|
||||
if err := writeSMTPLine(rw, "502 5.5.1 Command not implemented"); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeSMTPLine(rw *bufio.ReadWriter, line string) error {
|
||||
_, err := rw.WriteString(line + "\r\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return rw.Flush()
|
||||
}
|
||||
|
||||
func newTestTLSCertificate() (tls.Certificate, error) {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
CommonName: "aixinexchange01.aixin-chip.com",
|
||||
},
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
DNSNames: []string{"aixinexchange01", "aixinexchange01.aixin-chip.com"},
|
||||
}
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
|
||||
return tls.X509KeyPair(certPEM, keyPEM)
|
||||
}
|
||||
|
||||
func withSMTPSettings(t *testing.T) {
|
||||
t.Helper()
|
||||
originalSMTPServer := SMTPServer
|
||||
originalSMTPPort := SMTPPort
|
||||
originalSMTPSSLEnabled := SMTPSSLEnabled
|
||||
originalSMTPStartTLSEnabled := SMTPStartTLSEnabled
|
||||
originalSMTPInsecureSkipVerify := SMTPInsecureSkipVerify
|
||||
originalSMTPForceAuthLogin := SMTPForceAuthLogin
|
||||
originalSMTPAccount := SMTPAccount
|
||||
originalSMTPFrom := SMTPFrom
|
||||
originalSMTPToken := SMTPToken
|
||||
originalSystemName := SystemName
|
||||
|
||||
t.Cleanup(func() {
|
||||
SMTPServer = originalSMTPServer
|
||||
SMTPPort = originalSMTPPort
|
||||
SMTPSSLEnabled = originalSMTPSSLEnabled
|
||||
SMTPStartTLSEnabled = originalSMTPStartTLSEnabled
|
||||
SMTPInsecureSkipVerify = originalSMTPInsecureSkipVerify
|
||||
SMTPForceAuthLogin = originalSMTPForceAuthLogin
|
||||
SMTPAccount = originalSMTPAccount
|
||||
SMTPFrom = originalSMTPFrom
|
||||
SMTPToken = originalSMTPToken
|
||||
SystemName = originalSystemName
|
||||
})
|
||||
}
|
||||
|
||||
func TestSendEmailUsesExplicitStartTLSWithInsecureCertificate(t *testing.T) {
|
||||
server := newFakeSMTPServer(t)
|
||||
defer server.close()
|
||||
withSMTPSettings(t)
|
||||
|
||||
SMTPServer = server.host
|
||||
SMTPPort = server.port
|
||||
SMTPSSLEnabled = false
|
||||
SMTPStartTLSEnabled = true
|
||||
SMTPInsecureSkipVerify = true
|
||||
SMTPForceAuthLogin = false
|
||||
SMTPAccount = "sender@example.com"
|
||||
SMTPFrom = "sender@example.com"
|
||||
SMTPToken = "secret"
|
||||
SystemName = "New API"
|
||||
|
||||
err := SendEmail("Verification", "receiver@example.com", "<p>123456</p>")
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case message := <-server.messages:
|
||||
require.Contains(t, message, "Subject: =?UTF-8?B?")
|
||||
require.Contains(t, message, "<p>123456</p>")
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for SMTP DATA")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendEmailExplicitStartTLSRequiresServerSupport(t *testing.T) {
|
||||
server := newFakeSMTPServerWithSTARTTLSAdvertisement(t, false)
|
||||
defer server.close()
|
||||
withSMTPSettings(t)
|
||||
|
||||
SMTPServer = server.host
|
||||
SMTPPort = server.port
|
||||
SMTPSSLEnabled = false
|
||||
SMTPStartTLSEnabled = true
|
||||
SMTPInsecureSkipVerify = true
|
||||
SMTPForceAuthLogin = false
|
||||
SMTPAccount = "sender@example.com"
|
||||
SMTPFrom = "sender@example.com"
|
||||
SMTPToken = "secret"
|
||||
SystemName = "New API"
|
||||
|
||||
err := SendEmail("Verification", "receiver@example.com", "<p>123456</p>")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "STARTTLS")
|
||||
}
|
||||
|
||||
func TestSendEmailDoesNotAutoUpgradeWhenStartTLSDisabled(t *testing.T) {
|
||||
server := newFakeSMTPServerWithSTARTTLSAdvertisement(t, true)
|
||||
defer server.close()
|
||||
withSMTPSettings(t)
|
||||
|
||||
SMTPServer = server.host
|
||||
SMTPPort = server.port
|
||||
SMTPSSLEnabled = false
|
||||
SMTPStartTLSEnabled = false
|
||||
SMTPInsecureSkipVerify = false
|
||||
SMTPForceAuthLogin = false
|
||||
SMTPAccount = "sender@example.com"
|
||||
SMTPFrom = "sender@example.com"
|
||||
SMTPToken = "secret"
|
||||
SystemName = "New API"
|
||||
|
||||
err := SendEmail("Verification", "receiver@example.com", "<p>123456</p>")
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case command := <-server.startTLSCommands:
|
||||
t.Fatalf("unexpected SMTP STARTTLS command: %s", command)
|
||||
default:
|
||||
}
|
||||
|
||||
select {
|
||||
case message := <-server.messages:
|
||||
require.Contains(t, message, "<p>123456</p>")
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for SMTP DATA")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSMTPClientHonorsExplicitStartTLSWhenPortIs465(t *testing.T) {
|
||||
server := newFakeSMTPServer(t)
|
||||
defer server.close()
|
||||
withSMTPSettings(t)
|
||||
|
||||
SMTPServer = server.host
|
||||
SMTPPort = 465
|
||||
SMTPSSLEnabled = false
|
||||
SMTPStartTLSEnabled = true
|
||||
SMTPInsecureSkipVerify = true
|
||||
|
||||
client, err := newSMTPClient(fmt.Sprintf("%s:%d", server.host, server.port))
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
select {
|
||||
case command := <-server.startTLSCommands:
|
||||
require.Equal(t, "STARTTLS", command)
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for SMTP STARTTLS")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSMTPClientKeepsImplicitTLSForLegacyPort465(t *testing.T) {
|
||||
server := newFakeImplicitTLSSMTPServer(t)
|
||||
defer server.close()
|
||||
withSMTPSettings(t)
|
||||
|
||||
SMTPServer = server.host
|
||||
SMTPPort = 465
|
||||
SMTPSSLEnabled = false
|
||||
SMTPStartTLSEnabled = false
|
||||
SMTPInsecureSkipVerify = true
|
||||
|
||||
client, err := newSMTPClient(fmt.Sprintf("%s:%d", server.host, server.port))
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
}
|
||||
|
||||
func TestSendEmailSkipsAuthWhenCredentialsAreEmpty(t *testing.T) {
|
||||
server := newFakeSMTPServerWithSTARTTLSAdvertisement(t, false)
|
||||
defer server.close()
|
||||
withSMTPSettings(t)
|
||||
|
||||
SMTPServer = server.host
|
||||
SMTPPort = server.port
|
||||
SMTPSSLEnabled = false
|
||||
SMTPStartTLSEnabled = false
|
||||
SMTPInsecureSkipVerify = false
|
||||
SMTPForceAuthLogin = false
|
||||
SMTPAccount = ""
|
||||
SMTPFrom = "sender@example.com"
|
||||
SMTPToken = ""
|
||||
SystemName = "New API"
|
||||
|
||||
err := SendEmail("Verification", "receiver@example.com", "<p>123456</p>")
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case command := <-server.authCommands:
|
||||
t.Fatalf("unexpected SMTP auth command: %s", command)
|
||||
default:
|
||||
}
|
||||
|
||||
select {
|
||||
case message := <-server.messages:
|
||||
require.Contains(t, message, "<p>123456</p>")
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for SMTP DATA")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendEmailSkipsAuthWhenCredentialsAreIncomplete(t *testing.T) {
|
||||
server := newFakeSMTPServerWithSTARTTLSAdvertisement(t, false)
|
||||
defer server.close()
|
||||
withSMTPSettings(t)
|
||||
|
||||
SMTPServer = server.host
|
||||
SMTPPort = server.port
|
||||
SMTPSSLEnabled = false
|
||||
SMTPStartTLSEnabled = false
|
||||
SMTPInsecureSkipVerify = false
|
||||
SMTPForceAuthLogin = false
|
||||
SMTPAccount = "sender@example.com"
|
||||
SMTPFrom = "sender@example.com"
|
||||
SMTPToken = ""
|
||||
SystemName = "New API"
|
||||
|
||||
err := SendEmail("Verification", "receiver@example.com", "<p>123456</p>")
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case command := <-server.authCommands:
|
||||
t.Fatalf("unexpected SMTP auth command: %s", command)
|
||||
default:
|
||||
}
|
||||
|
||||
select {
|
||||
case message := <-server.messages:
|
||||
require.Contains(t, message, "<p>123456</p>")
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for SMTP DATA")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendEmailUsesNTLMWhenServerOnlySupportsNTLM(t *testing.T) {
|
||||
server := newFakeSMTPServer(t)
|
||||
server.authMechanisms = []string{"NTLM"}
|
||||
defer server.close()
|
||||
withSMTPSettings(t)
|
||||
|
||||
SMTPServer = server.host
|
||||
SMTPPort = server.port
|
||||
SMTPSSLEnabled = false
|
||||
SMTPStartTLSEnabled = true
|
||||
SMTPInsecureSkipVerify = true
|
||||
SMTPForceAuthLogin = false
|
||||
SMTPAccount = "no-reply"
|
||||
SMTPFrom = "no-reply@example.com"
|
||||
SMTPToken = "secret"
|
||||
SystemName = "New API"
|
||||
|
||||
err := SendEmail("Verification", "receiver@example.com", "<p>123456</p>")
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case command := <-server.authCommands:
|
||||
require.True(t, strings.HasPrefix(command, "AUTH NTLM "), "unexpected auth command: %s", command)
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for SMTP AUTH")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendEmailUsesNTLMForMicrosoftAccountWhenServerOnlySupportsNTLM(t *testing.T) {
|
||||
server := newFakeSMTPServer(t)
|
||||
server.authMechanisms = []string{"NTLM"}
|
||||
defer server.close()
|
||||
withSMTPSettings(t)
|
||||
|
||||
SMTPServer = server.host
|
||||
SMTPPort = server.port
|
||||
SMTPSSLEnabled = false
|
||||
SMTPStartTLSEnabled = true
|
||||
SMTPInsecureSkipVerify = true
|
||||
SMTPForceAuthLogin = false
|
||||
SMTPAccount = "no-reply@contoso.onmicrosoft.com"
|
||||
SMTPFrom = "no-reply@contoso.onmicrosoft.com"
|
||||
SMTPToken = "secret"
|
||||
SystemName = "New API"
|
||||
|
||||
err := SendEmail("Verification", "receiver@example.com", "<p>123456</p>")
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case command := <-server.authCommands:
|
||||
require.True(t, strings.HasPrefix(command, "AUTH NTLM "), "unexpected auth command: %s", command)
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for SMTP AUTH")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendEmailExplicitStartTLSRejectsUntrustedCertificateByDefault(t *testing.T) {
|
||||
server := newFakeSMTPServer(t)
|
||||
defer server.close()
|
||||
withSMTPSettings(t)
|
||||
|
||||
SMTPServer = server.host
|
||||
SMTPPort = server.port
|
||||
SMTPSSLEnabled = false
|
||||
SMTPStartTLSEnabled = true
|
||||
SMTPInsecureSkipVerify = false
|
||||
SMTPForceAuthLogin = false
|
||||
SMTPAccount = "sender@example.com"
|
||||
SMTPFrom = "sender@example.com"
|
||||
SMTPToken = "secret"
|
||||
SystemName = "New API"
|
||||
|
||||
err := SendEmail("Verification", "receiver@example.com", "<p>123456</p>")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, fmt.Sprint(err), "certificate")
|
||||
}
|
||||
@@ -95,6 +95,8 @@ func InitEnv() {
|
||||
}
|
||||
}
|
||||
}
|
||||
SMTPStartTLSEnabled = GetEnvOrDefaultBool("SMTP_STARTTLS_ENABLE", GetEnvOrDefaultBool("SMTP_STARTTLS_ENABLED", false))
|
||||
SMTPInsecureSkipVerify = GetEnvOrDefaultBool("SMTP_INSECURE_SKIP_VERIFY", GetEnvOrDefaultBool("SMTP_TLS_INSECURE_SKIP_VERIFY", false))
|
||||
|
||||
// Parse requestInterval and set RequestInterval
|
||||
requestInterval, _ = strconv.Atoi(os.Getenv("POLLING_INTERVAL"))
|
||||
|
||||
Reference in New Issue
Block a user