Compare commits

..

1 commit

Author SHA1 Message Date
Michael Fuchs
c202e8d20c
Merge 128b2bd32e into e1db5bf66a 2024-10-11 17:22:52 +00:00
12 changed files with 51 additions and 264 deletions

View file

@ -54,7 +54,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
uses: github/codeql-action/init@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -65,7 +65,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
uses: github/codeql-action/autobuild@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -79,4 +79,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
uses: github/codeql-action/analyze@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12

View file

@ -75,6 +75,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
uses: github/codeql-action/upload-sarif@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
with:
sarif_file: results.sarif

10
auth.go
View file

@ -35,7 +35,7 @@ const (
// IETF draft. The IETF draft is more lax than the MS spec, therefore we follow the I-D, which
// automatically matches the MS spec.
//
// Since the "LOGIN" SASL authentication mechanism transmits the username and password in
// Since the "LOGIN" SASL authentication mechansim transmits the username and password in
// plaintext over the internet connection, we only allow this mechanism over a TLS secured
// connection.
//
@ -47,11 +47,11 @@ 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 = "NOAUTH"
SMTPAuthNoAuth SMTPAuthType = ""
// SMTPAuthPlain is the "PLAIN" authentication mechanism as described in RFC 4616.
//
// Since the "PLAIN" SASL authentication mechanism transmits the username and password in
// Since the "PLAIN" SASL authentication mechansim transmits the username and password in
// plaintext over the internet connection, we only allow this mechanism over a TLS secured
// connection.
//
@ -76,7 +76,7 @@ const (
//
// SCRAM-SHA-X-PLUS authentication require TLS channel bindings to protect against MitM attacks and
// to guarantee that the integrity of the transport layer is preserved throughout the authentication
// process. Therefore we only allow this mechanism over a TLS secured connection.
// process. Therefore we only allow this mechansim over a TLS secured connection.
//
// SCRAM-SHA-1-PLUS is still considered secure for certain applications, particularly when used as part
// of a challenge-response authentication mechanism (as we use it). However, it is generally
@ -95,7 +95,7 @@ const (
//
// SCRAM-SHA-X-PLUS authentication require TLS channel bindings to protect against MitM attacks and
// to guarantee that the integrity of the transport layer is preserved throughout the authentication
// process. Therefore we only allow this mechanism over a TLS secured connection.
// process. Therefore we only allow this mechansim over a TLS secured connection.
//
// https://datatracker.ietf.org/doc/html/rfc7677
SMTPAuthSCRAMSHA256PLUS SMTPAuthType = "SCRAM-SHA-256-PLUS"

View file

@ -145,9 +145,6 @@ 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
@ -259,12 +256,11 @@ 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,
tlsconfig: &tls.Config{ServerName: host, MinVersion: DefaultTLSMinVersion},
tlspolicy: DefaultTLSPolicy,
connTimeout: DefaultTimeout,
host: host,
port: DefaultPort,
tlsconfig: &tls.Config{ServerName: host, MinVersion: DefaultTLSMinVersion},
tlspolicy: DefaultTLSPolicy,
}
// Set default HELO/EHLO hostname
@ -368,10 +364,9 @@ 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. 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.
// 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.
//
// Returns:
// - An Option function that enables debug logging for the Client.
@ -676,22 +671,6 @@ 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.
@ -886,19 +865,6 @@ 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
@ -955,9 +921,6 @@ 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
}
@ -1065,16 +1028,9 @@ 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.
//
// 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.
//
// 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.
// Finally, it attempts to authenticate the client using the selected method.
//
// Returns:
@ -1084,8 +1040,7 @@ 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 != SMTPAuthNoAuth {
if c.smtpAuth == nil && c.smtpAuthType != SMTPAuthCustom {
hasSMTPAuth, smtpAuthType := c.smtpClient.Extension("AUTH")
if !hasSMTPAuth {
return fmt.Errorf("server does not support SMTP AUTH")

View file

@ -123,7 +123,6 @@ 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},
@ -579,23 +578,6 @@ 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 {
@ -1180,81 +1162,6 @@ 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 {

2
doc.go
View file

@ -11,4 +11,4 @@ package mail
// VERSION indicates the current version of the package. It is also attached to the default user
// agent string.
const VERSION = "0.5.1"
const VERSION = "0.5.0"

2
eml.go
View file

@ -383,7 +383,7 @@ ReadNextPart:
return fmt.Errorf("failed to get next part of multipart message: %w", err)
}
for err == nil {
// Multipart/related and Multipart/alternative parts need to be parsed separately
// Multipart/related and Multipart/alternative parts need to be parsed seperately
if contentTypeSlice, ok := multiPart.Header[HeaderContentType.String()]; ok && len(contentTypeSlice) == 1 {
contentType, _ := parseMultiPartHeader(contentTypeSlice[0])
if strings.EqualFold(contentType, TypeMultipartRelated.String()) ||

View file

@ -41,48 +41,42 @@ func NewJSON(output io.Writer, level Level) *JSONlog {
}
}
// logMessage is a helper function to handle different log levels and formats.
func logMessage(level Level, log *slog.Logger, logData Log, formatFunc func(string, ...interface{}) string) {
lGroup := log.WithGroup(DirString).With(
slog.String(DirFromString, logData.directionFrom()),
slog.String(DirToString, logData.directionTo()),
)
switch level {
case LevelDebug:
lGroup.Debug(formatFunc(logData.Format, logData.Messages...))
case LevelInfo:
lGroup.Info(formatFunc(logData.Format, logData.Messages...))
case LevelWarn:
lGroup.Warn(formatFunc(logData.Format, logData.Messages...))
case LevelError:
lGroup.Error(formatFunc(logData.Format, logData.Messages...))
}
}
// Debugf logs a debug message via the structured JSON logger
func (l *JSONlog) Debugf(log Log) {
if l.level >= LevelDebug {
logMessage(LevelDebug, l.log, log, fmt.Sprintf)
l.log.WithGroup(DirString).With(
slog.String(DirFromString, log.directionFrom()),
slog.String(DirToString, log.directionTo()),
).Debug(fmt.Sprintf(log.Format, log.Messages...))
}
}
// Infof logs a info message via the structured JSON logger
func (l *JSONlog) Infof(log Log) {
if l.level >= LevelInfo {
logMessage(LevelInfo, l.log, log, fmt.Sprintf)
l.log.WithGroup(DirString).With(
slog.String(DirFromString, log.directionFrom()),
slog.String(DirToString, log.directionTo()),
).Info(fmt.Sprintf(log.Format, log.Messages...))
}
}
// Warnf logs a warn message via the structured JSON logger
func (l *JSONlog) Warnf(log Log) {
if l.level >= LevelWarn {
logMessage(LevelWarn, l.log, log, fmt.Sprintf)
l.log.WithGroup(DirString).With(
slog.String(DirFromString, log.directionFrom()),
slog.String(DirToString, log.directionTo()),
).Warn(fmt.Sprintf(log.Format, log.Messages...))
}
}
// Errorf logs a warn message via the structured JSON logger
func (l *JSONlog) Errorf(log Log) {
if l.level >= LevelError {
logMessage(LevelError, l.log, log, fmt.Sprintf)
l.log.WithGroup(DirString).With(
slog.String(DirFromString, log.directionFrom()),
slog.String(DirToString, log.directionTo()),
).Error(fmt.Sprintf(log.Format, log.Messages...))
}
}

View file

@ -35,36 +35,34 @@ func New(output io.Writer, level Level) *Stdlog {
}
}
// logStdMessage is a helper function to handle different log levels and formats for Stdlog.
func logStdMessage(logger *log.Logger, logData Log, callDepth int) {
format := fmt.Sprintf("%s %s", logData.directionPrefix(), logData.Format)
_ = logger.Output(callDepth, fmt.Sprintf(format, logData.Messages...))
}
// Debugf performs a Printf() on the debug logger
func (l *Stdlog) Debugf(log Log) {
if l.level >= LevelDebug {
logStdMessage(l.debug, log, CallDepth)
format := fmt.Sprintf("%s %s", log.directionPrefix(), log.Format)
_ = l.debug.Output(CallDepth, fmt.Sprintf(format, log.Messages...))
}
}
// Infof performs a Printf() on the info logger
func (l *Stdlog) Infof(log Log) {
if l.level >= LevelInfo {
logStdMessage(l.info, log, CallDepth)
format := fmt.Sprintf("%s %s", log.directionPrefix(), log.Format)
_ = l.info.Output(CallDepth, fmt.Sprintf(format, log.Messages...))
}
}
// Warnf performs a Printf() on the warn logger
func (l *Stdlog) Warnf(log Log) {
if l.level >= LevelWarn {
logStdMessage(l.warn, log, CallDepth)
format := fmt.Sprintf("%s %s", log.directionPrefix(), log.Format)
_ = l.warn.Output(CallDepth, fmt.Sprintf(format, log.Messages...))
}
}
// Errorf performs a Printf() on the error logger
func (l *Stdlog) Errorf(log Log) {
if l.level >= LevelError {
logStdMessage(l.err, log, CallDepth)
format := fmt.Sprintf("%s %s", log.directionPrefix(), log.Format)
_ = l.err.Output(CallDepth, fmt.Sprintf(format, log.Messages...))
}
}

View file

@ -29,7 +29,7 @@ type loginAuth struct {
// See: https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
// Since there is no official standard RFC and we've seen different implementations
// of this mechanism (sending "Username:", "Username", "username", "User name", etc.)
// we follow the IETF-Draft and ignore any server challenge to allow compatibility
// we follow the IETF-Draft and ignore any server challange to allow compatiblity
// with most mail servers/providers.
//
// LoginAuth will only send the credentials if the connection is using TLS

View file

@ -54,9 +54,6 @@ 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
@ -81,9 +78,6 @@ 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
@ -180,15 +174,7 @@ func (c *Client) Hello(localName string) error {
func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
c.mutex.Lock()
var logMsg []interface{}
logMsg = args
logFmt := format
if c.authIsActive {
logMsg = []interface{}{"<SMTP auth data redacted>"}
logFmt = "%s"
}
c.debugLog(log.DirClientToServer, logFmt, logMsg...)
c.debugLog(log.DirClientToServer, format, args...)
id, err := c.Text.Cmd(format, args...)
if err != nil {
c.mutex.Unlock()
@ -196,13 +182,7 @@ func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, s
}
c.Text.StartResponse(id)
code, msg, err := c.Text.ReadResponse(expectCode)
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.debugLog(log.DirServerToClient, "%d %s", code, msg)
c.Text.EndResponse(id)
c.mutex.Unlock()
return code, msg, err
@ -276,20 +256,6 @@ 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 {
@ -590,13 +556,6 @@ 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

View file

@ -1111,32 +1111,6 @@ 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
@ -2163,7 +2137,7 @@ func SkipFlaky(t testing.TB, issue int) {
}
// testSCRAMSMTPServer represents a test server for SCRAM-based SMTP authentication.
// It does not do any acutal computation of the challenges but verifies that the expected
// It does not do any acutal computation of the challanges but verifies that the expected
// fields are present. We have actual real authentication tests for all SCRAM modes in the
// go-mail client_test.go
type testSCRAMSMTPServer struct {