mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-22 22:00:49 +01:00
Compare commits
11 commits
bb2fd0f970
...
c903f6e1b4
Author | SHA1 | Date | |
---|---|---|---|
c903f6e1b4 | |||
a638090d0e | |||
fb14e1e7dd | |||
f120485c98 | |||
569e8fbc70 | |||
8ea80c0739 | |||
9ae7681651 | |||
e854b2192f | |||
|
7b297d79b8 | ||
c2d9104b45 | |||
021666d6ad |
9 changed files with 134 additions and 42 deletions
10
auth.go
10
auth.go
|
@ -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"
|
||||||
|
|
27
client.go
27
client.go
|
@ -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")
|
||||||
|
|
|
@ -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
2
doc.go
|
@ -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
2
eml.go
|
@ -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()) ||
|
||||||
|
|
|
@ -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...))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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...))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue