Compare commits

...

23 commits

Author SHA1 Message Date
Michael Fuchs
0354aa1c7e
Merge cc4c5bfd04 into c903f6e1b4 2024-10-17 14:21:22 +00:00
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
bb2fd0f970
Merge pull request #338 from wneessen/feature/no_auth_logging
Redact logging of SMTP authentication data
2024-10-15 20:25:57 +02:00
3234c13277
Add tests for SetLogAuthData method
Introduced TestClient_SetLogAuthData to verify the proper behavior of the SetLogAuthData method in both client and SMTP tests. This ensures that logAuthData is enabled or disabled as expected, increasing code reliability.
2024-10-15 20:02:24 +02:00
0944296cff
Enable logging of SMTP authentication data
Added a new option and methods to enable logging of SMTP authentication data. Updated documentation to indicate caution when using this feature due to potential data protection risks.
2024-10-15 19:52:59 +02:00
55a5d02fe0
Add support for configurable SMTP auth data logging
Added the `logAuthData` flag to enable conditional logging of SMTP authentication data. Introduced the `SetLogAuthData` method for clients to toggle this flag. Adjusted existing logging logic to respect this new configuration.
2024-10-15 19:52:31 +02:00
73663f6a6f
Merge pull request #337 from wneessen/dependabot/github_actions/github/codeql-action-3.26.13
Bump github/codeql-action from 3.26.12 to 3.26.13
2024-10-14 15:23:30 +02:00
dependabot[bot]
495794184d
Bump github/codeql-action from 3.26.12 to 3.26.13
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.12 to 3.26.13.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](c36620d31a...f779452ac5)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-14 13:18:14 +00:00
7acfe8015d
Redact authentication logs
Add a boolean flag `authIsActive` to manage redaction of sensitive authentication information in debug logs. When this flag is true, authentication details are replaced with `<auth redacted>`.
2024-10-12 20:53:58 +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
e1db5bf66a
Merge pull request #334 from wneessen/dependabot/github_actions/actions/upload-artifact-4.4.3
Bump actions/upload-artifact from 4.4.2 to 4.4.3
2024-10-10 16:26:28 +02:00
dependabot[bot]
7bc19a11dd
Bump actions/upload-artifact from 4.4.2 to 4.4.3
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.4.2 to 4.4.3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](84480863f2...b4b15b8c7c)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-10 13:40:06 +00:00
7b315e5fe9
Merge pull request #333 from wneessen/dependabot/github_actions/actions/upload-artifact-4.4.2
Bump actions/upload-artifact from 4.4.1 to 4.4.2
2024-10-09 16:30:29 +02:00
dependabot[bot]
295390999e
Bump actions/upload-artifact from 4.4.1 to 4.4.2
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.4.1 to 4.4.2.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](604373da63...84480863f2)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-09 14:07:22 +00:00
12 changed files with 265 additions and 52 deletions

View file

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

View file

@ -67,7 +67,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab. # format to the repository Actions tab.
- name: "Upload artifact" - name: "Upload artifact"
uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4.4.1 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with: with:
name: SARIF file name: SARIF file
path: results.sarif path: results.sarif
@ -75,6 +75,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard. # Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning" - name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
with: with:
sarif_file: results.sarif 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 // 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

@ -145,6 +145,9 @@ type (
// isEncrypted indicates wether the Client connection is encrypted or not. // isEncrypted indicates wether the Client connection is encrypted or not.
isEncrypted bool isEncrypted bool
// logAuthData indicates whether authentication-related data should be logged.
logAuthData bool
// logger is a logger that satisfies the log.Logger interface. // logger is a logger that satisfies the log.Logger interface.
logger log.Logger logger log.Logger
@ -256,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
@ -364,9 +368,10 @@ func WithSSLPort(fallback bool) Option {
// WithDebugLog enables debug logging for the Client. // WithDebugLog enables debug logging for the Client.
// //
// This function activates debug logging, which logs incoming and outgoing communication between the // 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 // Client and the SMTP server to os.Stderr. By default the debug logging will redact any kind of SMTP
// unencrypted authentication data, depending on the SMTP authentication method in use, which could // authentication data. If you need access to the actual authentication data in your logs, you can
// pose a data protection risk. // enable authentication data logging with the WithLogAuthData option or by setting it with the
// Client.SetLogAuthData method.
// //
// Returns: // Returns:
// - An Option function that enables debug logging for the Client. // - 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. // 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. // 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 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. // 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 // 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 { if c.useDebugLog {
c.smtpClient.SetDebugLog(true) c.smtpClient.SetDebugLog(true)
} }
if c.logAuthData {
c.smtpClient.SetLogAuthData()
}
if err = c.smtpClient.Hello(c.helo); err != nil { if err = c.smtpClient.Hello(c.helo); err != nil {
return err 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 // 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:
@ -1040,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

@ -123,6 +123,7 @@ func TestNewClientWithOptions(t *testing.T) {
{"WithoutNoop()", WithoutNoop(), false}, {"WithoutNoop()", WithoutNoop(), false},
{"WithDebugLog()", WithDebugLog(), false}, {"WithDebugLog()", WithDebugLog(), false},
{"WithLogger()", WithLogger(log.New(os.Stderr, log.LevelDebug)), 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) { {"WithDialContextFunc()", WithDialContextFunc(func(ctx context.Context, network, address string) (net.Conn, error) {
return nil, nil return nil, nil
}), false}, }), 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 // TestSetSMTPAuthCustom tests the SetSMTPAuthCustom method for the Client object
func TestSetSMTPAuthCustom(t *testing.T) { func TestSetSMTPAuthCustom(t *testing.T) {
tests := []struct { 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 // 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

@ -54,6 +54,9 @@ type Client struct {
// auth supported auth mechanisms // auth supported auth mechanisms
auth []string 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 // keep a reference to the connection so it can be used to create a TLS connection later
conn net.Conn conn net.Conn
@ -78,6 +81,9 @@ type Client struct {
// isConnected indicates if the Client has an active connection // isConnected indicates if the Client has an active connection
isConnected bool 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 is the name to use in HELO/EHLO
localName string // 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) { func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
c.mutex.Lock() 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...) id, err := c.Text.Cmd(format, args...)
if err != nil { if err != nil {
c.mutex.Unlock() c.mutex.Unlock()
@ -182,7 +196,13 @@ func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, s
} }
c.Text.StartResponse(id) c.Text.StartResponse(id)
code, msg, err := c.Text.ReadResponse(expectCode) 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.Text.EndResponse(id)
c.mutex.Unlock() c.mutex.Unlock()
return code, msg, err return code, msg, err
@ -256,6 +276,20 @@ func (c *Client) Auth(a Auth) error {
if err := c.hello(); err != nil { if err := c.hello(); err != nil {
return err 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 encoding := base64.StdEncoding
mech, resp, err := a.Start(&ServerInfo{c.serverName, c.tls, c.auth}) mech, resp, err := a.Start(&ServerInfo{c.serverName, c.tls, c.auth})
if err != nil { if err != nil {
@ -556,6 +590,13 @@ func (c *Client) SetLogger(l log.Logger) {
c.logger = l 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 // SetDSNMailReturnOption sets the DSN mail return option for the Mail method
func (c *Client) SetDSNMailReturnOption(d string) { func (c *Client) SetDSNMailReturnOption(d string) {
c.dsnmrtype = d c.dsnmrtype = d

View file

@ -1111,6 +1111,32 @@ func TestClient_SetLogger(t *testing.T) {
c.logger.Debugf(log.Log{Direction: log.DirServerToClient, Format: "%s", Messages: []interface{}{"test"}}) 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 var newClientServer = `220 hello world
250-mx.google.com at your service 250-mx.google.com at your service
250-SIZE 35651584 250-SIZE 35651584
@ -2137,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 {