Compare commits

...

11 commits

Author SHA1 Message Date
c903f6e1b4
Merge pull request #340 from wneessen/bug/339_fix-spelling-errors
Fix spelling errors
2024-10-16 10:41:41 +02:00
a638090d0e
Fix typo in multipart comment
Corrected the spelling of "seperately" to "separately" in a comment explaining the parsing of multipart/related and multipart/alternative parts.
2024-10-16 10:38:40 +02:00
fb14e1e7dd
Fix typos in auth mechanism comments
Corrected multiple instances of "mechansim" to "mechanism" in the comments describing SASL authentication methods to improve readability and maintain code quality.
2024-10-16 10:38:13 +02:00
f120485c98
Correct typo in comment
Fix a typo in smtp_test.go's comment from "challanges" to "challenges" to improve readability and accuracy of documentation. This change does not affect the code's functionality.
2024-10-16 10:37:13 +02:00
569e8fbc70
Fix typos in comments for better readability
Corrected spelling errors in comments for "challenge" and "compatibility" to improve clarity. This ensures better understanding and adherence to the documented IETF draft standard.
2024-10-16 10:35:29 +02:00
8ea80c0739
Update doc.go
Bump version to v0.5.1 for release
2024-10-16 09:50:22 +02:00
9ae7681651
Merge pull request #336 from sarff/log-opt
code duplication reduction for jsonlog.go and stdlog.go
2024-10-16 09:49:53 +02:00
e854b2192f
Merge pull request #335 from wneessen/bug/332_server-does-not-support-smtp-auth-error-when-using-localhost-in-v050
Add default SMTP authentication type to NewClient
2024-10-16 09:26:51 +02:00
dmit
7b297d79b8 code duplication reduction for jsonlog.go and stdlog.go 2024-10-11 13:56:11 +03:00
c2d9104b45
Update environment variables for SMTP authentication
Renamed environment variables from TEST_USER and TEST_PASS to TEST_SMTPAUTH_USER and TEST_SMTPAUTH_PASS for clarity and consistency in setting SMTP authentication credentials. This change ensures that the correct credentials are applied during tests.
2024-10-11 11:55:58 +02:00
021666d6ad
#332: Add default SMTP authentication type to NewClient
This commit fixes a regression introduced in v0.5.0. We now set the default SMTPAuthType to NOAUTH in NewClient. Enhanced documentation and added test cases for different SMTP authentication scenarios.
2024-10-11 11:21:56 +02:00
9 changed files with 134 additions and 42 deletions

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

View file

@ -259,11 +259,12 @@ var (
// - An error if any critical default values are missing or options fail to apply. // - An error if any critical default values are missing or options fail to apply.
func NewClient(host string, opts ...Option) (*Client, error) { func NewClient(host string, opts ...Option) (*Client, error) {
c := &Client{ c := &Client{
connTimeout: DefaultTimeout, smtpAuthType: SMTPAuthNoAuth,
host: host, connTimeout: DefaultTimeout,
port: DefaultPort, host: host,
tlsconfig: &tls.Config{ServerName: host, MinVersion: DefaultTLSMinVersion}, port: DefaultPort,
tlspolicy: DefaultTLSPolicy, tlsconfig: &tls.Config{ServerName: host, MinVersion: DefaultTLSMinVersion},
tlspolicy: DefaultTLSPolicy,
} }
// Set default HELO/EHLO hostname // Set default HELO/EHLO hostname
@ -1064,9 +1065,16 @@ func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) e
// determines the supported authentication methods, and applies the appropriate authentication // determines the supported authentication methods, and applies the appropriate authentication
// type. An error is returned if authentication fails. // type. An error is returned if authentication fails.
// //
// This method first verifies the connection to the SMTP server. If no custom authentication // By default NewClient sets the SMTP authentication type to SMTPAuthNoAuth, meaning, that no
// mechanism is provided, it checks which authentication methods are supported by the server. // SMTP authentication will be performed. If the user makes use of SetSMTPAuth or initialzes the
// Based on the configured SMTPAuthType, it sets up the appropriate authentication mechanism. // 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. // Finally, it attempts to authenticate the client using the selected method.
// //
// Returns: // Returns:
@ -1076,7 +1084,8 @@ func (c *Client) auth() error {
if err := c.checkConn(); err != nil { if err := c.checkConn(); err != nil {
return fmt.Errorf("failed to authenticate: %w", err) 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") hasSMTPAuth, smtpAuthType := c.smtpClient.Extension("AUTH")
if !hasSMTPAuth { if !hasSMTPAuth {
return fmt.Errorf("server does not support SMTP AUTH") return fmt.Errorf("server does not support SMTP AUTH")

View file

@ -1180,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 // TestClient_auth tests the Dial(), Send() and Close() method of Client with broken settings
func TestClient_auth(t *testing.T) { func TestClient_auth(t *testing.T) {
tests := []struct { 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 // VERSION indicates the current version of the package. It is also attached to the default user
// agent string. // agent string.
const VERSION = "0.5.0" const VERSION = "0.5.1"

2
eml.go
View file

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

View file

@ -41,42 +41,48 @@ 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 // Debugf logs a debug message via the structured JSON logger
func (l *JSONlog) Debugf(log Log) { func (l *JSONlog) Debugf(log Log) {
if l.level >= LevelDebug { if l.level >= LevelDebug {
l.log.WithGroup(DirString).With( logMessage(LevelDebug, l.log, log, fmt.Sprintf)
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 // Infof logs a info message via the structured JSON logger
func (l *JSONlog) Infof(log Log) { func (l *JSONlog) Infof(log Log) {
if l.level >= LevelInfo { if l.level >= LevelInfo {
l.log.WithGroup(DirString).With( logMessage(LevelInfo, l.log, log, fmt.Sprintf)
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 // Warnf logs a warn message via the structured JSON logger
func (l *JSONlog) Warnf(log Log) { func (l *JSONlog) Warnf(log Log) {
if l.level >= LevelWarn { if l.level >= LevelWarn {
l.log.WithGroup(DirString).With( logMessage(LevelWarn, l.log, log, fmt.Sprintf)
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 // Errorf logs a warn message via the structured JSON logger
func (l *JSONlog) Errorf(log Log) { func (l *JSONlog) Errorf(log Log) {
if l.level >= LevelError { if l.level >= LevelError {
l.log.WithGroup(DirString).With( logMessage(LevelError, l.log, log, fmt.Sprintf)
slog.String(DirFromString, log.directionFrom()),
slog.String(DirToString, log.directionTo()),
).Error(fmt.Sprintf(log.Format, log.Messages...))
} }
} }

View file

@ -35,34 +35,36 @@ 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 // Debugf performs a Printf() on the debug logger
func (l *Stdlog) Debugf(log Log) { func (l *Stdlog) Debugf(log Log) {
if l.level >= LevelDebug { if l.level >= LevelDebug {
format := fmt.Sprintf("%s %s", log.directionPrefix(), log.Format) logStdMessage(l.debug, log, CallDepth)
_ = l.debug.Output(CallDepth, fmt.Sprintf(format, log.Messages...))
} }
} }
// Infof performs a Printf() on the info logger // Infof performs a Printf() on the info logger
func (l *Stdlog) Infof(log Log) { func (l *Stdlog) Infof(log Log) {
if l.level >= LevelInfo { if l.level >= LevelInfo {
format := fmt.Sprintf("%s %s", log.directionPrefix(), log.Format) logStdMessage(l.info, log, CallDepth)
_ = l.info.Output(CallDepth, fmt.Sprintf(format, log.Messages...))
} }
} }
// Warnf performs a Printf() on the warn logger // Warnf performs a Printf() on the warn logger
func (l *Stdlog) Warnf(log Log) { func (l *Stdlog) Warnf(log Log) {
if l.level >= LevelWarn { if l.level >= LevelWarn {
format := fmt.Sprintf("%s %s", log.directionPrefix(), log.Format) logStdMessage(l.warn, log, CallDepth)
_ = l.warn.Output(CallDepth, fmt.Sprintf(format, log.Messages...))
} }
} }
// Errorf performs a Printf() on the error logger // Errorf performs a Printf() on the error logger
func (l *Stdlog) Errorf(log Log) { func (l *Stdlog) Errorf(log Log) {
if l.level >= LevelError { if l.level >= LevelError {
format := fmt.Sprintf("%s %s", log.directionPrefix(), log.Format) logStdMessage(l.err, log, CallDepth)
_ = 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 // 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 // Since there is no official standard RFC and we've seen different implementations
// of this mechanism (sending "Username:", "Username", "username", "User name", etc.) // of this mechanism (sending "Username:", "Username", "username", "User name", etc.)
// we follow the IETF-Draft and ignore any server challange to allow compatiblity // we follow the IETF-Draft and ignore any server challenge to allow compatibility
// with most mail servers/providers. // with most mail servers/providers.
// //
// LoginAuth will only send the credentials if the connection is using TLS // LoginAuth will only send the credentials if the connection is using TLS

View file

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