mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-22 13:50:49 +01:00
Compare commits
8 commits
f311fa9904
...
b0f514ea8a
Author | SHA1 | Date | |
---|---|---|---|
b0f514ea8a | |||
bb2fd0f970 | |||
3234c13277 | |||
0944296cff | |||
55a5d02fe0 | |||
7acfe8015d | |||
c2d9104b45 | |||
021666d6ad |
5 changed files with 220 additions and 15 deletions
2
auth.go
2
auth.go
|
@ -47,7 +47,7 @@ const (
|
|||
// SMTPAuthNoAuth is equivalent to performing no authentication at all. It is a convenience
|
||||
// option and should not be used. Instead, for mail servers that do no support/require
|
||||
// authentication, the Client should not be passed the WithSMTPAuth option at all.
|
||||
SMTPAuthNoAuth SMTPAuthType = ""
|
||||
SMTPAuthNoAuth SMTPAuthType = "NOAUTH"
|
||||
|
||||
// SMTPAuthPlain is the "PLAIN" authentication mechanism as described in RFC 4616.
|
||||
//
|
||||
|
|
59
client.go
59
client.go
|
@ -145,6 +145,9 @@ type (
|
|||
// isEncrypted indicates wether the Client connection is encrypted or not.
|
||||
isEncrypted bool
|
||||
|
||||
// logAuthData indicates whether authentication-related data should be logged.
|
||||
logAuthData bool
|
||||
|
||||
// logger is a logger that satisfies the log.Logger interface.
|
||||
logger log.Logger
|
||||
|
||||
|
@ -256,6 +259,7 @@ var (
|
|||
// - An error if any critical default values are missing or options fail to apply.
|
||||
func NewClient(host string, opts ...Option) (*Client, error) {
|
||||
c := &Client{
|
||||
smtpAuthType: SMTPAuthNoAuth,
|
||||
connTimeout: DefaultTimeout,
|
||||
host: host,
|
||||
port: DefaultPort,
|
||||
|
@ -364,9 +368,10 @@ func WithSSLPort(fallback bool) Option {
|
|||
// WithDebugLog enables debug logging for the Client.
|
||||
//
|
||||
// This function activates debug logging, which logs incoming and outgoing communication between the
|
||||
// Client and the SMTP server to os.Stderr. Be cautious when using this option, as the logs may include
|
||||
// unencrypted authentication data, depending on the SMTP authentication method in use, which could
|
||||
// pose a data protection risk.
|
||||
// Client and the SMTP server to os.Stderr. By default the debug logging will redact any kind of SMTP
|
||||
// authentication data. If you need access to the actual authentication data in your logs, you can
|
||||
// enable authentication data logging with the WithLogAuthData option or by setting it with the
|
||||
// Client.SetLogAuthData method.
|
||||
//
|
||||
// Returns:
|
||||
// - An Option function that enables debug logging for the Client.
|
||||
|
@ -671,6 +676,22 @@ func WithDialContextFunc(dialCtxFunc DialContextFunc) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithLogAuthData enables logging of authentication data.
|
||||
//
|
||||
// This function sets the logAuthData field of the Client to true, enabling the logging of authentication data.
|
||||
//
|
||||
// Be cautious when using this option, as the logs may include unencrypted authentication data, depending on
|
||||
// the SMTP authentication method in use, which could pose a data protection risk.
|
||||
//
|
||||
// Returns:
|
||||
// - An Option function that configures the Client to enable authentication data logging.
|
||||
func WithLogAuthData() Option {
|
||||
return func(c *Client) error {
|
||||
c.logAuthData = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// TLSPolicy returns the TLSPolicy that is currently set on the Client as a string.
|
||||
//
|
||||
// This method retrieves the current TLSPolicy configured for the Client and returns it as a string representation.
|
||||
|
@ -865,6 +886,19 @@ func (c *Client) SetSMTPAuthCustom(smtpAuth smtp.Auth) {
|
|||
c.smtpAuthType = SMTPAuthCustom
|
||||
}
|
||||
|
||||
// SetLogAuthData sets or overrides the logging of SMTP authentication data for the Client.
|
||||
//
|
||||
// This function sets the logAuthData field of the Client to true, enabling the logging of authentication data.
|
||||
//
|
||||
// Be cautious when using this option, as the logs may include unencrypted authentication data, depending on
|
||||
// the SMTP authentication method in use, which could pose a data protection risk.
|
||||
//
|
||||
// Parameters:
|
||||
// - logAuth: Set wether or not to log SMTP authentication data for the Client.
|
||||
func (c *Client) SetLogAuthData(logAuth bool) {
|
||||
c.logAuthData = logAuth
|
||||
}
|
||||
|
||||
// DialWithContext establishes a connection to the server using the provided context.Context.
|
||||
//
|
||||
// This function adds a deadline based on the Client's timeout to the provided context.Context
|
||||
|
@ -921,6 +955,9 @@ func (c *Client) DialWithContext(dialCtx context.Context) error {
|
|||
if c.useDebugLog {
|
||||
c.smtpClient.SetDebugLog(true)
|
||||
}
|
||||
if c.logAuthData {
|
||||
c.smtpClient.SetLogAuthData()
|
||||
}
|
||||
if err = c.smtpClient.Hello(c.helo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1028,9 +1065,16 @@ func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) e
|
|||
// determines the supported authentication methods, and applies the appropriate authentication
|
||||
// type. An error is returned if authentication fails.
|
||||
//
|
||||
// This method first verifies the connection to the SMTP server. If no custom authentication
|
||||
// mechanism is provided, it checks which authentication methods are supported by the server.
|
||||
// Based on the configured SMTPAuthType, it sets up the appropriate authentication mechanism.
|
||||
// By default NewClient sets the SMTP authentication type to SMTPAuthNoAuth, meaning, that no
|
||||
// SMTP authentication will be performed. If the user makes use of SetSMTPAuth or initialzes the
|
||||
// client with WithSMTPAuth, the SMTP authentication type will be set in the Client, forcing
|
||||
// this method to determine if the server supports the selected authentication method and
|
||||
// assigning the corresponding smtp.Auth function to it.
|
||||
//
|
||||
// If the user set a custom SMTP authentication function using SetSMTPAuthCustom or
|
||||
// WithSMTPAuthCustom, we will not perform any detection and assignment logic and will trust
|
||||
// the user with their provided smtp.Auth function.
|
||||
//
|
||||
// Finally, it attempts to authenticate the client using the selected method.
|
||||
//
|
||||
// Returns:
|
||||
|
@ -1040,7 +1084,8 @@ func (c *Client) auth() error {
|
|||
if err := c.checkConn(); err != nil {
|
||||
return fmt.Errorf("failed to authenticate: %w", err)
|
||||
}
|
||||
if c.smtpAuth == nil && c.smtpAuthType != SMTPAuthCustom {
|
||||
|
||||
if c.smtpAuth == nil && c.smtpAuthType != SMTPAuthNoAuth {
|
||||
hasSMTPAuth, smtpAuthType := c.smtpClient.Extension("AUTH")
|
||||
if !hasSMTPAuth {
|
||||
return fmt.Errorf("server does not support SMTP AUTH")
|
||||
|
|
|
@ -123,6 +123,7 @@ func TestNewClientWithOptions(t *testing.T) {
|
|||
{"WithoutNoop()", WithoutNoop(), false},
|
||||
{"WithDebugLog()", WithDebugLog(), false},
|
||||
{"WithLogger()", WithLogger(log.New(os.Stderr, log.LevelDebug)), false},
|
||||
{"WithLogger()", WithLogAuthData(), false},
|
||||
{"WithDialContextFunc()", WithDialContextFunc(func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return nil, nil
|
||||
}), false},
|
||||
|
@ -578,6 +579,23 @@ func TestWithoutNoop(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClient_SetLogAuthData(t *testing.T) {
|
||||
c, err := NewClient(DefaultHost, WithLogAuthData())
|
||||
if err != nil {
|
||||
t.Errorf("failed to create new client: %s", err)
|
||||
return
|
||||
}
|
||||
if !c.logAuthData {
|
||||
t.Errorf("WithLogAuthData failed. c.logAuthData expected to be: %t, got: %t", true,
|
||||
c.logAuthData)
|
||||
}
|
||||
c.SetLogAuthData(false)
|
||||
if c.logAuthData {
|
||||
t.Errorf("SetLogAuthData failed. c.logAuthData expected to be: %t, got: %t", false,
|
||||
c.logAuthData)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetSMTPAuthCustom tests the SetSMTPAuthCustom method for the Client object
|
||||
func TestSetSMTPAuthCustom(t *testing.T) {
|
||||
tests := []struct {
|
||||
|
@ -1162,6 +1180,81 @@ func TestClient_Send_withBrokenRecipient(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClient_DialWithContext_switchAuth(t *testing.T) {
|
||||
if os.Getenv("TEST_ALLOW_SEND") == "" {
|
||||
t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test")
|
||||
}
|
||||
|
||||
// We start with no auth explicitly set
|
||||
client, err := NewClient(
|
||||
os.Getenv("TEST_HOST"),
|
||||
WithTLSPortPolicy(TLSMandatory),
|
||||
)
|
||||
defer func() {
|
||||
_ = client.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
t.Errorf("failed to create client: %s", err)
|
||||
return
|
||||
}
|
||||
if err = client.DialWithContext(context.Background()); err != nil {
|
||||
t.Errorf("failed to dial to sending server: %s", err)
|
||||
}
|
||||
if err = client.Close(); err != nil {
|
||||
t.Errorf("failed to close client connection: %s", err)
|
||||
}
|
||||
|
||||
// We switch to LOGIN auth, which the server supports
|
||||
client.SetSMTPAuth(SMTPAuthLogin)
|
||||
client.SetUsername(os.Getenv("TEST_SMTPAUTH_USER"))
|
||||
client.SetPassword(os.Getenv("TEST_SMTPAUTH_PASS"))
|
||||
if err = client.DialWithContext(context.Background()); err != nil {
|
||||
t.Errorf("failed to dial to sending server: %s", err)
|
||||
}
|
||||
if err = client.Close(); err != nil {
|
||||
t.Errorf("failed to close client connection: %s", err)
|
||||
}
|
||||
|
||||
// We switch to CRAM-MD5, which the server does not support - error expected
|
||||
client.SetSMTPAuth(SMTPAuthCramMD5)
|
||||
if err = client.DialWithContext(context.Background()); err == nil {
|
||||
t.Errorf("expected error when dialing with unsupported auth mechanism, got nil")
|
||||
return
|
||||
}
|
||||
if !errors.Is(err, ErrCramMD5AuthNotSupported) {
|
||||
t.Errorf("expected dial error: %s, but got: %s", ErrCramMD5AuthNotSupported, err)
|
||||
}
|
||||
|
||||
// We switch to CUSTOM by providing PLAIN auth as function - the server supports this
|
||||
client.SetSMTPAuthCustom(smtp.PlainAuth("", os.Getenv("TEST_SMTPAUTH_USER"),
|
||||
os.Getenv("TEST_SMTPAUTH_PASS"), os.Getenv("TEST_HOST")))
|
||||
if client.smtpAuthType != SMTPAuthCustom {
|
||||
t.Errorf("expected auth type to be Custom, got: %s", client.smtpAuthType)
|
||||
}
|
||||
if err = client.DialWithContext(context.Background()); err != nil {
|
||||
t.Errorf("failed to dial to sending server: %s", err)
|
||||
}
|
||||
if err = client.Close(); err != nil {
|
||||
t.Errorf("failed to close client connection: %s", err)
|
||||
}
|
||||
|
||||
// We switch back to explicit no authenticaiton
|
||||
client.SetSMTPAuth(SMTPAuthNoAuth)
|
||||
if err = client.DialWithContext(context.Background()); err != nil {
|
||||
t.Errorf("failed to dial to sending server: %s", err)
|
||||
}
|
||||
if err = client.Close(); err != nil {
|
||||
t.Errorf("failed to close client connection: %s", err)
|
||||
}
|
||||
|
||||
// Finally we set an empty string as SMTPAuthType and expect and error. This way we can
|
||||
// verify that we do not accidentaly skip authentication with an empty string SMTPAuthType
|
||||
client.SetSMTPAuth("")
|
||||
if err = client.DialWithContext(context.Background()); err == nil {
|
||||
t.Errorf("expected error when dialing with empty auth mechanism, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestClient_auth tests the Dial(), Send() and Close() method of Client with broken settings
|
||||
func TestClient_auth(t *testing.T) {
|
||||
tests := []struct {
|
||||
|
|
45
smtp/smtp.go
45
smtp/smtp.go
|
@ -54,6 +54,9 @@ type Client struct {
|
|||
// auth supported auth mechanisms
|
||||
auth []string
|
||||
|
||||
// authIsActive indicates that the Client is currently during SMTP authentication
|
||||
authIsActive bool
|
||||
|
||||
// keep a reference to the connection so it can be used to create a TLS connection later
|
||||
conn net.Conn
|
||||
|
||||
|
@ -78,6 +81,9 @@ type Client struct {
|
|||
// isConnected indicates if the Client has an active connection
|
||||
isConnected bool
|
||||
|
||||
// logAuthData indicates if the Client should include SMTP authentication data in the logs
|
||||
logAuthData bool
|
||||
|
||||
// localName is the name to use in HELO/EHLO
|
||||
localName string // the name to use in HELO/EHLO
|
||||
|
||||
|
@ -174,7 +180,15 @@ func (c *Client) Hello(localName string) error {
|
|||
func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
|
||||
c.mutex.Lock()
|
||||
|
||||
c.debugLog(log.DirClientToServer, format, args...)
|
||||
var logMsg []interface{}
|
||||
logMsg = args
|
||||
logFmt := format
|
||||
if c.authIsActive {
|
||||
logMsg = []interface{}{"<SMTP auth data redacted>"}
|
||||
logFmt = "%s"
|
||||
}
|
||||
c.debugLog(log.DirClientToServer, logFmt, logMsg...)
|
||||
|
||||
id, err := c.Text.Cmd(format, args...)
|
||||
if err != nil {
|
||||
c.mutex.Unlock()
|
||||
|
@ -182,7 +196,13 @@ func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, s
|
|||
}
|
||||
c.Text.StartResponse(id)
|
||||
code, msg, err := c.Text.ReadResponse(expectCode)
|
||||
c.debugLog(log.DirServerToClient, "%d %s", code, msg)
|
||||
|
||||
logMsg = []interface{}{code, msg}
|
||||
if c.authIsActive && code >= 300 && code <= 400 {
|
||||
logMsg = []interface{}{code, "<SMTP auth data redacted>"}
|
||||
}
|
||||
c.debugLog(log.DirServerToClient, "%d %s", logMsg...)
|
||||
|
||||
c.Text.EndResponse(id)
|
||||
c.mutex.Unlock()
|
||||
return code, msg, err
|
||||
|
@ -256,6 +276,20 @@ func (c *Client) Auth(a Auth) error {
|
|||
if err := c.hello(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
if !c.logAuthData {
|
||||
c.authIsActive = true
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
defer func() {
|
||||
c.mutex.Lock()
|
||||
if !c.logAuthData {
|
||||
c.authIsActive = false
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
}()
|
||||
|
||||
encoding := base64.StdEncoding
|
||||
mech, resp, err := a.Start(&ServerInfo{c.serverName, c.tls, c.auth})
|
||||
if err != nil {
|
||||
|
@ -556,6 +590,13 @@ func (c *Client) SetLogger(l log.Logger) {
|
|||
c.logger = l
|
||||
}
|
||||
|
||||
// SetLogAuthData enables logging of authentication data in the Client.
|
||||
func (c *Client) SetLogAuthData() {
|
||||
c.mutex.Lock()
|
||||
c.logAuthData = true
|
||||
c.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetDSNMailReturnOption sets the DSN mail return option for the Mail method
|
||||
func (c *Client) SetDSNMailReturnOption(d string) {
|
||||
c.dsnmrtype = d
|
||||
|
|
|
@ -1111,6 +1111,32 @@ func TestClient_SetLogger(t *testing.T) {
|
|||
c.logger.Debugf(log.Log{Direction: log.DirServerToClient, Format: "%s", Messages: []interface{}{"test"}})
|
||||
}
|
||||
|
||||
func TestClient_SetLogAuthData(t *testing.T) {
|
||||
server := strings.Join(strings.Split(newClientServer, "\n"), "\r\n")
|
||||
|
||||
var cmdbuf strings.Builder
|
||||
bcmdbuf := bufio.NewWriter(&cmdbuf)
|
||||
out := func() string {
|
||||
if err := bcmdbuf.Flush(); err != nil {
|
||||
t.Errorf("failed to flush: %s", err)
|
||||
}
|
||||
return cmdbuf.String()
|
||||
}
|
||||
var fake faker
|
||||
fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
|
||||
c, err := NewClient(fake, "fake.host")
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient: %v\n(after %v)", err, out())
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
}()
|
||||
c.SetLogAuthData()
|
||||
if !c.logAuthData {
|
||||
t.Error("Expected logAuthData to be true but received false")
|
||||
}
|
||||
}
|
||||
|
||||
var newClientServer = `220 hello world
|
||||
250-mx.google.com at your service
|
||||
250-SIZE 35651584
|
||||
|
|
Loading…
Reference in a new issue