mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-22 13:50:49 +01:00
Compare commits
41 commits
b564960d71
...
3e25812ef3
Author | SHA1 | Date | |
---|---|---|---|
|
3e25812ef3 | ||
c47f08dc7f | |||
|
87c0575dd4 | ||
cec7e38332 | |||
|
9ad77012e3 | ||
55e5a1536e | |||
c7d0a03ddc | |||
f79c1b8ebe | |||
df1a141368 | |||
e2ed5b747a | |||
2bd950469a | |||
3c29f68cc1 | |||
f5531eae14 | |||
91caf200ec | |||
|
63e6fc882d | ||
957cd8e0ca | |||
09133ef2a4 | |||
bf44fd2ad1 | |||
7bebdda27c | |||
c903f6e1b4 | |||
a638090d0e | |||
fb14e1e7dd | |||
f120485c98 | |||
569e8fbc70 | |||
8ea80c0739 | |||
9ae7681651 | |||
e854b2192f | |||
bb2fd0f970 | |||
3234c13277 | |||
0944296cff | |||
55a5d02fe0 | |||
73663f6a6f | |||
|
495794184d | ||
7acfe8015d | |||
|
7b297d79b8 | ||
c2d9104b45 | |||
021666d6ad | |||
e1db5bf66a | |||
|
7bc19a11dd | ||
7b315e5fe9 | |||
|
295390999e |
20 changed files with 555 additions and 91 deletions
2
.github/workflows/codecov.yml
vendored
2
.github/workflows/codecov.yml
vendored
|
@ -50,7 +50,7 @@ jobs:
|
|||
- name: Checkout Code
|
||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
|
||||
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- name: Install sendmail
|
||||
|
|
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
|
@ -54,7 +54,7 @@ jobs:
|
|||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
|
||||
uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||
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@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
|
||||
uses: github/codeql-action/autobuild@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||
|
||||
# ℹ️ 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@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
|
||||
uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||
|
|
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
|
@ -28,4 +28,4 @@ jobs:
|
|||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4
|
||||
uses: actions/dependency-review-action@a6993e2c61fd5dc440b409aa1d6904921c5e1894 # v4.3.5
|
||||
|
|
2
.github/workflows/golangci-lint.yml
vendored
2
.github/workflows/golangci-lint.yml
vendored
|
@ -24,7 +24,7 @@ jobs:
|
|||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
|
||||
- uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
||||
with:
|
||||
go-version: '1.23'
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
|
2
.github/workflows/offline-tests.yml
vendored
2
.github/workflows/offline-tests.yml
vendored
|
@ -37,7 +37,7 @@ jobs:
|
|||
- name: Checkout Code
|
||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
|
||||
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- name: Run Tests
|
||||
|
|
4
.github/workflows/scorecards.yml
vendored
4
.github/workflows/scorecards.yml
vendored
|
@ -67,7 +67,7 @@ jobs:
|
|||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||
# format to the repository Actions tab.
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4.4.1
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
|
@ -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@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
|
||||
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
|
2
.github/workflows/sonarqube.yml
vendored
2
.github/workflows/sonarqube.yml
vendored
|
@ -37,7 +37,7 @@ jobs:
|
|||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
|
||||
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
||||
with:
|
||||
go-version: '1.23'
|
||||
|
||||
|
|
83
auth.go
83
auth.go
|
@ -4,7 +4,11 @@
|
|||
|
||||
package mail
|
||||
|
||||
import "errors"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SMTPAuthType is a type wrapper for a string type. It represents the type of SMTP authentication
|
||||
// mechanism to be used.
|
||||
|
@ -35,7 +39,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 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
|
||||
// connection.
|
||||
//
|
||||
|
@ -44,20 +48,53 @@ const (
|
|||
// https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
|
||||
SMTPAuthLogin SMTPAuthType = "LOGIN"
|
||||
|
||||
// SMTPAuthLoginNoEnc is the "LOGIN" SASL authentication mechanism. This authentication mechanism
|
||||
// does not have an official RFC that could be followed. There is a spec by Microsoft and an
|
||||
// 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
|
||||
// plaintext over the internet connection, by default we only allow this mechanism over
|
||||
// a TLS secured connection. This authentiation mechanism overrides this default and will
|
||||
// allow LOGIN authentication via an unencrypted channel. This can be useful if the
|
||||
// connection has already been secured in a different way (e. g. a SSH tunnel)
|
||||
//
|
||||
// Note: Use this authentication method with caution. If used in the wrong way, you might
|
||||
// expose your authentication information over unencrypted channels!
|
||||
//
|
||||
// https://msopenspecs.azureedge.net/files/MS-XLOGIN/%5bMS-XLOGIN%5d.pdf
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
|
||||
SMTPAuthLoginNoEnc SMTPAuthType = "LOGIN-NOENC"
|
||||
|
||||
// SMTPAuthNoAuth is equivalent to performing no authentication at all. It is a convenience
|
||||
// option and should not be used. Instead, for mail servers that do no support/require
|
||||
// authentication, the Client should not be passed the WithSMTPAuth option at all.
|
||||
SMTPAuthNoAuth SMTPAuthType = ""
|
||||
SMTPAuthNoAuth SMTPAuthType = "NOAUTH"
|
||||
|
||||
// SMTPAuthPlain is the "PLAIN" authentication mechanism as described in RFC 4616.
|
||||
//
|
||||
// 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
|
||||
// connection.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc4616/
|
||||
SMTPAuthPlain SMTPAuthType = "PLAIN"
|
||||
|
||||
// SMTPAuthPlainNoEnc is the "PLAIN" authentication mechanism as described in RFC 4616.
|
||||
//
|
||||
// Since the "PLAIN" SASL authentication mechanism transmits the username and password in
|
||||
// plaintext over the internet connection, by default we only allow this mechanism over
|
||||
// a TLS secured connection. This authentiation mechanism overrides this default and will
|
||||
// allow PLAIN authentication via an unencrypted channel. This can be useful if the
|
||||
// connection has already been secured in a different way (e. g. a SSH tunnel)
|
||||
//
|
||||
// Note: Use this authentication method with caution. If used in the wrong way, you might
|
||||
// expose your authentication information over unencrypted channels!
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc4616/
|
||||
SMTPAuthPlainNoEnc SMTPAuthType = "PLAIN-NOENC"
|
||||
|
||||
// SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism.
|
||||
// https://developers.google.com/gmail/imap/xoauth2-protocol
|
||||
SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2"
|
||||
|
@ -76,7 +113,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 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
|
||||
// of a challenge-response authentication mechanism (as we use it). However, it is generally
|
||||
|
@ -95,7 +132,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 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
|
||||
SMTPAuthSCRAMSHA256PLUS SMTPAuthType = "SCRAM-SHA-256-PLUS"
|
||||
|
@ -134,3 +171,37 @@ var (
|
|||
// authentication type.
|
||||
ErrSCRAMSHA256PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256-PLUS")
|
||||
)
|
||||
|
||||
// UnmarshalString satisfies the fig.StringUnmarshaler interface for the SMTPAuthType type
|
||||
// https://pkg.go.dev/github.com/kkyr/fig#StringUnmarshaler
|
||||
func (sa *SMTPAuthType) UnmarshalString(value string) error {
|
||||
switch strings.ToLower(value) {
|
||||
case "cram-md5", "crammd5", "cram":
|
||||
*sa = SMTPAuthCramMD5
|
||||
case "custom":
|
||||
*sa = SMTPAuthCustom
|
||||
case "login":
|
||||
*sa = SMTPAuthLogin
|
||||
case "login-noenc":
|
||||
*sa = SMTPAuthLoginNoEnc
|
||||
case "none", "noauth", "no":
|
||||
*sa = SMTPAuthNoAuth
|
||||
case "plain":
|
||||
*sa = SMTPAuthPlain
|
||||
case "plain-noenc":
|
||||
*sa = SMTPAuthPlainNoEnc
|
||||
case "scram-sha-1", "scram-sha1", "scramsha1":
|
||||
*sa = SMTPAuthSCRAMSHA1
|
||||
case "scram-sha-1-plus", "scram-sha1-plus", "scramsha1plus":
|
||||
*sa = SMTPAuthSCRAMSHA1PLUS
|
||||
case "scram-sha-256", "scram-sha256", "scramsha256":
|
||||
*sa = SMTPAuthSCRAMSHA256
|
||||
case "scram-sha-256-plus", "scram-sha256-plus", "scramsha256plus":
|
||||
*sa = SMTPAuthSCRAMSHA256PLUS
|
||||
case "xoauth2", "oauth2":
|
||||
*sa = SMTPAuthXOAUTH2
|
||||
default:
|
||||
return fmt.Errorf("unsupported SMTP auth type: %s", value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
59
auth_test.go
Normal file
59
auth_test.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
// SPDX-FileCopyrightText: 2024 The go-mail Authors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mail
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSMTPAuthType_UnmarshalString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
authString string
|
||||
expected SMTPAuthType
|
||||
}{
|
||||
{"CRAM-MD5: cram-md5", "cram-md5", SMTPAuthCramMD5},
|
||||
{"CRAM-MD5: crammd5", "crammd5", SMTPAuthCramMD5},
|
||||
{"CRAM-MD5: cram", "cram", SMTPAuthCramMD5},
|
||||
{"CUSTOM", "custom", SMTPAuthCustom},
|
||||
{"LOGIN", "login", SMTPAuthLogin},
|
||||
{"LOGIN-NOENC", "login-noenc", SMTPAuthLoginNoEnc},
|
||||
{"NONE: none", "none", SMTPAuthNoAuth},
|
||||
{"NONE: noauth", "noauth", SMTPAuthNoAuth},
|
||||
{"NONE: no", "no", SMTPAuthNoAuth},
|
||||
{"PLAIN", "plain", SMTPAuthPlain},
|
||||
{"PLAIN-NOENC", "plain-noenc", SMTPAuthPlainNoEnc},
|
||||
{"SCRAM-SHA-1: scram-sha-1", "scram-sha-1", SMTPAuthSCRAMSHA1},
|
||||
{"SCRAM-SHA-1: scram-sha1", "scram-sha1", SMTPAuthSCRAMSHA1},
|
||||
{"SCRAM-SHA-1: scramsha1", "scramsha1", SMTPAuthSCRAMSHA1},
|
||||
{"SCRAM-SHA-1-PLUS: scram-sha-1-plus", "scram-sha-1-plus", SMTPAuthSCRAMSHA1PLUS},
|
||||
{"SCRAM-SHA-1-PLUS: scram-sha1-plus", "scram-sha1-plus", SMTPAuthSCRAMSHA1PLUS},
|
||||
{"SCRAM-SHA-1-PLUS: scramsha1plus", "scramsha1plus", SMTPAuthSCRAMSHA1PLUS},
|
||||
{"SCRAM-SHA-256: scram-sha-256", "scram-sha-256", SMTPAuthSCRAMSHA256},
|
||||
{"SCRAM-SHA-256: scram-sha256", "scram-sha256", SMTPAuthSCRAMSHA256},
|
||||
{"SCRAM-SHA-256: scramsha256", "scramsha256", SMTPAuthSCRAMSHA256},
|
||||
{"SCRAM-SHA-256-PLUS: scram-sha-256-plus", "scram-sha-256-plus", SMTPAuthSCRAMSHA256PLUS},
|
||||
{"SCRAM-SHA-256-PLUS: scram-sha256-plus", "scram-sha256-plus", SMTPAuthSCRAMSHA256PLUS},
|
||||
{"SCRAM-SHA-256-PLUS: scramsha256plus", "scramsha256plus", SMTPAuthSCRAMSHA256PLUS},
|
||||
{"XOAUTH2: xoauth2", "xoauth2", SMTPAuthXOAUTH2},
|
||||
{"XOAUTH2: oauth2", "oauth2", SMTPAuthXOAUTH2},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var authType SMTPAuthType
|
||||
if err := authType.UnmarshalString(tt.authString); err != nil {
|
||||
t.Errorf("UnmarshalString() for type %s failed: %s", tt.authString, err)
|
||||
}
|
||||
if authType != tt.expected {
|
||||
t.Errorf("UnmarshalString() for type %s failed: expected %s, got %s",
|
||||
tt.authString, tt.expected, authType)
|
||||
}
|
||||
})
|
||||
}
|
||||
t.Run("should fail", func(t *testing.T) {
|
||||
var authType SMTPAuthType
|
||||
if err := authType.UnmarshalString("invalid"); err == nil {
|
||||
t.Error("UnmarshalString() should have failed")
|
||||
}
|
||||
})
|
||||
}
|
73
client.go
73
client.go
|
@ -145,6 +145,9 @@ type (
|
|||
// isEncrypted indicates wether the Client connection is encrypted or not.
|
||||
isEncrypted bool
|
||||
|
||||
// logAuthData indicates whether authentication-related data should be logged.
|
||||
logAuthData bool
|
||||
|
||||
// logger is a logger that satisfies the log.Logger interface.
|
||||
logger log.Logger
|
||||
|
||||
|
@ -256,6 +259,7 @@ var (
|
|||
// - An error if any critical default values are missing or options fail to apply.
|
||||
func NewClient(host string, opts ...Option) (*Client, error) {
|
||||
c := &Client{
|
||||
smtpAuthType: SMTPAuthNoAuth,
|
||||
connTimeout: DefaultTimeout,
|
||||
host: host,
|
||||
port: DefaultPort,
|
||||
|
@ -364,9 +368,10 @@ func WithSSLPort(fallback bool) Option {
|
|||
// WithDebugLog enables debug logging for the Client.
|
||||
//
|
||||
// This function activates debug logging, which logs incoming and outgoing communication between the
|
||||
// Client and the SMTP server to os.Stderr. Be cautious when using this option, as the logs may include
|
||||
// unencrypted authentication data, depending on the SMTP authentication method in use, which could
|
||||
// pose a data protection risk.
|
||||
// Client and the SMTP server to os.Stderr. By default the debug logging will redact any kind of SMTP
|
||||
// authentication data. If you need access to the actual authentication data in your logs, you can
|
||||
// enable authentication data logging with the WithLogAuthData option or by setting it with the
|
||||
// Client.SetLogAuthData method.
|
||||
//
|
||||
// Returns:
|
||||
// - An Option function that enables debug logging for the Client.
|
||||
|
@ -671,6 +676,22 @@ func WithDialContextFunc(dialCtxFunc DialContextFunc) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithLogAuthData enables logging of authentication data.
|
||||
//
|
||||
// This function sets the logAuthData field of the Client to true, enabling the logging of authentication data.
|
||||
//
|
||||
// Be cautious when using this option, as the logs may include unencrypted authentication data, depending on
|
||||
// the SMTP authentication method in use, which could pose a data protection risk.
|
||||
//
|
||||
// Returns:
|
||||
// - An Option function that configures the Client to enable authentication data logging.
|
||||
func WithLogAuthData() Option {
|
||||
return func(c *Client) error {
|
||||
c.logAuthData = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// TLSPolicy returns the TLSPolicy that is currently set on the Client as a string.
|
||||
//
|
||||
// This method retrieves the current TLSPolicy configured for the Client and returns it as a string representation.
|
||||
|
@ -865,6 +886,19 @@ func (c *Client) SetSMTPAuthCustom(smtpAuth smtp.Auth) {
|
|||
c.smtpAuthType = SMTPAuthCustom
|
||||
}
|
||||
|
||||
// SetLogAuthData sets or overrides the logging of SMTP authentication data for the Client.
|
||||
//
|
||||
// This function sets the logAuthData field of the Client to true, enabling the logging of authentication data.
|
||||
//
|
||||
// Be cautious when using this option, as the logs may include unencrypted authentication data, depending on
|
||||
// the SMTP authentication method in use, which could pose a data protection risk.
|
||||
//
|
||||
// Parameters:
|
||||
// - logAuth: Set wether or not to log SMTP authentication data for the Client.
|
||||
func (c *Client) SetLogAuthData(logAuth bool) {
|
||||
c.logAuthData = logAuth
|
||||
}
|
||||
|
||||
// DialWithContext establishes a connection to the server using the provided context.Context.
|
||||
//
|
||||
// This function adds a deadline based on the Client's timeout to the provided context.Context
|
||||
|
@ -921,6 +955,9 @@ func (c *Client) DialWithContext(dialCtx context.Context) error {
|
|||
if c.useDebugLog {
|
||||
c.smtpClient.SetDebugLog(true)
|
||||
}
|
||||
if c.logAuthData {
|
||||
c.smtpClient.SetLogAuthData()
|
||||
}
|
||||
if err = c.smtpClient.Hello(c.helo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1028,9 +1065,16 @@ func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) e
|
|||
// determines the supported authentication methods, and applies the appropriate authentication
|
||||
// type. An error is returned if authentication fails.
|
||||
//
|
||||
// This method first verifies the connection to the SMTP server. If no custom authentication
|
||||
// mechanism is provided, it checks which authentication methods are supported by the server.
|
||||
// Based on the configured SMTPAuthType, it sets up the appropriate authentication mechanism.
|
||||
// By default NewClient sets the SMTP authentication type to SMTPAuthNoAuth, meaning, that no
|
||||
// SMTP authentication will be performed. If the user makes use of SetSMTPAuth or initialzes the
|
||||
// client with WithSMTPAuth, the SMTP authentication type will be set in the Client, forcing
|
||||
// this method to determine if the server supports the selected authentication method and
|
||||
// assigning the corresponding smtp.Auth function to it.
|
||||
//
|
||||
// If the user set a custom SMTP authentication function using SetSMTPAuthCustom or
|
||||
// WithSMTPAuthCustom, we will not perform any detection and assignment logic and will trust
|
||||
// the user with their provided smtp.Auth function.
|
||||
//
|
||||
// Finally, it attempts to authenticate the client using the selected method.
|
||||
//
|
||||
// Returns:
|
||||
|
@ -1040,7 +1084,8 @@ func (c *Client) auth() error {
|
|||
if err := c.checkConn(); err != nil {
|
||||
return fmt.Errorf("failed to authenticate: %w", err)
|
||||
}
|
||||
if c.smtpAuth == nil && c.smtpAuthType != SMTPAuthCustom {
|
||||
|
||||
if c.smtpAuth == nil && c.smtpAuthType != SMTPAuthNoAuth {
|
||||
hasSMTPAuth, smtpAuthType := c.smtpClient.Extension("AUTH")
|
||||
if !hasSMTPAuth {
|
||||
return fmt.Errorf("server does not support SMTP AUTH")
|
||||
|
@ -1051,12 +1096,22 @@ func (c *Client) auth() error {
|
|||
if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) {
|
||||
return ErrPlainAuthNotSupported
|
||||
}
|
||||
c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host)
|
||||
c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host, false)
|
||||
case SMTPAuthPlainNoEnc:
|
||||
if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) {
|
||||
return ErrPlainAuthNotSupported
|
||||
}
|
||||
c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host, true)
|
||||
case SMTPAuthLogin:
|
||||
if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) {
|
||||
return ErrLoginAuthNotSupported
|
||||
}
|
||||
c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host)
|
||||
c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host, false)
|
||||
case SMTPAuthLoginNoEnc:
|
||||
if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) {
|
||||
return ErrLoginAuthNotSupported
|
||||
}
|
||||
c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host, true)
|
||||
case SMTPAuthCramMD5:
|
||||
if !strings.Contains(smtpAuthType, string(SMTPAuthCramMD5)) {
|
||||
return ErrCramMD5AuthNotSupported
|
||||
|
|
103
client_test.go
103
client_test.go
|
@ -110,7 +110,7 @@ func TestNewClientWithOptions(t *testing.T) {
|
|||
{"WithSMTPAuth()", WithSMTPAuth(SMTPAuthLogin), false},
|
||||
{
|
||||
"WithSMTPAuthCustom()",
|
||||
WithSMTPAuthCustom(smtp.PlainAuth("", "", "", "")),
|
||||
WithSMTPAuthCustom(smtp.PlainAuth("", "", "", "", false)),
|
||||
false,
|
||||
},
|
||||
{"WithUsername()", WithUsername("test"), false},
|
||||
|
@ -123,6 +123,7 @@ func TestNewClientWithOptions(t *testing.T) {
|
|||
{"WithoutNoop()", WithoutNoop(), false},
|
||||
{"WithDebugLog()", WithDebugLog(), false},
|
||||
{"WithLogger()", WithLogger(log.New(os.Stderr, log.LevelDebug)), false},
|
||||
{"WithLogger()", WithLogAuthData(), false},
|
||||
{"WithDialContextFunc()", WithDialContextFunc(func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return nil, nil
|
||||
}), false},
|
||||
|
@ -578,6 +579,23 @@ func TestWithoutNoop(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClient_SetLogAuthData(t *testing.T) {
|
||||
c, err := NewClient(DefaultHost, WithLogAuthData())
|
||||
if err != nil {
|
||||
t.Errorf("failed to create new client: %s", err)
|
||||
return
|
||||
}
|
||||
if !c.logAuthData {
|
||||
t.Errorf("WithLogAuthData failed. c.logAuthData expected to be: %t, got: %t", true,
|
||||
c.logAuthData)
|
||||
}
|
||||
c.SetLogAuthData(false)
|
||||
if c.logAuthData {
|
||||
t.Errorf("SetLogAuthData failed. c.logAuthData expected to be: %t, got: %t", false,
|
||||
c.logAuthData)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetSMTPAuthCustom tests the SetSMTPAuthCustom method for the Client object
|
||||
func TestSetSMTPAuthCustom(t *testing.T) {
|
||||
tests := []struct {
|
||||
|
@ -587,8 +605,8 @@ func TestSetSMTPAuthCustom(t *testing.T) {
|
|||
sf bool
|
||||
}{
|
||||
{"SMTPAuth: CRAM-MD5", smtp.CRAMMD5Auth("", ""), "CRAM-MD5", false},
|
||||
{"SMTPAuth: LOGIN", smtp.LoginAuth("", "", ""), "LOGIN", false},
|
||||
{"SMTPAuth: PLAIN", smtp.PlainAuth("", "", "", ""), "PLAIN", false},
|
||||
{"SMTPAuth: LOGIN", smtp.LoginAuth("", "", "", false), "LOGIN", false},
|
||||
{"SMTPAuth: PLAIN", smtp.PlainAuth("", "", "", "", false), "PLAIN", false},
|
||||
}
|
||||
si := smtp.ServerInfo{TLS: true}
|
||||
for _, tt := range tests {
|
||||
|
@ -789,7 +807,7 @@ func TestClient_DialWithContextInvalidAuth(t *testing.T) {
|
|||
}
|
||||
c.user = "invalid"
|
||||
c.pass = "invalid"
|
||||
c.SetSMTPAuthCustom(smtp.LoginAuth("invalid", "invalid", "invalid"))
|
||||
c.SetSMTPAuthCustom(smtp.LoginAuth("invalid", "invalid", "invalid", false))
|
||||
ctx := context.Background()
|
||||
if err = c.DialWithContext(ctx); err == nil {
|
||||
t.Errorf("dial succeeded but was supposed to fail")
|
||||
|
@ -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"), false))
|
||||
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 {
|
||||
|
@ -1862,7 +1955,7 @@ func TestClient_DialSendConcurrent_local(t *testing.T) {
|
|||
wg.Wait()
|
||||
|
||||
if err = client.Close(); err != nil {
|
||||
t.Errorf("failed to close server connection: %s", err)
|
||||
t.Logf("failed to close server connection: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
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
|
||||
// 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)
|
||||
}
|
||||
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 {
|
||||
contentType, _ := parseMultiPartHeader(contentTypeSlice[0])
|
||||
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
|
||||
func (l *JSONlog) Debugf(log Log) {
|
||||
if l.level >= LevelDebug {
|
||||
l.log.WithGroup(DirString).With(
|
||||
slog.String(DirFromString, log.directionFrom()),
|
||||
slog.String(DirToString, log.directionTo()),
|
||||
).Debug(fmt.Sprintf(log.Format, log.Messages...))
|
||||
logMessage(LevelDebug, l.log, log, fmt.Sprintf)
|
||||
}
|
||||
}
|
||||
|
||||
// Infof logs a info message via the structured JSON logger
|
||||
func (l *JSONlog) Infof(log Log) {
|
||||
if l.level >= LevelInfo {
|
||||
l.log.WithGroup(DirString).With(
|
||||
slog.String(DirFromString, log.directionFrom()),
|
||||
slog.String(DirToString, log.directionTo()),
|
||||
).Info(fmt.Sprintf(log.Format, log.Messages...))
|
||||
logMessage(LevelInfo, l.log, log, fmt.Sprintf)
|
||||
}
|
||||
}
|
||||
|
||||
// Warnf logs a warn message via the structured JSON logger
|
||||
func (l *JSONlog) Warnf(log Log) {
|
||||
if l.level >= LevelWarn {
|
||||
l.log.WithGroup(DirString).With(
|
||||
slog.String(DirFromString, log.directionFrom()),
|
||||
slog.String(DirToString, log.directionTo()),
|
||||
).Warn(fmt.Sprintf(log.Format, log.Messages...))
|
||||
logMessage(LevelWarn, l.log, log, fmt.Sprintf)
|
||||
}
|
||||
}
|
||||
|
||||
// Errorf logs a warn message via the structured JSON logger
|
||||
func (l *JSONlog) Errorf(log Log) {
|
||||
if l.level >= LevelError {
|
||||
l.log.WithGroup(DirString).With(
|
||||
slog.String(DirFromString, log.directionFrom()),
|
||||
slog.String(DirToString, log.directionTo()),
|
||||
).Error(fmt.Sprintf(log.Format, log.Messages...))
|
||||
logMessage(LevelError, l.log, log, fmt.Sprintf)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
func (l *Stdlog) Debugf(log Log) {
|
||||
if l.level >= LevelDebug {
|
||||
format := fmt.Sprintf("%s %s", log.directionPrefix(), log.Format)
|
||||
_ = l.debug.Output(CallDepth, fmt.Sprintf(format, log.Messages...))
|
||||
logStdMessage(l.debug, log, CallDepth)
|
||||
}
|
||||
}
|
||||
|
||||
// Infof performs a Printf() on the info logger
|
||||
func (l *Stdlog) Infof(log Log) {
|
||||
if l.level >= LevelInfo {
|
||||
format := fmt.Sprintf("%s %s", log.directionPrefix(), log.Format)
|
||||
_ = l.info.Output(CallDepth, fmt.Sprintf(format, log.Messages...))
|
||||
logStdMessage(l.info, log, CallDepth)
|
||||
}
|
||||
}
|
||||
|
||||
// Warnf performs a Printf() on the warn logger
|
||||
func (l *Stdlog) Warnf(log Log) {
|
||||
if l.level >= LevelWarn {
|
||||
format := fmt.Sprintf("%s %s", log.directionPrefix(), log.Format)
|
||||
_ = l.warn.Output(CallDepth, fmt.Sprintf(format, log.Messages...))
|
||||
logStdMessage(l.warn, log, CallDepth)
|
||||
}
|
||||
}
|
||||
|
||||
// Errorf performs a Printf() on the error logger
|
||||
func (l *Stdlog) Errorf(log Log) {
|
||||
if l.level >= LevelError {
|
||||
format := fmt.Sprintf("%s %s", log.directionPrefix(), log.Format)
|
||||
_ = l.err.Output(CallDepth, fmt.Sprintf(format, log.Messages...))
|
||||
logStdMessage(l.err, log, CallDepth)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ type loginAuth struct {
|
|||
username, password string
|
||||
host string
|
||||
respStep uint8
|
||||
allowUnencryptedAuth bool
|
||||
}
|
||||
|
||||
// LoginAuth returns an [Auth] that implements the LOGIN authentication
|
||||
|
@ -29,14 +30,14 @@ 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 challange to allow compatiblity
|
||||
// we follow the IETF-Draft and ignore any server challenge to allow compatibility
|
||||
// with most mail servers/providers.
|
||||
//
|
||||
// LoginAuth will only send the credentials if the connection is using TLS
|
||||
// or is connected to localhost. Otherwise authentication will fail with an
|
||||
// error, without sending the credentials.
|
||||
func LoginAuth(username, password, host string) Auth {
|
||||
return &loginAuth{username, password, host, 0}
|
||||
func LoginAuth(username, password, host string, allowUnEnc bool) Auth {
|
||||
return &loginAuth{username, password, host, 0, allowUnEnc}
|
||||
}
|
||||
|
||||
// Start begins the SMTP authentication process by validating server's TLS status and hostname.
|
||||
|
@ -47,7 +48,7 @@ func (a *loginAuth) Start(server *ServerInfo) (string, []byte, error) {
|
|||
// In particular, it doesn't matter if the server advertises LOGIN auth.
|
||||
// That might just be the attacker saying
|
||||
// "it's ok, you can trust me with your password."
|
||||
if !server.TLS && !isLocalhost(server.Name) {
|
||||
if !a.allowUnencryptedAuth && !server.TLS && !isLocalhost(server.Name) {
|
||||
return "", nil, ErrUnencrypted
|
||||
}
|
||||
if server.Name != a.host {
|
||||
|
|
|
@ -17,6 +17,7 @@ package smtp
|
|||
type plainAuth struct {
|
||||
identity, username, password string
|
||||
host string
|
||||
allowUnencryptedAuth bool
|
||||
}
|
||||
|
||||
// PlainAuth returns an [Auth] that implements the PLAIN authentication
|
||||
|
@ -27,8 +28,8 @@ type plainAuth struct {
|
|||
// PlainAuth will only send the credentials if the connection is using TLS
|
||||
// or is connected to localhost. Otherwise authentication will fail with an
|
||||
// error, without sending the credentials.
|
||||
func PlainAuth(identity, username, password, host string) Auth {
|
||||
return &plainAuth{identity, username, password, host}
|
||||
func PlainAuth(identity, username, password, host string, allowUnEnc bool) Auth {
|
||||
return &plainAuth{identity, username, password, host, allowUnEnc}
|
||||
}
|
||||
|
||||
func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
|
||||
|
@ -37,7 +38,7 @@ func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
|
|||
// In particular, it doesn't matter if the server advertises PLAIN auth.
|
||||
// That might just be the attacker saying
|
||||
// "it's ok, you can trust me with your password."
|
||||
if !server.TLS && !isLocalhost(server.Name) {
|
||||
if !a.allowUnencryptedAuth && !server.TLS && !isLocalhost(server.Name) {
|
||||
return "", nil, ErrUnencrypted
|
||||
}
|
||||
if server.Name != a.host {
|
||||
|
|
|
@ -67,7 +67,7 @@ var (
|
|||
func ExamplePlainAuth() {
|
||||
// hostname is used by PlainAuth to validate the TLS certificate.
|
||||
hostname := "mail.example.com"
|
||||
auth := smtp.PlainAuth("", "user@example.com", "password", hostname)
|
||||
auth := smtp.PlainAuth("", "user@example.com", "password", hostname, false)
|
||||
|
||||
err := smtp.SendMail(hostname+":25", auth, from, recipients, msg)
|
||||
if err != nil {
|
||||
|
@ -77,7 +77,7 @@ func ExamplePlainAuth() {
|
|||
|
||||
func ExampleSendMail() {
|
||||
// Set up authentication information.
|
||||
auth := smtp.PlainAuth("", "user@example.com", "password", "mail.example.com")
|
||||
auth := smtp.PlainAuth("", "user@example.com", "password", "mail.example.com", false)
|
||||
|
||||
// Connect to the server, authenticate, set the sender and recipient,
|
||||
// and send the email all in one step.
|
||||
|
|
45
smtp/smtp.go
45
smtp/smtp.go
|
@ -54,6 +54,9 @@ type Client struct {
|
|||
// auth supported auth mechanisms
|
||||
auth []string
|
||||
|
||||
// authIsActive indicates that the Client is currently during SMTP authentication
|
||||
authIsActive bool
|
||||
|
||||
// keep a reference to the connection so it can be used to create a TLS connection later
|
||||
conn net.Conn
|
||||
|
||||
|
@ -78,6 +81,9 @@ type Client struct {
|
|||
// isConnected indicates if the Client has an active connection
|
||||
isConnected bool
|
||||
|
||||
// logAuthData indicates if the Client should include SMTP authentication data in the logs
|
||||
logAuthData bool
|
||||
|
||||
// localName is the name to use in HELO/EHLO
|
||||
localName string // the name to use in HELO/EHLO
|
||||
|
||||
|
@ -174,7 +180,15 @@ func (c *Client) Hello(localName string) error {
|
|||
func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
|
||||
c.mutex.Lock()
|
||||
|
||||
c.debugLog(log.DirClientToServer, format, args...)
|
||||
var logMsg []interface{}
|
||||
logMsg = args
|
||||
logFmt := format
|
||||
if c.authIsActive {
|
||||
logMsg = []interface{}{"<SMTP auth data redacted>"}
|
||||
logFmt = "%s"
|
||||
}
|
||||
c.debugLog(log.DirClientToServer, logFmt, logMsg...)
|
||||
|
||||
id, err := c.Text.Cmd(format, args...)
|
||||
if err != nil {
|
||||
c.mutex.Unlock()
|
||||
|
@ -182,7 +196,13 @@ func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, s
|
|||
}
|
||||
c.Text.StartResponse(id)
|
||||
code, msg, err := c.Text.ReadResponse(expectCode)
|
||||
c.debugLog(log.DirServerToClient, "%d %s", code, msg)
|
||||
|
||||
logMsg = []interface{}{code, msg}
|
||||
if c.authIsActive && code >= 300 && code <= 400 {
|
||||
logMsg = []interface{}{code, "<SMTP auth data redacted>"}
|
||||
}
|
||||
c.debugLog(log.DirServerToClient, "%d %s", logMsg...)
|
||||
|
||||
c.Text.EndResponse(id)
|
||||
c.mutex.Unlock()
|
||||
return code, msg, err
|
||||
|
@ -256,6 +276,20 @@ func (c *Client) Auth(a Auth) error {
|
|||
if err := c.hello(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
if !c.logAuthData {
|
||||
c.authIsActive = true
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
defer func() {
|
||||
c.mutex.Lock()
|
||||
if !c.logAuthData {
|
||||
c.authIsActive = false
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
}()
|
||||
|
||||
encoding := base64.StdEncoding
|
||||
mech, resp, err := a.Start(&ServerInfo{c.serverName, c.tls, c.auth})
|
||||
if err != nil {
|
||||
|
@ -556,6 +590,13 @@ func (c *Client) SetLogger(l log.Logger) {
|
|||
c.logger = l
|
||||
}
|
||||
|
||||
// SetLogAuthData enables logging of authentication data in the Client.
|
||||
func (c *Client) SetLogAuthData() {
|
||||
c.mutex.Lock()
|
||||
c.logAuthData = true
|
||||
c.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetDSNMailReturnOption sets the DSN mail return option for the Mail method
|
||||
func (c *Client) SetDSNMailReturnOption(d string) {
|
||||
c.dsnmrtype = d
|
||||
|
|
|
@ -50,7 +50,7 @@ type authTest struct {
|
|||
|
||||
var authTests = []authTest{
|
||||
{
|
||||
PlainAuth("", "user", "pass", "testserver"),
|
||||
PlainAuth("", "user", "pass", "testserver", false),
|
||||
[]string{},
|
||||
"PLAIN",
|
||||
[]string{"\x00user\x00pass"},
|
||||
|
@ -58,7 +58,15 @@ var authTests = []authTest{
|
|||
false,
|
||||
},
|
||||
{
|
||||
PlainAuth("foo", "bar", "baz", "testserver"),
|
||||
PlainAuth("", "user", "pass", "testserver", true),
|
||||
[]string{},
|
||||
"PLAIN",
|
||||
[]string{"\x00user\x00pass"},
|
||||
[]bool{false, false},
|
||||
false,
|
||||
},
|
||||
{
|
||||
PlainAuth("foo", "bar", "baz", "testserver", false),
|
||||
[]string{},
|
||||
"PLAIN",
|
||||
[]string{"foo\x00bar\x00baz"},
|
||||
|
@ -66,7 +74,7 @@ var authTests = []authTest{
|
|||
false,
|
||||
},
|
||||
{
|
||||
PlainAuth("foo", "bar", "baz", "testserver"),
|
||||
PlainAuth("foo", "bar", "baz", "testserver", false),
|
||||
[]string{"foo"},
|
||||
"PLAIN",
|
||||
[]string{"foo\x00bar\x00baz", ""},
|
||||
|
@ -74,7 +82,7 @@ var authTests = []authTest{
|
|||
false,
|
||||
},
|
||||
{
|
||||
LoginAuth("user", "pass", "testserver"),
|
||||
LoginAuth("user", "pass", "testserver", false),
|
||||
[]string{"Username:", "Password:"},
|
||||
"LOGIN",
|
||||
[]string{"", "user", "pass"},
|
||||
|
@ -82,7 +90,15 @@ var authTests = []authTest{
|
|||
false,
|
||||
},
|
||||
{
|
||||
LoginAuth("user", "pass", "testserver"),
|
||||
LoginAuth("user", "pass", "testserver", true),
|
||||
[]string{"Username:", "Password:"},
|
||||
"LOGIN",
|
||||
[]string{"", "user", "pass"},
|
||||
[]bool{false, false},
|
||||
false,
|
||||
},
|
||||
{
|
||||
LoginAuth("user", "pass", "testserver", false),
|
||||
[]string{"User Name\x00", "Password\x00"},
|
||||
"LOGIN",
|
||||
[]string{"", "user", "pass"},
|
||||
|
@ -90,7 +106,7 @@ var authTests = []authTest{
|
|||
false,
|
||||
},
|
||||
{
|
||||
LoginAuth("user", "pass", "testserver"),
|
||||
LoginAuth("user", "pass", "testserver", false),
|
||||
[]string{"Invalid", "Invalid:"},
|
||||
"LOGIN",
|
||||
[]string{"", "user", "pass"},
|
||||
|
@ -98,7 +114,7 @@ var authTests = []authTest{
|
|||
false,
|
||||
},
|
||||
{
|
||||
LoginAuth("user", "pass", "testserver"),
|
||||
LoginAuth("user", "pass", "testserver", false),
|
||||
[]string{"Invalid", "Invalid:", "Too many"},
|
||||
"LOGIN",
|
||||
[]string{"", "user", "pass", ""},
|
||||
|
@ -237,7 +253,47 @@ func TestAuthPlain(t *testing.T) {
|
|||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
auth := PlainAuth("foo", "bar", "baz", tt.authName)
|
||||
auth := PlainAuth("foo", "bar", "baz", tt.authName, false)
|
||||
_, _, err := auth.Start(tt.server)
|
||||
got := ""
|
||||
if err != nil {
|
||||
got = err.Error()
|
||||
}
|
||||
if got != tt.err {
|
||||
t.Errorf("%d. got error = %q; want %q", i, got, tt.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthPlainNoEnc(t *testing.T) {
|
||||
tests := []struct {
|
||||
authName string
|
||||
server *ServerInfo
|
||||
err string
|
||||
}{
|
||||
{
|
||||
authName: "servername",
|
||||
server: &ServerInfo{Name: "servername", TLS: true},
|
||||
},
|
||||
{
|
||||
// OK to use PlainAuth on localhost without TLS
|
||||
authName: "localhost",
|
||||
server: &ServerInfo{Name: "localhost", TLS: false},
|
||||
},
|
||||
{
|
||||
// Also OK on non-TLS secured connections. The NoEnc mechanism is meant to allow
|
||||
// non-encrypted connections.
|
||||
authName: "servername",
|
||||
server: &ServerInfo{Name: "servername", Auth: []string{"PLAIN"}},
|
||||
},
|
||||
{
|
||||
authName: "servername",
|
||||
server: &ServerInfo{Name: "attacker", TLS: true},
|
||||
err: "wrong host name",
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
auth := PlainAuth("foo", "bar", "baz", tt.authName, true)
|
||||
_, _, err := auth.Start(tt.server)
|
||||
got := ""
|
||||
if err != nil {
|
||||
|
@ -283,7 +339,51 @@ func TestAuthLogin(t *testing.T) {
|
|||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
auth := LoginAuth("foo", "bar", tt.authName)
|
||||
auth := LoginAuth("foo", "bar", tt.authName, false)
|
||||
_, _, err := auth.Start(tt.server)
|
||||
got := ""
|
||||
if err != nil {
|
||||
got = err.Error()
|
||||
}
|
||||
if got != tt.err {
|
||||
t.Errorf("%d. got error = %q; want %q", i, got, tt.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthLoginNoEnc(t *testing.T) {
|
||||
tests := []struct {
|
||||
authName string
|
||||
server *ServerInfo
|
||||
err string
|
||||
}{
|
||||
{
|
||||
authName: "servername",
|
||||
server: &ServerInfo{Name: "servername", TLS: true},
|
||||
},
|
||||
{
|
||||
// OK to use LoginAuth on localhost without TLS
|
||||
authName: "localhost",
|
||||
server: &ServerInfo{Name: "localhost", TLS: false},
|
||||
},
|
||||
{
|
||||
// Also OK on non-TLS secured connections. The NoEnc mechanism is meant to allow
|
||||
// non-encrypted connections.
|
||||
authName: "servername",
|
||||
server: &ServerInfo{Name: "servername", Auth: []string{"LOGIN"}},
|
||||
},
|
||||
{
|
||||
authName: "servername",
|
||||
server: &ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}},
|
||||
},
|
||||
{
|
||||
authName: "servername",
|
||||
server: &ServerInfo{Name: "attacker", TLS: true},
|
||||
err: "wrong host name",
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
auth := LoginAuth("foo", "bar", tt.authName, true)
|
||||
_, _, err := auth.Start(tt.server)
|
||||
got := ""
|
||||
if err != nil {
|
||||
|
@ -317,7 +417,11 @@ func TestXOAuth2OK(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("NewClient: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
defer func() {
|
||||
if err := c.Close(); err != nil {
|
||||
t.Errorf("failed to close client: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
auth := XOAuth2Auth("user", "token")
|
||||
err = c.Auth(auth)
|
||||
|
@ -355,7 +459,11 @@ func TestXOAuth2Error(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("NewClient: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
defer func() {
|
||||
if err := c.Close(); err != nil {
|
||||
t.Errorf("failed to close client: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
auth := XOAuth2Auth("user", "token")
|
||||
err = c.Auth(auth)
|
||||
|
@ -707,7 +815,7 @@ func TestBasic(t *testing.T) {
|
|||
// fake TLS so authentication won't complain
|
||||
c.tls = true
|
||||
c.serverName = "smtp.google.com"
|
||||
if err := c.Auth(PlainAuth("", "user", "pass", "smtp.google.com")); err != nil {
|
||||
if err := c.Auth(PlainAuth("", "user", "pass", "smtp.google.com", false)); err != nil {
|
||||
t.Fatalf("AUTH failed: %s", err)
|
||||
}
|
||||
|
||||
|
@ -1111,6 +1219,32 @@ func TestClient_SetLogger(t *testing.T) {
|
|||
c.logger.Debugf(log.Log{Direction: log.DirServerToClient, Format: "%s", Messages: []interface{}{"test"}})
|
||||
}
|
||||
|
||||
func TestClient_SetLogAuthData(t *testing.T) {
|
||||
server := strings.Join(strings.Split(newClientServer, "\n"), "\r\n")
|
||||
|
||||
var cmdbuf strings.Builder
|
||||
bcmdbuf := bufio.NewWriter(&cmdbuf)
|
||||
out := func() string {
|
||||
if err := bcmdbuf.Flush(); err != nil {
|
||||
t.Errorf("failed to flush: %s", err)
|
||||
}
|
||||
return cmdbuf.String()
|
||||
}
|
||||
var fake faker
|
||||
fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
|
||||
c, err := NewClient(fake, "fake.host")
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient: %v\n(after %v)", err, out())
|
||||
}
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
}()
|
||||
c.SetLogAuthData()
|
||||
if !c.logAuthData {
|
||||
t.Error("Expected logAuthData to be true but received false")
|
||||
}
|
||||
}
|
||||
|
||||
var newClientServer = `220 hello world
|
||||
250-mx.google.com at your service
|
||||
250-SIZE 35651584
|
||||
|
@ -1252,7 +1386,7 @@ func TestHello(t *testing.T) {
|
|||
case 3:
|
||||
c.tls = true
|
||||
c.serverName = "smtp.google.com"
|
||||
err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com"))
|
||||
err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com", false))
|
||||
case 4:
|
||||
err = c.Mail("test@example.com")
|
||||
case 5:
|
||||
|
@ -1497,7 +1631,7 @@ func TestSendMailWithAuth(t *testing.T) {
|
|||
}
|
||||
}()
|
||||
|
||||
err = SendMail(l.Addr().String(), PlainAuth("", "user", "pass", "smtp.google.com"), "test@example.com", []string{"other@example.com"}, []byte(strings.Replace(`From: test@example.com
|
||||
err = SendMail(l.Addr().String(), PlainAuth("", "user", "pass", "smtp.google.com", false), "test@example.com", []string{"other@example.com"}, []byte(strings.Replace(`From: test@example.com
|
||||
To: other@example.com
|
||||
Subject: SendMail test
|
||||
|
||||
|
@ -1505,6 +1639,7 @@ SendMail is working for me.
|
|||
`, "\n", "\r\n", -1)))
|
||||
if err == nil {
|
||||
t.Error("SendMail: Server doesn't support AUTH, expected to get an error, but got none ")
|
||||
return
|
||||
}
|
||||
if err.Error() != "smtp: server doesn't support AUTH" {
|
||||
t.Errorf("Expected: smtp: server doesn't support AUTH, got: %s", err)
|
||||
|
@ -1532,7 +1667,7 @@ func TestAuthFailed(t *testing.T) {
|
|||
|
||||
c.tls = true
|
||||
c.serverName = "smtp.google.com"
|
||||
err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com"))
|
||||
err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com", false))
|
||||
|
||||
if err == nil {
|
||||
t.Error("Auth: expected error; got none")
|
||||
|
@ -2137,7 +2272,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 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
|
||||
// go-mail client_test.go
|
||||
type testSCRAMSMTPServer struct {
|
||||
|
|
Loading…
Reference in a new issue