Compare commits

...

41 commits

Author SHA1 Message Date
Michael Fuchs
3e25812ef3
Merge faffc025cb into c47f08dc7f 2024-10-27 08:42:20 +00:00
c47f08dc7f
Merge pull request #346 from wneessen/dependabot/github_actions/actions/setup-go-5.1.0
Bump actions/setup-go from 5.0.2 to 5.1.0
2024-10-25 16:32:48 +02:00
dependabot[bot]
87c0575dd4
Bump actions/setup-go from 5.0.2 to 5.1.0
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5.0.2 to 5.1.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](0a12ed9d6a...41dfa10bad)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-25 13:39:33 +00:00
cec7e38332
Merge pull request #345 from wneessen/dependabot/github_actions/github/codeql-action-3.27.0
Bump github/codeql-action from 3.26.13 to 3.27.0
2024-10-23 15:32:09 +02:00
dependabot[bot]
9ad77012e3
Bump github/codeql-action from 3.26.13 to 3.27.0
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.13 to 3.27.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](f779452ac5...662472033e)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-23 13:16:56 +00:00
55e5a1536e
Merge pull request #344 from wneessen/feature/342_allow-bypass-plain-auth-tls-check
Allow unencrypted PLAIN and LOGIN smtp authentication
2024-10-22 17:39:04 +02:00
c7d0a03ddc
Change error log to debug log in client_test.go
Updated the log level from error to debug for the client.Close() call failure in client_test.go. This change helps reduce noise in test output when the server connection fails.
2024-10-22 16:21:09 +02:00
f79c1b8ebe
Add fig.StringUnmarshaler support for LOGIN-NOENC and PLAIN-NOENC authentication methods 2024-10-22 16:09:12 +02:00
df1a141368
Handle client close errors in SMTP tests
Update defer statements to log errors if client fails to close in smtp_test.go. Additionally, add a return statement to avoid further errors after a failed SendMail operation.
2024-10-22 16:02:43 +02:00
e2ed5b747a
Add tests for PlainAuth and LoginAuth without encryption
Introduced new test functions TestAuthPlainNoEnc and TestAuthLoginNoEnc in smtp_test.go to verify behaviors of PlainAuth and LoginAuth without TLS encryption. These tests ensure that authentication mechanisms handle non-encrypted and diverse server configurations correctly.
2024-10-22 15:50:18 +02:00
2bd950469a
Add 'skipTLS' parameter to auth functions in tests
Updated PlainAuth and LoginAuth calls in smtp_test.go and example_test.go to include a 'skipTLS' boolean parameter. This ensures consistent function signatures throughout the test cases and examples.
2024-10-22 15:44:40 +02:00
3c29f68cc1
Add support for unsecured SMTP LOGIN auth
Implemented an option to allow SMTP LOGIN authentication over unencrypted channels by introducing a new `SMTPAuthLoginNoEnc` type. Updated relevant functions and tests to handle the new parameter for unsecured authentication.
2024-10-22 15:38:51 +02:00
f5531eae14
Add support for PLAIN authentication without encryption
Implemented a new SMTPAuthPlainNoEnc option to allow PLAIN authentication over unencrypted connections. Refactored the PlainAuth function to accept an additional allowUnencryptedAuth parameter. Updated relevant tests to cover the new authentication method.
2024-10-22 15:30:15 +02:00
91caf200ec
Merge pull request #343 from wneessen/dependabot/github_actions/actions/dependency-review-action-4.3.5
Bump actions/dependency-review-action from 4.3.4 to 4.3.5
2024-10-22 15:13:07 +02:00
dependabot[bot]
63e6fc882d
Bump actions/dependency-review-action from 4.3.4 to 4.3.5
Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4.3.4 to 4.3.5.
- [Release notes](https://github.com/actions/dependency-review-action/releases)
- [Commits](5a2ce3f5b9...a6993e2c61)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-22 13:10:46 +00:00
957cd8e0ca
Merge pull request #341 from wneessen/fig-support
Add fig.StringUnmarshaler support for SMTPAuthType
2024-10-21 22:51:46 +02:00
09133ef2a4
Add test for invalid SMTPAuthType
Introduce a new test case to verify that `UnmarshalString` fails when given an invalid string. This ensures the robustness of the error handling in the `SMTPAuthType` unmarshalling process.
2024-10-21 22:34:47 +02:00
bf44fd2ad1
Add SPDX license headers to auth_test.go
Included the SPDX license identifiers for copyright and license type at the beginning of the auth_test.go file. This ensures clear licensing information is provided and helps with automated license compliance.
2024-10-21 22:26:30 +02:00
7bebdda27c
Add fig.StringUnmarshaler support for SMTPAuthType
Implement the fig.StringUnmarshaler interface for SMTPAuthType to map strings to corresponding authentication types. Added comprehensive unit tests to ensure correct functionality for all supported auth type strings.
2024-10-21 22:24:21 +02: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
20 changed files with 555 additions and 91 deletions

View file

@ -50,7 +50,7 @@ jobs:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
- name: Setup go - name: Setup go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
- name: Install sendmail - name: Install sendmail

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@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
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@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
# 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@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0

View file

@ -28,4 +28,4 @@ jobs:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4 uses: actions/dependency-review-action@a6993e2c61fd5dc440b409aa1d6904921c5e1894 # v4.3.5

View file

@ -24,7 +24,7 @@ jobs:
with: with:
egress-policy: audit egress-policy: audit
- uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with: with:
go-version: '1.23' go-version: '1.23'
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0

View file

@ -37,7 +37,7 @@ jobs:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
- name: Setup go - name: Setup go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
- name: Run Tests - name: Run Tests

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@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
with: with:
sarif_file: results.sarif sarif_file: results.sarif

View file

@ -37,7 +37,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with: with:
go-version: '1.23' go-version: '1.23'

83
auth.go
View file

@ -4,7 +4,11 @@
package mail package mail
import "errors" import (
"errors"
"fmt"
"strings"
)
// SMTPAuthType is a type wrapper for a string type. It represents the type of SMTP authentication // SMTPAuthType is a type wrapper for a string type. It represents the type of SMTP authentication
// mechanism to be used. // 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 // 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.
// //
@ -44,20 +48,53 @@ const (
// https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00 // https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
SMTPAuthLogin SMTPAuthType = "LOGIN" 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 // 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.
// //
// https://datatracker.ietf.org/doc/html/rfc4616/ // https://datatracker.ietf.org/doc/html/rfc4616/
SMTPAuthPlain SMTPAuthType = "PLAIN" 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. // SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism.
// https://developers.google.com/gmail/imap/xoauth2-protocol // https://developers.google.com/gmail/imap/xoauth2-protocol
SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2" SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2"
@ -76,7 +113,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 +132,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"
@ -134,3 +171,37 @@ var (
// authentication type. // authentication type.
ErrSCRAMSHA256PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256-PLUS") 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
View 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")
}
})
}

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")
@ -1051,12 +1096,22 @@ func (c *Client) auth() error {
if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) { if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) {
return ErrPlainAuthNotSupported 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: case SMTPAuthLogin:
if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) { if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) {
return ErrLoginAuthNotSupported 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: case SMTPAuthCramMD5:
if !strings.Contains(smtpAuthType, string(SMTPAuthCramMD5)) { if !strings.Contains(smtpAuthType, string(SMTPAuthCramMD5)) {
return ErrCramMD5AuthNotSupported return ErrCramMD5AuthNotSupported

View file

@ -110,7 +110,7 @@ func TestNewClientWithOptions(t *testing.T) {
{"WithSMTPAuth()", WithSMTPAuth(SMTPAuthLogin), false}, {"WithSMTPAuth()", WithSMTPAuth(SMTPAuthLogin), false},
{ {
"WithSMTPAuthCustom()", "WithSMTPAuthCustom()",
WithSMTPAuthCustom(smtp.PlainAuth("", "", "", "")), WithSMTPAuthCustom(smtp.PlainAuth("", "", "", "", false)),
false, false,
}, },
{"WithUsername()", WithUsername("test"), false}, {"WithUsername()", WithUsername("test"), false},
@ -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 {
@ -587,8 +605,8 @@ func TestSetSMTPAuthCustom(t *testing.T) {
sf bool sf bool
}{ }{
{"SMTPAuth: CRAM-MD5", smtp.CRAMMD5Auth("", ""), "CRAM-MD5", false}, {"SMTPAuth: CRAM-MD5", smtp.CRAMMD5Auth("", ""), "CRAM-MD5", false},
{"SMTPAuth: LOGIN", smtp.LoginAuth("", "", ""), "LOGIN", false}, {"SMTPAuth: LOGIN", smtp.LoginAuth("", "", "", false), "LOGIN", false},
{"SMTPAuth: PLAIN", smtp.PlainAuth("", "", "", ""), "PLAIN", false}, {"SMTPAuth: PLAIN", smtp.PlainAuth("", "", "", "", false), "PLAIN", false},
} }
si := smtp.ServerInfo{TLS: true} si := smtp.ServerInfo{TLS: true}
for _, tt := range tests { for _, tt := range tests {
@ -789,7 +807,7 @@ func TestClient_DialWithContextInvalidAuth(t *testing.T) {
} }
c.user = "invalid" c.user = "invalid"
c.pass = "invalid" c.pass = "invalid"
c.SetSMTPAuthCustom(smtp.LoginAuth("invalid", "invalid", "invalid")) c.SetSMTPAuthCustom(smtp.LoginAuth("invalid", "invalid", "invalid", false))
ctx := context.Background() ctx := context.Background()
if err = c.DialWithContext(ctx); err == nil { if err = c.DialWithContext(ctx); err == nil {
t.Errorf("dial succeeded but was supposed to fail") 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 // 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 {
@ -1862,7 +1955,7 @@ func TestClient_DialSendConcurrent_local(t *testing.T) {
wg.Wait() wg.Wait()
if err = client.Close(); err != nil { 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
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

@ -10,9 +10,10 @@ import (
// loginAuth is the type that satisfies the Auth interface for the "SMTP LOGIN" auth // loginAuth is the type that satisfies the Auth interface for the "SMTP LOGIN" auth
type loginAuth struct { type loginAuth struct {
username, password string username, password string
host string host string
respStep uint8 respStep uint8
allowUnencryptedAuth bool
} }
// LoginAuth returns an [Auth] that implements the LOGIN authentication // 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 // 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
// or is connected to localhost. Otherwise authentication will fail with an // or is connected to localhost. Otherwise authentication will fail with an
// error, without sending the credentials. // error, without sending the credentials.
func LoginAuth(username, password, host string) Auth { func LoginAuth(username, password, host string, allowUnEnc bool) Auth {
return &loginAuth{username, password, host, 0} return &loginAuth{username, password, host, 0, allowUnEnc}
} }
// Start begins the SMTP authentication process by validating server's TLS status and hostname. // 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. // In particular, it doesn't matter if the server advertises LOGIN auth.
// That might just be the attacker saying // That might just be the attacker saying
// "it's ok, you can trust me with your password." // "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 return "", nil, ErrUnencrypted
} }
if server.Name != a.host { if server.Name != a.host {

View file

@ -17,6 +17,7 @@ package smtp
type plainAuth struct { type plainAuth struct {
identity, username, password string identity, username, password string
host string host string
allowUnencryptedAuth bool
} }
// PlainAuth returns an [Auth] that implements the PLAIN authentication // 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 // PlainAuth will only send the credentials if the connection is using TLS
// or is connected to localhost. Otherwise authentication will fail with an // or is connected to localhost. Otherwise authentication will fail with an
// error, without sending the credentials. // error, without sending the credentials.
func PlainAuth(identity, username, password, host string) Auth { func PlainAuth(identity, username, password, host string, allowUnEnc bool) Auth {
return &plainAuth{identity, username, password, host} return &plainAuth{identity, username, password, host, allowUnEnc}
} }
func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) { 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. // In particular, it doesn't matter if the server advertises PLAIN auth.
// That might just be the attacker saying // That might just be the attacker saying
// "it's ok, you can trust me with your password." // "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 return "", nil, ErrUnencrypted
} }
if server.Name != a.host { if server.Name != a.host {

View file

@ -67,7 +67,7 @@ var (
func ExamplePlainAuth() { func ExamplePlainAuth() {
// hostname is used by PlainAuth to validate the TLS certificate. // hostname is used by PlainAuth to validate the TLS certificate.
hostname := "mail.example.com" 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) err := smtp.SendMail(hostname+":25", auth, from, recipients, msg)
if err != nil { if err != nil {
@ -77,7 +77,7 @@ func ExamplePlainAuth() {
func ExampleSendMail() { func ExampleSendMail() {
// Set up authentication information. // 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, // Connect to the server, authenticate, set the sender and recipient,
// and send the email all in one step. // and send the email all in one step.

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

@ -50,7 +50,7 @@ type authTest struct {
var authTests = []authTest{ var authTests = []authTest{
{ {
PlainAuth("", "user", "pass", "testserver"), PlainAuth("", "user", "pass", "testserver", false),
[]string{}, []string{},
"PLAIN", "PLAIN",
[]string{"\x00user\x00pass"}, []string{"\x00user\x00pass"},
@ -58,7 +58,15 @@ var authTests = []authTest{
false, 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{}, []string{},
"PLAIN", "PLAIN",
[]string{"foo\x00bar\x00baz"}, []string{"foo\x00bar\x00baz"},
@ -66,7 +74,7 @@ var authTests = []authTest{
false, false,
}, },
{ {
PlainAuth("foo", "bar", "baz", "testserver"), PlainAuth("foo", "bar", "baz", "testserver", false),
[]string{"foo"}, []string{"foo"},
"PLAIN", "PLAIN",
[]string{"foo\x00bar\x00baz", ""}, []string{"foo\x00bar\x00baz", ""},
@ -74,7 +82,7 @@ var authTests = []authTest{
false, false,
}, },
{ {
LoginAuth("user", "pass", "testserver"), LoginAuth("user", "pass", "testserver", false),
[]string{"Username:", "Password:"}, []string{"Username:", "Password:"},
"LOGIN", "LOGIN",
[]string{"", "user", "pass"}, []string{"", "user", "pass"},
@ -82,7 +90,15 @@ var authTests = []authTest{
false, 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"}, []string{"User Name\x00", "Password\x00"},
"LOGIN", "LOGIN",
[]string{"", "user", "pass"}, []string{"", "user", "pass"},
@ -90,7 +106,7 @@ var authTests = []authTest{
false, false,
}, },
{ {
LoginAuth("user", "pass", "testserver"), LoginAuth("user", "pass", "testserver", false),
[]string{"Invalid", "Invalid:"}, []string{"Invalid", "Invalid:"},
"LOGIN", "LOGIN",
[]string{"", "user", "pass"}, []string{"", "user", "pass"},
@ -98,7 +114,7 @@ var authTests = []authTest{
false, false,
}, },
{ {
LoginAuth("user", "pass", "testserver"), LoginAuth("user", "pass", "testserver", false),
[]string{"Invalid", "Invalid:", "Too many"}, []string{"Invalid", "Invalid:", "Too many"},
"LOGIN", "LOGIN",
[]string{"", "user", "pass", ""}, []string{"", "user", "pass", ""},
@ -237,7 +253,47 @@ func TestAuthPlain(t *testing.T) {
}, },
} }
for i, tt := range tests { 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) _, _, err := auth.Start(tt.server)
got := "" got := ""
if err != nil { if err != nil {
@ -283,7 +339,51 @@ func TestAuthLogin(t *testing.T) {
}, },
} }
for i, tt := range tests { 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) _, _, err := auth.Start(tt.server)
got := "" got := ""
if err != nil { if err != nil {
@ -317,7 +417,11 @@ func TestXOAuth2OK(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("NewClient: %v", err) 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") auth := XOAuth2Auth("user", "token")
err = c.Auth(auth) err = c.Auth(auth)
@ -355,7 +459,11 @@ func TestXOAuth2Error(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("NewClient: %v", err) 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") auth := XOAuth2Auth("user", "token")
err = c.Auth(auth) err = c.Auth(auth)
@ -707,7 +815,7 @@ func TestBasic(t *testing.T) {
// fake TLS so authentication won't complain // fake TLS so authentication won't complain
c.tls = true c.tls = true
c.serverName = "smtp.google.com" 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) 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"}}) 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
@ -1252,7 +1386,7 @@ func TestHello(t *testing.T) {
case 3: case 3:
c.tls = true c.tls = true
c.serverName = "smtp.google.com" 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: case 4:
err = c.Mail("test@example.com") err = c.Mail("test@example.com")
case 5: 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 To: other@example.com
Subject: SendMail test Subject: SendMail test
@ -1505,6 +1639,7 @@ SendMail is working for me.
`, "\n", "\r\n", -1))) `, "\n", "\r\n", -1)))
if err == nil { if err == nil {
t.Error("SendMail: Server doesn't support AUTH, expected to get an error, but got none ") 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" { if err.Error() != "smtp: server doesn't support AUTH" {
t.Errorf("Expected: smtp: server doesn't support AUTH, got: %s", err) 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.tls = true
c.serverName = "smtp.google.com" 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 { if err == nil {
t.Error("Auth: expected error; got none") 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. // 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 {