From 0ea963185534c9cfa2f1d329257f3f56a5358e80 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 16 Jul 2024 13:07:20 +0200 Subject: [PATCH 001/181] Refactor error handling in message delivery process The error handling process in sending messages has been refactored for better accuracy and efficiency. Instead of immediately joining errors and returning, they are now collected in a slice and joined in a deferred function to ensure all errors are captured before being returned. Furthermore, the SendError structure has been updated to include a reference to the affected message, providing better context for each error. --- client_120.go | 38 +++++++++++++++++++++++--------------- senderror.go | 9 +++++---- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/client_120.go b/client_120.go index ed80fee..3a867d9 100644 --- a/client_120.go +++ b/client_120.go @@ -18,25 +18,33 @@ func (c *Client) Send(messages ...*Msg) (returnErr error) { returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} return } - for _, message := range messages { + + var errs []error + defer func() { + returnErr = errors.Join(errs...) + }() + + for msgid, message := range messages { message.sendError = nil if message.encoding == NoEncoding { if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok { message.sendError = &SendError{Reason: ErrNoUnencoded, isTemp: false} - returnErr = errors.Join(returnErr, message.sendError) + errs = append(errs, message.sendError) continue } } from, err := message.GetSender(false) if err != nil { - message.sendError = &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err)} - returnErr = errors.Join(returnErr, message.sendError) + message.sendError = &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err), + affectedMsg: messages[msgid]} + errs = append(errs, message.sendError) continue } rcpts, err := message.GetRecipients() if err != nil { - message.sendError = &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err)} - returnErr = errors.Join(returnErr, message.sendError) + message.sendError = &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err), + affectedMsg: messages[msgid]} + errs = append(errs, message.sendError) continue } @@ -47,9 +55,9 @@ func (c *Client) Send(messages ...*Msg) (returnErr error) { } if err = c.smtpClient.Mail(from); err != nil { message.sendError = &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)} - returnErr = errors.Join(returnErr, message.sendError) + errs = append(errs, message.sendError) if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { - returnErr = errors.Join(returnErr, resetSendErr) + errs = append(errs, resetSendErr) } continue } @@ -70,40 +78,40 @@ func (c *Client) Send(messages ...*Msg) (returnErr error) { } if failed { if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { - returnErr = errors.Join(returnErr, resetSendErr) + errs = append(errs, resetSendErr) } message.sendError = rcptSendErr - returnErr = errors.Join(returnErr, message.sendError) + errs = append(errs, message.sendError) continue } writer, err := c.smtpClient.Data() if err != nil { message.sendError = &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)} - returnErr = errors.Join(returnErr, message.sendError) + errs = append(errs, message.sendError) continue } _, err = message.WriteTo(writer) if err != nil { message.sendError = &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)} - returnErr = errors.Join(returnErr, message.sendError) + errs = append(errs, message.sendError) continue } message.isDelivered = true if err = writer.Close(); err != nil { message.sendError = &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)} - returnErr = errors.Join(returnErr, message.sendError) + errs = append(errs, message.sendError) continue } if err = c.Reset(); err != nil { message.sendError = &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)} - returnErr = errors.Join(returnErr, message.sendError) + errs = append(errs, message.sendError) continue } if err = c.checkConn(); err != nil { message.sendError = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} - returnErr = errors.Join(returnErr, message.sendError) + errs = append(errs, message.sendError) } } diff --git a/senderror.go b/senderror.go index dfe7502..72500cd 100644 --- a/senderror.go +++ b/senderror.go @@ -56,10 +56,11 @@ const ( // SendError is an error wrapper for delivery errors of the Msg type SendError struct { - Reason SendErrReason - isTemp bool - errlist []error - rcpt []string + affectedMsg *Msg + errlist []error + isTemp bool + rcpt []string + Reason SendErrReason } // SendErrReason represents a comparable reason on why the delivery failed From 6d9829776a6eb08e5aed28c8fe309d4bd8ee0ba3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:09:18 +0000 Subject: [PATCH 002/181] Bump step-security/harden-runner from 2.9.1 to 2.10.1 Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.9.1 to 2.10.1. - [Release notes](https://github.com/step-security/harden-runner/releases) - [Commits](https://github.com/step-security/harden-runner/compare/5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde...91182cccc01eb5e619899d80e4e971d6181294a7) --- updated-dependencies: - dependency-name: step-security/harden-runner dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/codecov.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/golangci-lint.yml | 2 +- .github/workflows/govulncheck.yml | 2 +- .github/workflows/reuse.yml | 2 +- .github/workflows/scorecards.yml | 2 +- .github/workflows/sonarqube.yml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 41a6c27..2178e32 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -39,7 +39,7 @@ jobs: go: ['1.20', '1.21', '1.22', '1.23'] steps: - name: Harden Runner - uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index caab6ba..3f749a2 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -45,7 +45,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 13ad6e7..9c8db47 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 8604469..5c08b40 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml index 8b1693d..aecfaaa 100644 --- a/.github/workflows/govulncheck.yml +++ b/.github/workflows/govulncheck.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit - name: Run govulncheck diff --git a/.github/workflows/reuse.yml b/.github/workflows/reuse.yml index 1897833..04fd414 100644 --- a/.github/workflows/reuse.yml +++ b/.github/workflows/reuse.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 2e0f045..37522c2 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 1c77858..df060c3 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit From 68109ed40d40ec64bb4314e85b290c94e76b236a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 13:46:00 +0000 Subject: [PATCH 003/181] Bump github/codeql-action from 3.26.6 to 3.26.7 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.6 to 3.26.7. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/4dd16135b69a43b6c8efb853346f8437d92d3c93...8214744c546c1e5c8f03dde8fab3a7353211988d) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 3f749a2..64049e7 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -54,7 +54,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 + uses: github/codeql-action/init@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 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@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 + uses: github/codeql-action/autobuild@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 # ℹī¸ 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@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 + uses: github/codeql-action/analyze@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 37522c2..1f11f2a 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -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@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 + uses: github/codeql-action/upload-sarif@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 with: sarif_file: results.sarif From ee726487f17860be9e40b1bf6b0ee0123e84a503 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 18 Sep 2024 11:06:48 +0200 Subject: [PATCH 004/181] Refactor message sending logic for better modularity Extracted message sending functionality into a new helper function `sendSingleMsg`. This improves the code readability and maintainability by reducing the complexity of the main loop and encapsulating error handling per message. --- client_120.go | 167 ++++++++++++++++++++++++-------------------------- 1 file changed, 79 insertions(+), 88 deletions(-) diff --git a/client_120.go b/client_120.go index 3a867d9..60e8e3c 100644 --- a/client_120.go +++ b/client_120.go @@ -24,96 +24,87 @@ func (c *Client) Send(messages ...*Msg) (returnErr error) { returnErr = errors.Join(errs...) }() - for msgid, message := range messages { - message.sendError = nil - if message.encoding == NoEncoding { - if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok { - message.sendError = &SendError{Reason: ErrNoUnencoded, isTemp: false} - errs = append(errs, message.sendError) - continue - } - } - from, err := message.GetSender(false) - if err != nil { - message.sendError = &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: messages[msgid]} - errs = append(errs, message.sendError) - continue - } - rcpts, err := message.GetRecipients() - if err != nil { - message.sendError = &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: messages[msgid]} - errs = append(errs, message.sendError) - continue - } - - if c.dsn { - if c.dsnmrtype != "" { - c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype)) - } - } - if err = c.smtpClient.Mail(from); err != nil { - message.sendError = &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)} - errs = append(errs, message.sendError) - if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { - errs = append(errs, resetSendErr) - } - continue - } - failed := false - rcptSendErr := &SendError{} - rcptSendErr.errlist = make([]error, 0) - rcptSendErr.rcpt = make([]string, 0) - rcptNotifyOpt := strings.Join(c.dsnrntype, ",") - c.smtpClient.SetDSNRcptNotifyOption(rcptNotifyOpt) - for _, rcpt := range rcpts { - if err = c.smtpClient.Rcpt(rcpt); err != nil { - rcptSendErr.Reason = ErrSMTPRcptTo - rcptSendErr.errlist = append(rcptSendErr.errlist, err) - rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt) - rcptSendErr.isTemp = isTempError(err) - failed = true - } - } - if failed { - if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { - errs = append(errs, resetSendErr) - } - message.sendError = rcptSendErr - errs = append(errs, message.sendError) - continue - } - writer, err := c.smtpClient.Data() - if err != nil { - message.sendError = &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)} - errs = append(errs, message.sendError) - continue - } - _, err = message.WriteTo(writer) - if err != nil { - message.sendError = &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)} - errs = append(errs, message.sendError) - continue - } - message.isDelivered = true - - if err = writer.Close(); err != nil { - message.sendError = &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)} - errs = append(errs, message.sendError) - continue - } - - if err = c.Reset(); err != nil { - message.sendError = &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)} - errs = append(errs, message.sendError) - continue - } - if err = c.checkConn(); err != nil { - message.sendError = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} - errs = append(errs, message.sendError) + for _, message := range messages { + if sendErr := c.sendSingleMsg(message); sendErr != nil { + message.sendError = sendErr + errs = append(errs, sendErr) } } return } + +// sendSingleMsg sends out a single message and returns an error if the transmission/delivery fails. +// It is invoked by the public Send methods +func (c *Client) sendSingleMsg(message *Msg) error { + if message.encoding == NoEncoding { + if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok { + return &SendError{Reason: ErrNoUnencoded, isTemp: false} + } + } + from, err := message.GetSender(false) + if err != nil { + return &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err), + affectedMsg: message} + } + rcpts, err := message.GetRecipients() + if err != nil { + return &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err), + affectedMsg: message} + } + + if c.dsn { + if c.dsnmrtype != "" { + c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype)) + } + } + if err = c.smtpClient.Mail(from); err != nil { + retError := &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)} + if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { + retError.errlist = append(retError.errlist, resetSendErr) + } + return retError + } + failed := false + rcptSendErr := &SendError{} + rcptSendErr.errlist = make([]error, 0) + rcptSendErr.rcpt = make([]string, 0) + rcptNotifyOpt := strings.Join(c.dsnrntype, ",") + c.smtpClient.SetDSNRcptNotifyOption(rcptNotifyOpt) + for _, rcpt := range rcpts { + if err = c.smtpClient.Rcpt(rcpt); err != nil { + rcptSendErr.Reason = ErrSMTPRcptTo + rcptSendErr.errlist = append(rcptSendErr.errlist, err) + rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt) + rcptSendErr.isTemp = isTempError(err) + failed = true + } + } + if failed { + if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { + rcptSendErr.errlist = append(rcptSendErr.errlist, resetSendErr) + } + return rcptSendErr + } + writer, err := c.smtpClient.Data() + if err != nil { + return &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)} + } + _, err = message.WriteTo(writer) + if err != nil { + return &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)} + } + message.isDelivered = true + + if err = writer.Close(); err != nil { + return &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)} + } + + if err = c.Reset(); err != nil { + return &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)} + } + if err = c.checkConn(); err != nil { + return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} + } + return nil +} From 8ee37abca2f15097e1a8315f2ee94b4cb0a50478 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 18 Sep 2024 12:29:42 +0200 Subject: [PATCH 005/181] Refactor error handling in message sending loop Changed from range over messages to range with index to correctly update sendError field in the original messages slice. This prevents shadowing issues and ensures proper error logging for each message. --- client_120.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client_120.go b/client_120.go index 60e8e3c..757a951 100644 --- a/client_120.go +++ b/client_120.go @@ -24,9 +24,9 @@ func (c *Client) Send(messages ...*Msg) (returnErr error) { returnErr = errors.Join(errs...) }() - for _, message := range messages { + for id, message := range messages { if sendErr := c.sendSingleMsg(message); sendErr != nil { - message.sendError = sendErr + messages[id].sendError = sendErr errs = append(errs, sendErr) } } From 2e7156182abd1bfc5127909cee86747689131a03 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 19 Sep 2024 10:35:32 +0200 Subject: [PATCH 006/181] Rename variable 'failed' to 'hasError' for clarity Renamed the variable 'failed' to 'hasError' to better reflect its purpose and improve code readability. This change helps in understanding that the variable indicates the presence of an error rather than a generic failure. --- client_120.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client_120.go b/client_120.go index 757a951..8eb584a 100644 --- a/client_120.go +++ b/client_120.go @@ -65,7 +65,7 @@ func (c *Client) sendSingleMsg(message *Msg) error { } return retError } - failed := false + hasError := false rcptSendErr := &SendError{} rcptSendErr.errlist = make([]error, 0) rcptSendErr.rcpt = make([]string, 0) @@ -77,10 +77,10 @@ func (c *Client) sendSingleMsg(message *Msg) error { rcptSendErr.errlist = append(rcptSendErr.errlist, err) rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt) rcptSendErr.isTemp = isTempError(err) - failed = true + hasError = true } } - if failed { + if hasError { if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { rcptSendErr.errlist = append(rcptSendErr.errlist, resetSendErr) } From 277ae9be191262701ab8f5f81ca51c577674345e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 19 Sep 2024 10:56:01 +0200 Subject: [PATCH 007/181] Refactor message sending logic Consolidated the message sending logic into a single `sendSingleMsg` function to reduce duplication and improve code maintainability. This change simplifies the `Send` method in multiple Go version files by removing redundant code and calling the new helper function instead. --- client.go | 75 +++++++++++++++++++++++++++++++++++++++ client_119.go | 98 ++------------------------------------------------- client_120.go | 76 --------------------------------------- 3 files changed, 77 insertions(+), 172 deletions(-) diff --git a/client.go b/client.go index 1d175d1..56a82a3 100644 --- a/client.go +++ b/client.go @@ -787,3 +787,78 @@ func (c *Client) auth() error { } return nil } + +// sendSingleMsg sends out a single message and returns an error if the transmission/delivery fails. +// It is invoked by the public Send methods +func (c *Client) sendSingleMsg(message *Msg) error { + if message.encoding == NoEncoding { + if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok { + return &SendError{Reason: ErrNoUnencoded, isTemp: false} + } + } + from, err := message.GetSender(false) + if err != nil { + return &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err), + affectedMsg: message} + } + rcpts, err := message.GetRecipients() + if err != nil { + return &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err), + affectedMsg: message} + } + + if c.dsn { + if c.dsnmrtype != "" { + c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype)) + } + } + if err = c.smtpClient.Mail(from); err != nil { + retError := &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)} + if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { + retError.errlist = append(retError.errlist, resetSendErr) + } + return retError + } + hasError := false + rcptSendErr := &SendError{} + rcptSendErr.errlist = make([]error, 0) + rcptSendErr.rcpt = make([]string, 0) + rcptNotifyOpt := strings.Join(c.dsnrntype, ",") + c.smtpClient.SetDSNRcptNotifyOption(rcptNotifyOpt) + for _, rcpt := range rcpts { + if err = c.smtpClient.Rcpt(rcpt); err != nil { + rcptSendErr.Reason = ErrSMTPRcptTo + rcptSendErr.errlist = append(rcptSendErr.errlist, err) + rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt) + rcptSendErr.isTemp = isTempError(err) + hasError = true + } + } + if hasError { + if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { + rcptSendErr.errlist = append(rcptSendErr.errlist, resetSendErr) + } + return rcptSendErr + } + writer, err := c.smtpClient.Data() + if err != nil { + return &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)} + } + _, err = message.WriteTo(writer) + if err != nil { + return &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)} + } + message.isDelivered = true + + if err = writer.Close(); err != nil { + return &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)} + } + + if err = c.Reset(); err != nil { + return &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)} + } + if err = c.checkConn(); err != nil { + return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} + } + return nil +} diff --git a/client_119.go b/client_119.go index 52e4b3f..fb94d99 100644 --- a/client_119.go +++ b/client_119.go @@ -7,8 +7,6 @@ package mail -import "strings" - // Send sends out the mail message func (c *Client) Send(messages ...*Msg) error { if cerr := c.checkConn(); cerr != nil { @@ -16,101 +14,9 @@ func (c *Client) Send(messages ...*Msg) error { } var errs []*SendError for _, message := range messages { - message.sendError = nil - if message.encoding == NoEncoding { - if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok { - sendErr := &SendError{Reason: ErrNoUnencoded, isTemp: false} - message.sendError = sendErr - errs = append(errs, sendErr) - continue - } - } - from, err := message.GetSender(false) - if err != nil { - sendErr := &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err)} - message.sendError = sendErr + if sendErr := c.sendSingleMsg(message); sendErr != nil { + messages[id].sendError = sendErr errs = append(errs, sendErr) - continue - } - rcpts, err := message.GetRecipients() - if err != nil { - sendErr := &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err)} - message.sendError = sendErr - errs = append(errs, sendErr) - continue - } - - if c.dsn { - if c.dsnmrtype != "" { - c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype)) - } - } - if err = c.smtpClient.Mail(from); err != nil { - sendErr := &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)} - if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { - sendErr.errlist = append(sendErr.errlist, resetSendErr) - } - message.sendError = sendErr - errs = append(errs, sendErr) - continue - } - failed := false - rcptSendErr := &SendError{} - rcptSendErr.errlist = make([]error, 0) - rcptSendErr.rcpt = make([]string, 0) - rcptNotifyOpt := strings.Join(c.dsnrntype, ",") - c.smtpClient.SetDSNRcptNotifyOption(rcptNotifyOpt) - for _, rcpt := range rcpts { - if err = c.smtpClient.Rcpt(rcpt); err != nil { - rcptSendErr.Reason = ErrSMTPRcptTo - rcptSendErr.errlist = append(rcptSendErr.errlist, err) - rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt) - rcptSendErr.isTemp = isTempError(err) - failed = true - } - } - if failed { - if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { - rcptSendErr.errlist = append(rcptSendErr.errlist, err) - } - message.sendError = rcptSendErr - errs = append(errs, rcptSendErr) - continue - } - writer, err := c.smtpClient.Data() - if err != nil { - sendErr := &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)} - message.sendError = sendErr - errs = append(errs, sendErr) - continue - } - _, err = message.WriteTo(writer) - if err != nil { - sendErr := &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)} - message.sendError = sendErr - errs = append(errs, sendErr) - continue - } - message.isDelivered = true - - if err = writer.Close(); err != nil { - sendErr := &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)} - message.sendError = sendErr - errs = append(errs, sendErr) - continue - } - - if err = c.Reset(); err != nil { - sendErr := &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)} - message.sendError = sendErr - errs = append(errs, sendErr) - continue - } - if err = c.checkConn(); err != nil { - sendErr := &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} - message.sendError = sendErr - errs = append(errs, sendErr) - continue } } diff --git a/client_120.go b/client_120.go index 8eb584a..4f82aa7 100644 --- a/client_120.go +++ b/client_120.go @@ -9,7 +9,6 @@ package mail import ( "errors" - "strings" ) // Send sends out the mail message @@ -33,78 +32,3 @@ func (c *Client) Send(messages ...*Msg) (returnErr error) { return } - -// sendSingleMsg sends out a single message and returns an error if the transmission/delivery fails. -// It is invoked by the public Send methods -func (c *Client) sendSingleMsg(message *Msg) error { - if message.encoding == NoEncoding { - if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok { - return &SendError{Reason: ErrNoUnencoded, isTemp: false} - } - } - from, err := message.GetSender(false) - if err != nil { - return &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message} - } - rcpts, err := message.GetRecipients() - if err != nil { - return &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message} - } - - if c.dsn { - if c.dsnmrtype != "" { - c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype)) - } - } - if err = c.smtpClient.Mail(from); err != nil { - retError := &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)} - if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { - retError.errlist = append(retError.errlist, resetSendErr) - } - return retError - } - hasError := false - rcptSendErr := &SendError{} - rcptSendErr.errlist = make([]error, 0) - rcptSendErr.rcpt = make([]string, 0) - rcptNotifyOpt := strings.Join(c.dsnrntype, ",") - c.smtpClient.SetDSNRcptNotifyOption(rcptNotifyOpt) - for _, rcpt := range rcpts { - if err = c.smtpClient.Rcpt(rcpt); err != nil { - rcptSendErr.Reason = ErrSMTPRcptTo - rcptSendErr.errlist = append(rcptSendErr.errlist, err) - rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt) - rcptSendErr.isTemp = isTempError(err) - hasError = true - } - } - if hasError { - if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { - rcptSendErr.errlist = append(rcptSendErr.errlist, resetSendErr) - } - return rcptSendErr - } - writer, err := c.smtpClient.Data() - if err != nil { - return &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)} - } - _, err = message.WriteTo(writer) - if err != nil { - return &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)} - } - message.isDelivered = true - - if err = writer.Close(); err != nil { - return &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)} - } - - if err = c.Reset(); err != nil { - return &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)} - } - if err = c.checkConn(); err != nil { - return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} - } - return nil -} From 3bdb6f7ccab55f41fd6960b047459e60e02ef38c Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 19 Sep 2024 10:59:22 +0200 Subject: [PATCH 008/181] Refactor variable naming in Send method Renamed variable `cerr` to `err` for consistency. This improves readability and standardizes error variable naming within the method. --- client_119.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client_119.go b/client_119.go index fb94d99..8084913 100644 --- a/client_119.go +++ b/client_119.go @@ -9,8 +9,8 @@ package mail // Send sends out the mail message func (c *Client) Send(messages ...*Msg) error { - if cerr := c.checkConn(); cerr != nil { - return &SendError{Reason: ErrConnCheck, errlist: []error{cerr}, isTemp: isTempError(cerr)} + if err := c.checkConn(); err != nil { + return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} } var errs []*SendError for _, message := range messages { From 69e211682c4a49b03340322e27410ca9e3ea737d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 19 Sep 2024 11:46:53 +0200 Subject: [PATCH 009/181] Add GetMessageID method to Msg Introduced GetMessageID to retrieve the Message ID from the Msg --- msg.go | 11 +++++++++++ msg_test.go | 15 +++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/msg.go b/msg.go index a909d04..11d8ab1 100644 --- a/msg.go +++ b/msg.go @@ -476,6 +476,17 @@ func (m *Msg) SetMessageID() { m.SetMessageIDWithValue(messageID) } +// GetMessageID returns the message ID of the Msg as string value. If no message ID +// is set, an empty string will be returned +func (m *Msg) GetMessageID() string { + if msgidheader, ok := m.genHeader[HeaderMessageID]; ok { + if len(msgidheader) > 0 { + return msgidheader[0] + } + } + return "" +} + // SetMessageIDWithValue sets the message id for the mail func (m *Msg) SetMessageIDWithValue(messageID string) { m.SetGenHeader(HeaderMessageID, fmt.Sprintf("<%s>", messageID)) diff --git a/msg_test.go b/msg_test.go index 16cd196..0656ff2 100644 --- a/msg_test.go +++ b/msg_test.go @@ -805,6 +805,21 @@ func TestMsg_SetMessageIDRandomness(t *testing.T) { } } +func TestMsg_GetMessageID(t *testing.T) { + expected := "this.is.a.message.id" + msg := NewMsg() + msg.SetMessageIDWithValue(expected) + val := msg.GetMessageID() + if !strings.EqualFold(val, fmt.Sprintf("<%s>", expected)) { + t.Errorf("GetMessageID() failed. Expected: %s, got: %s", fmt.Sprintf("<%s>", expected), val) + } + msg.genHeader[HeaderMessageID] = nil + val = msg.GetMessageID() + if val != "" { + t.Errorf("GetMessageID() failed. Expected empty string, got: %s", val) + } +} + // TestMsg_FromFormat tests the FromFormat and EnvelopeFrom methods for the Msg object func TestMsg_FromFormat(t *testing.T) { tests := []struct { From 0239318d949d6412bae106ca90ce4ebf8846b0b3 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 19 Sep 2024 12:09:23 +0200 Subject: [PATCH 010/181] Add affectedMsg field to SendError struct Included the affected message in the SendError struct for better error tracking and debugging. This enhancement ensures that any errors encountered during the sending process can be directly associated with the specific message that caused them. --- client.go | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/client.go b/client.go index 56a82a3..f8519b4 100644 --- a/client.go +++ b/client.go @@ -793,7 +793,7 @@ func (c *Client) auth() error { func (c *Client) sendSingleMsg(message *Msg) error { if message.encoding == NoEncoding { if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok { - return &SendError{Reason: ErrNoUnencoded, isTemp: false} + return &SendError{Reason: ErrNoUnencoded, isTemp: false, affectedMsg: message} } } from, err := message.GetSender(false) @@ -813,14 +813,15 @@ func (c *Client) sendSingleMsg(message *Msg) error { } } if err = c.smtpClient.Mail(from); err != nil { - retError := &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)} + retError := &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err), + affectedMsg: message} if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { retError.errlist = append(retError.errlist, resetSendErr) } return retError } hasError := false - rcptSendErr := &SendError{} + rcptSendErr := &SendError{affectedMsg: message} rcptSendErr.errlist = make([]error, 0) rcptSendErr.rcpt = make([]string, 0) rcptNotifyOpt := strings.Join(c.dsnrntype, ",") @@ -842,23 +843,28 @@ func (c *Client) sendSingleMsg(message *Msg) error { } writer, err := c.smtpClient.Data() if err != nil { - return &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err)} + return &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err), + affectedMsg: message} } _, err = message.WriteTo(writer) if err != nil { - return &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err)} + return &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err), + affectedMsg: message} } message.isDelivered = true if err = writer.Close(); err != nil { - return &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)} + return &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err), + affectedMsg: message} } if err = c.Reset(); err != nil { - return &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)} + return &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err), + affectedMsg: message} } if err = c.checkConn(); err != nil { - return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} + return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err), + affectedMsg: message} } return nil } From f469ba977d299cb17e33fc1b0f025769648cb7f1 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 19 Sep 2024 12:12:48 +0200 Subject: [PATCH 011/181] Add affected message ID to error log Include the affected message ID in the error message to provide more context for debugging. This change ensures that each error log contains essential information about the specific message associated with the error. --- senderror.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/senderror.go b/senderror.go index 72500cd..494bfd3 100644 --- a/senderror.go +++ b/senderror.go @@ -93,6 +93,11 @@ func (e *SendError) Error() string { } } } + if e.affectedMsg != nil && e.affectedMsg.GetMessageID() != "" { + errMessage.WriteString(", affected message ID: ") + errMessage.WriteString(e.affectedMsg.GetMessageID()) + } + return errMessage.String() } From 508a2f2a6c02ce4e72c14db7259fbe910769e183 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 19 Sep 2024 15:21:17 +0200 Subject: [PATCH 012/181] Refactor connection handling in tests Replaced `getTestConnection` with `getTestConnectionNoTestPort` for tests that skip port configuration, improving connection setup flexibility. Added environment variable support for setting ports in `getTestClient` to streamline port configuration in tests. --- client_test.go | 55 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/client_test.go b/client_test.go index 0ad309a..efccea4 100644 --- a/client_test.go +++ b/client_test.go @@ -629,7 +629,7 @@ func TestClient_DialWithContext(t *testing.T) { // TestClient_DialWithContext_Fallback tests the Client.DialWithContext method with the fallback // port functionality func TestClient_DialWithContext_Fallback(t *testing.T) { - c, err := getTestConnection(true) + c, err := getTestConnectionNoTestPort(true) if err != nil { t.Skipf("failed to create test client: %s. Skipping tests", err) } @@ -1302,6 +1302,50 @@ func getTestConnection(auth bool) (*Client, error) { return c, nil } +// getTestConnectionNoTestPort takes environment variables (except the port) to establish a +// connection to a real SMTP server to test all functionality that requires a connection +func getTestConnectionNoTestPort(auth bool) (*Client, error) { + if os.Getenv("TEST_SKIP_ONLINE") != "" { + return nil, fmt.Errorf("env variable TEST_SKIP_ONLINE is set. Skipping online tests") + } + th := os.Getenv("TEST_HOST") + if th == "" { + return nil, fmt.Errorf("no TEST_HOST set") + } + sv := false + if sve := os.Getenv("TEST_TLS_SKIP_VERIFY"); sve != "" { + sv = true + } + c, err := NewClient(th) + if err != nil { + return c, err + } + c.tlsconfig.InsecureSkipVerify = sv + if auth { + st := os.Getenv("TEST_SMTPAUTH_TYPE") + if st != "" { + c.SetSMTPAuth(SMTPAuthType(st)) + } + u := os.Getenv("TEST_SMTPAUTH_USER") + if u != "" { + c.SetUsername(u) + } + p := os.Getenv("TEST_SMTPAUTH_PASS") + if p != "" { + c.SetPassword(p) + } + // We don't want to log authentication data in tests + c.SetDebugLog(false) + } + if err := c.DialWithContext(context.Background()); err != nil { + return c, fmt.Errorf("connection to test server failed: %w", err) + } + if err := c.Close(); err != nil { + return c, fmt.Errorf("disconnect from test server failed: %w", err) + } + return c, nil +} + // getTestClient takes environment variables to establish a client without connecting // to the SMTP server func getTestClient(auth bool) (*Client, error) { @@ -1357,7 +1401,14 @@ func getTestConnectionWithDSN(auth bool) (*Client, error) { if th == "" { return nil, fmt.Errorf("no TEST_HOST set") } - c, err := NewClient(th, WithDSN()) + tp := 25 + if tps := os.Getenv("TEST_PORT"); tps != "" { + tpi, err := strconv.Atoi(tps) + if err == nil { + tp = tpi + } + } + c, err := NewClient(th, WithDSN(), WithPort(tp)) if err != nil { return c, err } From 664b7299e66f712fca5db2fea87d556b113f486d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Sep 2024 13:44:59 +0000 Subject: [PATCH 013/181] Bump github/codeql-action from 3.26.7 to 3.26.8 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.7 to 3.26.8. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/8214744c546c1e5c8f03dde8fab3a7353211988d...294a9d92911152fe08befb9ec03e240add280cb3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 64049e7..cac000e 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -54,7 +54,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 + uses: github/codeql-action/init@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 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@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 + uses: github/codeql-action/autobuild@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 # ℹī¸ 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@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 + uses: github/codeql-action/analyze@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 1f11f2a..8bbf2c6 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -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@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 + uses: github/codeql-action/upload-sarif@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 with: sarif_file: results.sarif From fcbd202595a4611baa1621a684d9baf30512b923 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 10:30:30 +0200 Subject: [PATCH 014/181] Add test for IsTemp method on nil SendError This commit introduces a new test case to verify the behavior of the IsTemp method when called on a nil SendError instance. It ensures that the method returns false as expected, improving test coverage for edge cases. --- senderror_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/senderror_test.go b/senderror_test.go index e83df00..789b290 100644 --- a/senderror_test.go +++ b/senderror_test.go @@ -83,6 +83,13 @@ func TestSendError_IsTemp(t *testing.T) { } } +func TestSendError_IsTempNil(t *testing.T) { + var se *SendError + if se.IsTemp() { + t.Error("expected false on nil-senderror") + } +} + // returnSendError is a helper method to retunr a SendError with a specific reason func returnSendError(r SendErrReason, t bool) error { return &SendError{Reason: r, isTemp: t} From d400379e2f09747484d21b0c2c1be7267a5e5d58 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 10:31:19 +0200 Subject: [PATCH 015/181] Refactor error handling for SendError struct Reformat the construction of SendError objects for better readability. This improves the clarity and maintainability of the error handling code within the client.go file. --- client.go | 48 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/client.go b/client.go index f8519b4..fac9a34 100644 --- a/client.go +++ b/client.go @@ -798,13 +798,17 @@ func (c *Client) sendSingleMsg(message *Msg) error { } from, err := message.GetSender(false) if err != nil { - return &SendError{Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message} + return &SendError{ + Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err), + affectedMsg: message, + } } rcpts, err := message.GetRecipients() if err != nil { - return &SendError{Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message} + return &SendError{ + Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err), + affectedMsg: message, + } } if c.dsn { @@ -813,8 +817,10 @@ func (c *Client) sendSingleMsg(message *Msg) error { } } if err = c.smtpClient.Mail(from); err != nil { - retError := &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message} + retError := &SendError{ + Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err), + affectedMsg: message, + } if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { retError.errlist = append(retError.errlist, resetSendErr) } @@ -843,28 +849,38 @@ func (c *Client) sendSingleMsg(message *Msg) error { } writer, err := c.smtpClient.Data() if err != nil { - return &SendError{Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message} + return &SendError{ + Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err), + affectedMsg: message, + } } _, err = message.WriteTo(writer) if err != nil { - return &SendError{Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message} + return &SendError{ + Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err), + affectedMsg: message, + } } message.isDelivered = true if err = writer.Close(); err != nil { - return &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message} + return &SendError{ + Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err), + affectedMsg: message, + } } if err = c.Reset(); err != nil { - return &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message} + return &SendError{ + Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err), + affectedMsg: message, + } } if err = c.checkConn(); err != nil { - return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message} + return &SendError{ + Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err), + affectedMsg: message, + } } return nil } From fbebcf96d80dcf2fa99985b7b727fef5250b3055 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 14:48:24 +0200 Subject: [PATCH 016/181] Add simple SMTP test server for unit testing Implemented a simple SMTP test server to facilitate unit testing. This server listens on a specified address and port, accepts connections, and processes common SMTP commands like HELO, MAIL FROM, RCPT TO, DATA, and QUIT. Several constants related to the server configuration were also added to the test file. --- client_test.go | 156 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 151 insertions(+), 5 deletions(-) diff --git a/client_test.go b/client_test.go index efccea4..31b68bb 100644 --- a/client_test.go +++ b/client_test.go @@ -5,6 +5,7 @@ package mail import ( + "bufio" "context" "crypto/tls" "errors" @@ -21,11 +22,18 @@ import ( "github.com/wneessen/go-mail/smtp" ) -// DefaultHost is used as default hostname for the Client -const DefaultHost = "localhost" - -// TestRcpt -const TestRcpt = "go-mail@mytrashmailer.com" +const ( + // DefaultHost is used as default hostname for the Client + DefaultHost = "localhost" + // TestRcpt is a trash mail address to send test mails to + TestRcpt = "go-mail@mytrashmailer.com" + // TestServerProto is the protocol used for the simple SMTP test server + TestServerProto = "tcp" + // TestServerAddr is the address the simple SMTP test server listens on + TestServerAddr = "127.0.0.1" + // TestServerPort is the port the simple SMTP test server listens on + TestServerPort = 2526 +) // TestNewClient tests the NewClient() method with its default options func TestNewClient(t *testing.T) { @@ -1545,3 +1553,141 @@ func (f faker) RemoteAddr() net.Addr { return nil } func (f faker) SetDeadline(time.Time) error { return nil } func (f faker) SetReadDeadline(time.Time) error { return nil } func (f faker) SetWriteDeadline(time.Time) error { return nil } + +func simpleSMTPServer(ctx context.Context, featureSet string) error { + listener, err := net.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, TestServerPort)) + if err != nil { + return fmt.Errorf("unable to listen on %s://%s: %w", TestServerProto, TestServerAddr, err) + } + + defer func() { + fmt.Printf("closing listener\n") + if err := listener.Close(); err != nil { + fmt.Printf("unable to close listener: %s\n", err) + } + }() + + for { + select { + case <-ctx.Done(): + return nil + default: + connection, err := listener.Accept() + var opErr *net.OpError + if err != nil { + if errors.As(err, &opErr) && opErr.Temporary() { + continue + } + return fmt.Errorf("unable to accept connection: %w", err) + } + handleTestServerConnection(connection, featureSet) + } + } +} + +func handleTestServerConnection(connection net.Conn, featureSet string) { + defer func() { + if err := connection.Close(); err != nil { + fmt.Printf("unable to close connection: %s\n", err) + } + }() + + reader := bufio.NewReader(connection) + writer := bufio.NewWriter(connection) + + writeLine := func(data string) error { + _, err := writer.WriteString(data + "\r\n") + if err != nil { + return fmt.Errorf("unable to write line: %w", err) + } + return writer.Flush() + } + writeOK := func() { + _ = writeLine("250 2.0.0 OK") + } + + if err := writeLine("220 go-mail test server ready ESMTP"); err != nil { + fmt.Printf("unable to write to client: %s\n", err) + return + } + + data, err := reader.ReadString('\n') + if err != nil { + fmt.Printf("unable to read from connection: %s\n", err) + } + if !strings.HasPrefix(data, "EHLO") && !strings.HasPrefix(data, "HELO") { + fmt.Printf("expected EHLO, got %q", data) + os.Exit(1) + } + if err = writeLine("250-localhost.localdomain\r\n" + featureSet); err != nil { + fmt.Printf("unable to write to connection: %s\n", err) + return + } + + for { + data, err = reader.ReadString('\n') + if err != nil { + if err == io.EOF { + fmt.Println("Connection closed by client.") + break + } + fmt.Println("Error reading data:", err) + break + } + + var datastring string + data = strings.TrimSpace(data) + switch { + case strings.HasPrefix(data, "MAIL FROM:"): + from := strings.TrimPrefix(data, "MAIL FROM:") + from = strings.ReplaceAll(from, "BODY=8BITMIME", "") + from = strings.ReplaceAll(from, "SMTPUTF8", "") + from = strings.TrimSpace(from) + if !strings.EqualFold(from, "") { + _ = writeLine(fmt.Sprintf("503 5.1.2 Invalid from: %s", from)) + break + } + writeOK() + case strings.HasPrefix(data, "RCPT TO:"): + to := strings.TrimPrefix(data, "RCPT TO:") + if !strings.EqualFold(to, "") { + _ = writeLine(fmt.Sprintf("500 5.1.2 Invalid to: %s", to)) + break + } + writeOK() + case strings.HasPrefix(data, "AUTH PLAIN"): + auth := strings.TrimPrefix(data, "AUTH PLAIN ") + if !strings.EqualFold(auth, "AHRvbmlAdGVzdGVyLmNvbQBWM3J5UzNjcjN0Kw==") { + _ = writeLine("535 5.7.8 Error: authentication failed") + break + } + _ = writeLine("235 2.7.0 Authentication successful") + case strings.EqualFold(data, "DATA"): + _ = writeLine("354 End data with .") + for { + ddata, derr := reader.ReadString('\n') + if derr != nil { + fmt.Printf("failed to read DATA data from connection: %s\n", derr) + break + } + ddata = strings.TrimSpace(ddata) + if ddata == "." { + _ = writeLine("250 2.0.0 Ok: queued as 1234567890") + break + } + datastring += ddata + "\n" + } + case strings.EqualFold(data, "noop"), + strings.EqualFold(data, "rset"), + strings.EqualFold(data, "vrfy"): + writeOK() + break + case strings.EqualFold(data, "quit"): + _ = writeLine("221 2.0.0 Bye") + break + default: + _ = writeLine("500 5.5.2 Error: bad syntax") + } + fmt.Printf("DATA received: %s", datastring) + } +} From 0aa24c6f3af5d1716d801cbd993258cbd9e08059 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 15:05:26 +0200 Subject: [PATCH 017/181] Add methods to retrieve message ID and message from SendError Implement MessageID and Msg methods in SendError to allow retrieval of the message ID and the affected message, respectively. These methods handle cases where the error or the message is nil, returning an empty string or nil as appropriate. --- senderror.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/senderror.go b/senderror.go index 494bfd3..90c8c4a 100644 --- a/senderror.go +++ b/senderror.go @@ -118,6 +118,23 @@ func (e *SendError) IsTemp() bool { return e.isTemp } +// MessageID returns the message ID of the affected Msg that caused the error +// If no message ID was set for the Msg, an empty string will be returned +func (e *SendError) MessageID() string { + if e == nil || e.affectedMsg == nil { + return "" + } + return e.affectedMsg.GetMessageID() +} + +// Msg returns the pointer to the affected message that caused the error +func (e *SendError) Msg() *Msg { + if e == nil || e.affectedMsg == nil { + return nil + } + return e.affectedMsg +} + // String implements the Stringer interface for the SendErrReason func (r SendErrReason) String() string { switch r { From 6af6a28f78973c480a853e080b17507f386f7895 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 15:05:43 +0200 Subject: [PATCH 018/181] Add test for SendError with no encoding Introduced `TestClient_SendErrorNoEncoding` to verify client behavior when sending a message without encoding. Adjusted server connection handling for better error reporting and connection closure, replacing abrupt exits with returns where appropriate. --- client_test.go | 73 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/client_test.go b/client_test.go index 31b68bb..c1706bf 100644 --- a/client_test.go +++ b/client_test.go @@ -1259,6 +1259,72 @@ func TestClient_DialAndSendWithContext_withSendError(t *testing.T) { } } +func TestClient_SendErrorNoEncoding(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + featureSet := "250-AUTH PLAIN\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) // wait until tcp server has been settled + + message := NewMsg() + if err := message.From("invalid-from@domain.tld"); err != nil { + t.Errorf("failed to set FROM address: %s", err) + return + } + if err := message.To("invalid-to@domain.tld"); err != nil { + t.Errorf("failed to set TO address: %s", err) + return + } + message.Subject("Test subject") + message.SetBodyString(TypeTextPlain, "Test body") + message.SetMessageIDWithValue("this.is.a.message.id") + message.SetEncoding(NoEncoding) + + client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), + WithUsername("toni@tester.com"), + WithPassword("V3ryS3cr3t+")) + if err != nil { + t.Errorf("unable to create new client: %s", err) + } + if err = client.DialWithContext(context.Background()); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Send(message); err == nil { + t.Error("expected Send() to fail but didn't") + } + + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Errorf("expected *SendError type as returned error, but got %T", sendErr) + } + if errors.As(err, &sendErr) { + if sendErr.IsTemp() { + t.Errorf("expected permanent error but IsTemp() returned true") + } + if sendErr.Reason != ErrNoUnencoded { + t.Errorf("expected ErrNoUnencoded error, but got %s", sendErr.Reason) + } + if !strings.EqualFold(sendErr.MessageID(), "") { + t.Errorf("expected message ID: %q, but got %q", "", + sendErr.MessageID()) + } + if sendErr.Msg() == nil { + t.Errorf("expected message to be set, but got nil") + } + } + + if err = client.Close(); err != nil { + t.Errorf("failed to close server connection: %s", err) + } +} + // getTestConnection takes environment variables to establish a connection to a real // SMTP server to test all functionality that requires a connection func getTestConnection(auth bool) (*Client, error) { @@ -1561,9 +1627,9 @@ func simpleSMTPServer(ctx context.Context, featureSet string) error { } defer func() { - fmt.Printf("closing listener\n") if err := listener.Close(); err != nil { fmt.Printf("unable to close listener: %s\n", err) + os.Exit(1) } }() @@ -1614,10 +1680,11 @@ func handleTestServerConnection(connection net.Conn, featureSet string) { data, err := reader.ReadString('\n') if err != nil { fmt.Printf("unable to read from connection: %s\n", err) + return } if !strings.HasPrefix(data, "EHLO") && !strings.HasPrefix(data, "HELO") { fmt.Printf("expected EHLO, got %q", data) - os.Exit(1) + return } if err = writeLine("250-localhost.localdomain\r\n" + featureSet); err != nil { fmt.Printf("unable to write to connection: %s\n", err) @@ -1628,7 +1695,6 @@ func handleTestServerConnection(connection net.Conn, featureSet string) { data, err = reader.ReadString('\n') if err != nil { if err == io.EOF { - fmt.Println("Connection closed by client.") break } fmt.Println("Error reading data:", err) @@ -1688,6 +1754,5 @@ func handleTestServerConnection(connection net.Conn, featureSet string) { default: _ = writeLine("500 5.5.2 Error: bad syntax") } - fmt.Printf("DATA received: %s", datastring) } } From b8f0462ce389820f2ef0c571208992ac685a2f6b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 15:49:03 +0200 Subject: [PATCH 019/181] Add error handling tests for SMTP client Implemented multiple tests to cover various error scenarios in the SMTP client, including invalid email addresses and data transmission failures. Introduced `failReset` flag in `simpleSMTPServer` to simulate server reset failures. --- client_test.go | 374 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 365 insertions(+), 9 deletions(-) diff --git a/client_test.go b/client_test.go index c1706bf..d1e269a 100644 --- a/client_test.go +++ b/client_test.go @@ -1265,7 +1265,7 @@ func TestClient_SendErrorNoEncoding(t *testing.T) { featureSet := "250-AUTH PLAIN\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctx, featureSet); err != nil { + if err := simpleSMTPServer(ctx, featureSet, false); err != nil { t.Errorf("failed to start test server: %s", err) return } @@ -1273,11 +1273,11 @@ func TestClient_SendErrorNoEncoding(t *testing.T) { time.Sleep(time.Millisecond * 300) // wait until tcp server has been settled message := NewMsg() - if err := message.From("invalid-from@domain.tld"); err != nil { + if err := message.From("valid-from@domain.tld"); err != nil { t.Errorf("failed to set FROM address: %s", err) return } - if err := message.To("invalid-to@domain.tld"); err != nil { + if err := message.To("valid-to@domain.tld"); err != nil { t.Errorf("failed to set TO address: %s", err) return } @@ -1325,6 +1325,344 @@ func TestClient_SendErrorNoEncoding(t *testing.T) { } } +func TestClient_SendErrorMailFrom(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, false); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) // wait until tcp server has been settled + + message := NewMsg() + if err := message.From("invalid-from@domain.tld"); err != nil { + t.Errorf("failed to set FROM address: %s", err) + return + } + if err := message.To("valid-to@domain.tld"); err != nil { + t.Errorf("failed to set TO address: %s", err) + return + } + message.Subject("Test subject") + message.SetBodyString(TypeTextPlain, "Test body") + message.SetMessageIDWithValue("this.is.a.message.id") + + client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), + WithUsername("toni@tester.com"), + WithPassword("V3ryS3cr3t+")) + if err != nil { + t.Errorf("unable to create new client: %s", err) + } + if err = client.DialWithContext(context.Background()); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Send(message); err == nil { + t.Error("expected Send() to fail but didn't") + } + + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Errorf("expected *SendError type as returned error, but got %T", sendErr) + } + if errors.As(err, &sendErr) { + if sendErr.IsTemp() { + t.Errorf("expected permanent error but IsTemp() returned true") + } + if sendErr.Reason != ErrSMTPMailFrom { + t.Errorf("expected ErrSMTPMailFrom error, but got %s", sendErr.Reason) + } + if !strings.EqualFold(sendErr.MessageID(), "") { + t.Errorf("expected message ID: %q, but got %q", "", + sendErr.MessageID()) + } + if sendErr.Msg() == nil { + t.Errorf("expected message to be set, but got nil") + } + } + + if err = client.Close(); err != nil { + t.Errorf("failed to close server connection: %s", err) + } +} + +func TestClient_SendErrorMailFromReset(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, true); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) // wait until tcp server has been settled + + message := NewMsg() + if err := message.From("invalid-from@domain.tld"); err != nil { + t.Errorf("failed to set FROM address: %s", err) + return + } + if err := message.To("valid-to@domain.tld"); err != nil { + t.Errorf("failed to set TO address: %s", err) + return + } + message.Subject("Test subject") + message.SetBodyString(TypeTextPlain, "Test body") + message.SetMessageIDWithValue("this.is.a.message.id") + + client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), + WithUsername("toni@tester.com"), + WithPassword("V3ryS3cr3t+")) + if err != nil { + t.Errorf("unable to create new client: %s", err) + } + if err = client.DialWithContext(context.Background()); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Send(message); err == nil { + t.Error("expected Send() to fail but didn't") + } + + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Errorf("expected *SendError type as returned error, but got %T", sendErr) + } + if errors.As(err, &sendErr) { + if sendErr.IsTemp() { + t.Errorf("expected permanent error but IsTemp() returned true") + } + if sendErr.Reason != ErrSMTPMailFrom { + t.Errorf("expected ErrSMTPMailFrom error, but got %s", sendErr.Reason) + } + if !strings.EqualFold(sendErr.MessageID(), "") { + t.Errorf("expected message ID: %q, but got %q", "", + sendErr.MessageID()) + } + if len(sendErr.errlist) != 2 { + t.Errorf("expected 2 errors, but got %d", len(sendErr.errlist)) + return + } + if !strings.EqualFold(sendErr.errlist[0].Error(), "503 5.1.2 Invalid from: ") { + t.Errorf("expected error: %q, but got %q", + "503 5.1.2 Invalid from: ", sendErr.errlist[0].Error()) + } + if !strings.EqualFold(sendErr.errlist[1].Error(), "500 5.1.2 Error: reset failed") { + t.Errorf("expected error: %q, but got %q", + "500 5.1.2 Error: reset failed", sendErr.errlist[1].Error()) + } + } + + if err = client.Close(); err != nil { + t.Errorf("failed to close server connection: %s", err) + } +} + +func TestClient_SendErrorToReset(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, true); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) // wait until tcp server has been settled + + message := NewMsg() + if err := message.From("valid-from@domain.tld"); err != nil { + t.Errorf("failed to set FROM address: %s", err) + return + } + if err := message.To("invalid-to@domain.tld"); err != nil { + t.Errorf("failed to set TO address: %s", err) + return + } + message.Subject("Test subject") + message.SetBodyString(TypeTextPlain, "Test body") + message.SetMessageIDWithValue("this.is.a.message.id") + + client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), + WithUsername("toni@tester.com"), + WithPassword("V3ryS3cr3t+")) + if err != nil { + t.Errorf("unable to create new client: %s", err) + } + if err = client.DialWithContext(context.Background()); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Send(message); err == nil { + t.Error("expected Send() to fail but didn't") + } + + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Errorf("expected *SendError type as returned error, but got %T", sendErr) + } + if errors.As(err, &sendErr) { + if sendErr.IsTemp() { + t.Errorf("expected permanent error but IsTemp() returned true") + } + if sendErr.Reason != ErrSMTPRcptTo { + t.Errorf("expected ErrSMTPRcptTo error, but got %s", sendErr.Reason) + } + if !strings.EqualFold(sendErr.MessageID(), "") { + t.Errorf("expected message ID: %q, but got %q", "", + sendErr.MessageID()) + } + if len(sendErr.errlist) != 2 { + t.Errorf("expected 2 errors, but got %d", len(sendErr.errlist)) + return + } + if !strings.EqualFold(sendErr.errlist[0].Error(), "500 5.1.2 Invalid to: ") { + t.Errorf("expected error: %q, but got %q", + "500 5.1.2 Invalid to: ", sendErr.errlist[0].Error()) + } + if !strings.EqualFold(sendErr.errlist[1].Error(), "500 5.1.2 Error: reset failed") { + t.Errorf("expected error: %q, but got %q", + "500 5.1.2 Error: reset failed", sendErr.errlist[1].Error()) + } + } + + if err = client.Close(); err != nil { + t.Errorf("failed to close server connection: %s", err) + } +} + +func TestClient_SendErrorDataClose(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, false); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + message := NewMsg() + if err := message.From("valid-from@domain.tld"); err != nil { + t.Errorf("failed to set FROM address: %s", err) + return + } + if err := message.To("valid-to@domain.tld"); err != nil { + t.Errorf("failed to set TO address: %s", err) + return + } + message.Subject("Test subject") + message.SetBodyString(TypeTextPlain, "DATA close should fail") + message.SetMessageIDWithValue("this.is.a.message.id") + + client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), + WithUsername("toni@tester.com"), + WithPassword("V3ryS3cr3t+")) + if err != nil { + t.Errorf("unable to create new client: %s", err) + } + if err = client.DialWithContext(context.Background()); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Send(message); err == nil { + t.Error("expected Send() to fail but didn't") + } + + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Errorf("expected *SendError type as returned error, but got %T", sendErr) + } + if errors.As(err, &sendErr) { + if sendErr.IsTemp() { + t.Errorf("expected permanent error but IsTemp() returned true") + } + if sendErr.Reason != ErrSMTPDataClose { + t.Errorf("expected ErrSMTPDataClose error, but got %s", sendErr.Reason) + } + if !strings.EqualFold(sendErr.MessageID(), "") { + t.Errorf("expected message ID: %q, but got %q", "", + sendErr.MessageID()) + } + } + + if err = client.Close(); err != nil { + t.Errorf("failed to close server connection: %s", err) + } +} + +func TestClient_SendErrorDataWrite(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, false); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + message := NewMsg() + if err := message.From("valid-from@domain.tld"); err != nil { + t.Errorf("failed to set FROM address: %s", err) + return + } + if err := message.To("valid-to@domain.tld"); err != nil { + t.Errorf("failed to set TO address: %s", err) + return + } + message.Subject("Test subject") + message.SetBodyString(TypeTextPlain, "DATA write should fail") + message.SetMessageIDWithValue("this.is.a.message.id") + message.SetGenHeader("X-Test-Header", "DATA write should fail") + + client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), + WithUsername("toni@tester.com"), + WithPassword("V3ryS3cr3t+")) + if err != nil { + t.Errorf("unable to create new client: %s", err) + } + if err = client.DialWithContext(context.Background()); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Send(message); err == nil { + t.Error("expected Send() to fail but didn't") + } + + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Errorf("expected *SendError type as returned error, but got %T", sendErr) + } + if errors.As(err, &sendErr) { + if sendErr.IsTemp() { + t.Errorf("expected permanent error but IsTemp() returned true") + } + if sendErr.Reason != ErrSMTPDataClose { + t.Errorf("expected ErrSMTPDataClose error, but got %s", sendErr.Reason) + } + if !strings.EqualFold(sendErr.MessageID(), "") { + t.Errorf("expected message ID: %q, but got %q", "", + sendErr.MessageID()) + } + } + + if err = client.Close(); err != nil { + t.Errorf("failed to close server connection: %s", err) + } +} + // getTestConnection takes environment variables to establish a connection to a real // SMTP server to test all functionality that requires a connection func getTestConnection(auth bool) (*Client, error) { @@ -1620,7 +1958,10 @@ func (f faker) SetDeadline(time.Time) error { return nil } func (f faker) SetReadDeadline(time.Time) error { return nil } func (f faker) SetWriteDeadline(time.Time) error { return nil } -func simpleSMTPServer(ctx context.Context, featureSet string) error { +// simpleSMTPServer starts a simple TCP server that resonds to SMTP commands. +// The provided featureSet represents in what the server responds to EHLO command +// failReset controls if a RSET succeeds +func simpleSMTPServer(ctx context.Context, featureSet string, failReset bool) error { listener, err := net.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, TestServerPort)) if err != nil { return fmt.Errorf("unable to listen on %s://%s: %w", TestServerProto, TestServerAddr, err) @@ -1646,12 +1987,12 @@ func simpleSMTPServer(ctx context.Context, featureSet string) error { } return fmt.Errorf("unable to accept connection: %w", err) } - handleTestServerConnection(connection, featureSet) + handleTestServerConnection(connection, featureSet, failReset) } } } -func handleTestServerConnection(connection net.Conn, featureSet string) { +func handleTestServerConnection(connection net.Conn, featureSet string, failReset bool) { defer func() { if err := connection.Close(); err != nil { fmt.Printf("unable to close connection: %s\n", err) @@ -1709,14 +2050,15 @@ func handleTestServerConnection(connection net.Conn, featureSet string) { from = strings.ReplaceAll(from, "BODY=8BITMIME", "") from = strings.ReplaceAll(from, "SMTPUTF8", "") from = strings.TrimSpace(from) - if !strings.EqualFold(from, "") { + if !strings.EqualFold(from, "") { _ = writeLine(fmt.Sprintf("503 5.1.2 Invalid from: %s", from)) break } writeOK() case strings.HasPrefix(data, "RCPT TO:"): to := strings.TrimPrefix(data, "RCPT TO:") - if !strings.EqualFold(to, "") { + to = strings.TrimSpace(to) + if !strings.EqualFold(to, "") { _ = writeLine(fmt.Sprintf("500 5.1.2 Invalid to: %s", to)) break } @@ -1737,17 +2079,31 @@ func handleTestServerConnection(connection net.Conn, featureSet string) { break } ddata = strings.TrimSpace(ddata) + if strings.EqualFold(ddata, "DATA write should fail") { + _ = writeLine("500 5.0.0 Error during DATA transmission") + break + } if ddata == "." { + if strings.Contains(datastring, "DATA close should fail") { + _ = writeLine("500 5.0.0 Error during DATA closing") + break + } _ = writeLine("250 2.0.0 Ok: queued as 1234567890") break } datastring += ddata + "\n" } case strings.EqualFold(data, "noop"), - strings.EqualFold(data, "rset"), strings.EqualFold(data, "vrfy"): writeOK() break + case strings.EqualFold(data, "rset"): + if failReset { + _ = writeLine("500 5.1.2 Error: reset failed") + break + } + writeOK() + break case strings.EqualFold(data, "quit"): _ = writeLine("221 2.0.0 Bye") break From 482194b4b345dca362fc29d321814b970cf74eeb Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 15:52:05 +0200 Subject: [PATCH 020/181] Add TestClient_SendErrorReset to validate SMTP error handling This test checks the client's ability to handle SMTP reset errors when sending an email. It verifies the correct error type, ensures it is recognized as a permanent error, and confirms the correct message ID handling. --- client_test.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/client_test.go b/client_test.go index d1e269a..bfa799f 100644 --- a/client_test.go +++ b/client_test.go @@ -1663,6 +1663,68 @@ func TestClient_SendErrorDataWrite(t *testing.T) { } } +func TestClient_SendErrorReset(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, true); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + message := NewMsg() + if err := message.From("valid-from@domain.tld"); err != nil { + t.Errorf("failed to set FROM address: %s", err) + return + } + if err := message.To("valid-to@domain.tld"); err != nil { + t.Errorf("failed to set TO address: %s", err) + return + } + message.Subject("Test subject") + message.SetBodyString(TypeTextPlain, "Test body") + message.SetMessageIDWithValue("this.is.a.message.id") + + client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), + WithUsername("toni@tester.com"), + WithPassword("V3ryS3cr3t+")) + if err != nil { + t.Errorf("unable to create new client: %s", err) + } + if err = client.DialWithContext(context.Background()); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Send(message); err == nil { + t.Error("expected Send() to fail but didn't") + } + + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Errorf("expected *SendError type as returned error, but got %T", sendErr) + } + if errors.As(err, &sendErr) { + if sendErr.IsTemp() { + t.Errorf("expected permanent error but IsTemp() returned true") + } + if sendErr.Reason != ErrSMTPReset { + t.Errorf("expected ErrSMTPReset error, but got %s", sendErr.Reason) + } + if !strings.EqualFold(sendErr.MessageID(), "") { + t.Errorf("expected message ID: %q, but got %q", "", + sendErr.MessageID()) + } + } + + if err = client.Close(); err != nil { + t.Errorf("failed to close server connection: %s", err) + } +} + // getTestConnection takes environment variables to establish a connection to a real // SMTP server to test all functionality that requires a connection func getTestConnection(auth bool) (*Client, error) { From 157c1381426d5ab6459195b5b169c9e4a1c2e8f4 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 15:58:51 +0200 Subject: [PATCH 021/181] Fix linter errors Replaced `err == io.EOF` with `errors.Is(err, io.EOF)` for better error comparison. Removed redundant `break` statements to streamline case logic and improve code readability. --- client_test.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client_test.go b/client_test.go index bfa799f..afd9cd9 100644 --- a/client_test.go +++ b/client_test.go @@ -2097,7 +2097,7 @@ func handleTestServerConnection(connection net.Conn, featureSet string, failRese for { data, err = reader.ReadString('\n') if err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { break } fmt.Println("Error reading data:", err) @@ -2158,17 +2158,14 @@ func handleTestServerConnection(connection net.Conn, featureSet string, failRese case strings.EqualFold(data, "noop"), strings.EqualFold(data, "vrfy"): writeOK() - break case strings.EqualFold(data, "rset"): if failReset { _ = writeLine("500 5.1.2 Error: reset failed") break } writeOK() - break case strings.EqualFold(data, "quit"): _ = writeLine("221 2.0.0 Bye") - break default: _ = writeLine("500 5.5.2 Error: bad syntax") } From d5437f6b7aa44ae2a2689c5eba691c560bb45879 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 16:23:31 +0200 Subject: [PATCH 022/181] Remove redundant comments from sleep statements Removed unnecessary comments that were clarifying the purpose of sleep statements in the test cases. This makes the code cleaner and easier to maintain by reducing clutter. --- client_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client_test.go b/client_test.go index afd9cd9..56eb8c5 100644 --- a/client_test.go +++ b/client_test.go @@ -1270,7 +1270,7 @@ func TestClient_SendErrorNoEncoding(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) // wait until tcp server has been settled + time.Sleep(time.Millisecond * 300) message := NewMsg() if err := message.From("valid-from@domain.tld"); err != nil { @@ -1336,7 +1336,7 @@ func TestClient_SendErrorMailFrom(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) // wait until tcp server has been settled + time.Sleep(time.Millisecond * 300) message := NewMsg() if err := message.From("invalid-from@domain.tld"); err != nil { @@ -1401,7 +1401,7 @@ func TestClient_SendErrorMailFromReset(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) // wait until tcp server has been settled + time.Sleep(time.Millisecond * 300) message := NewMsg() if err := message.From("invalid-from@domain.tld"); err != nil { @@ -1475,7 +1475,7 @@ func TestClient_SendErrorToReset(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) // wait until tcp server has been settled + time.Sleep(time.Millisecond * 300) message := NewMsg() if err := message.From("valid-from@domain.tld"); err != nil { From 8dfb121aec21774ed6c764c67680c1f57bbbd758 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 16:24:59 +0200 Subject: [PATCH 023/181] Update Go versions in GitHub Actions workflow Removed Go 1.21 and added Go 1.19 in the codecov.yml file to ensure compatibility with older projects and streamline the CI process. This helps in maintaining backward compatibility and avoids potential issues with unsupported Go versions. --- .github/workflows/codecov.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 2178e32..9ab2f52 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -36,7 +36,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - go: ['1.20', '1.21', '1.22', '1.23'] + go: ['1.19', '1.20', '1.23'] steps: - name: Harden Runner uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 From bd5a8a40b9b7738df74e857f6c598e992f46fd3a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 16:40:12 +0200 Subject: [PATCH 024/181] Fix error handling in Send method in client_119.go Refine Send method to correctly typecast and accumulate SendError instances. Introduce "errors" package import to utilize errors.As for precise error type checking, ensuring accurate error lists. This regression was introduced with PR #301 --- client_119.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client_119.go b/client_119.go index 8084913..7de5d59 100644 --- a/client_119.go +++ b/client_119.go @@ -7,16 +7,22 @@ package mail +import "errors" + // Send sends out the mail message func (c *Client) Send(messages ...*Msg) error { if err := c.checkConn(); err != nil { return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} } var errs []*SendError - for _, message := range messages { + for id, message := range messages { if sendErr := c.sendSingleMsg(message); sendErr != nil { messages[id].sendError = sendErr - errs = append(errs, sendErr) + + var msgSendErr *SendError + if errors.As(sendErr, &msgSendErr) { + errs = append(errs, msgSendErr) + } } } From 4ee11e840606a6f99e08cbaef45e511d7a768f88 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 16:44:15 +0200 Subject: [PATCH 025/181] Refactor SMTP server port handling in tests Modified tests to dynamically compute server ports from a base value, enhancing flexibility and preventing potential conflicts. Updated `simpleSMTPServer` function to accept a port parameter. --- client_test.go | 47 +++++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/client_test.go b/client_test.go index 56eb8c5..3d50a2d 100644 --- a/client_test.go +++ b/client_test.go @@ -31,8 +31,8 @@ const ( TestServerProto = "tcp" // TestServerAddr is the address the simple SMTP test server listens on TestServerAddr = "127.0.0.1" - // TestServerPort is the port the simple SMTP test server listens on - TestServerPort = 2526 + // TestServerPortBase is the base port for the simple SMTP test server + TestServerPortBase = 2025 ) // TestNewClient tests the NewClient() method with its default options @@ -1264,8 +1264,9 @@ func TestClient_SendErrorNoEncoding(t *testing.T) { defer cancel() featureSet := "250-AUTH PLAIN\r\n250-DSN\r\n250 SMTPUTF8" + serverPort := TestServerPortBase + 1 go func() { - if err := simpleSMTPServer(ctx, featureSet, false); err != nil { + if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil { t.Errorf("failed to start test server: %s", err) return } @@ -1286,7 +1287,7 @@ func TestClient_SendErrorNoEncoding(t *testing.T) { message.SetMessageIDWithValue("this.is.a.message.id") message.SetEncoding(NoEncoding) - client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + client, err := NewClient(TestServerAddr, WithPort(serverPort), WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), WithUsername("toni@tester.com"), WithPassword("V3ryS3cr3t+")) @@ -1329,9 +1330,10 @@ func TestClient_SendErrorMailFrom(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + serverPort := TestServerPortBase + 2 featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctx, featureSet, false); err != nil { + if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil { t.Errorf("failed to start test server: %s", err) return } @@ -1351,7 +1353,7 @@ func TestClient_SendErrorMailFrom(t *testing.T) { message.SetBodyString(TypeTextPlain, "Test body") message.SetMessageIDWithValue("this.is.a.message.id") - client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + client, err := NewClient(TestServerAddr, WithPort(serverPort), WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), WithUsername("toni@tester.com"), WithPassword("V3ryS3cr3t+")) @@ -1394,9 +1396,10 @@ func TestClient_SendErrorMailFromReset(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + serverPort := TestServerPortBase + 3 featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctx, featureSet, true); err != nil { + if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { t.Errorf("failed to start test server: %s", err) return } @@ -1416,7 +1419,7 @@ func TestClient_SendErrorMailFromReset(t *testing.T) { message.SetBodyString(TypeTextPlain, "Test body") message.SetMessageIDWithValue("this.is.a.message.id") - client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + client, err := NewClient(TestServerAddr, WithPort(serverPort), WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), WithUsername("toni@tester.com"), WithPassword("V3ryS3cr3t+")) @@ -1468,9 +1471,10 @@ func TestClient_SendErrorToReset(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + serverPort := TestServerPortBase + 4 featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctx, featureSet, true); err != nil { + if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { t.Errorf("failed to start test server: %s", err) return } @@ -1490,7 +1494,7 @@ func TestClient_SendErrorToReset(t *testing.T) { message.SetBodyString(TypeTextPlain, "Test body") message.SetMessageIDWithValue("this.is.a.message.id") - client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + client, err := NewClient(TestServerAddr, WithPort(serverPort), WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), WithUsername("toni@tester.com"), WithPassword("V3ryS3cr3t+")) @@ -1542,9 +1546,10 @@ func TestClient_SendErrorDataClose(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + serverPort := TestServerPortBase + 5 featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctx, featureSet, false); err != nil { + if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil { t.Errorf("failed to start test server: %s", err) return } @@ -1564,7 +1569,7 @@ func TestClient_SendErrorDataClose(t *testing.T) { message.SetBodyString(TypeTextPlain, "DATA close should fail") message.SetMessageIDWithValue("this.is.a.message.id") - client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + client, err := NewClient(TestServerAddr, WithPort(serverPort), WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), WithUsername("toni@tester.com"), WithPassword("V3ryS3cr3t+")) @@ -1604,9 +1609,10 @@ func TestClient_SendErrorDataWrite(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + serverPort := TestServerPortBase + 6 featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctx, featureSet, false); err != nil { + if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil { t.Errorf("failed to start test server: %s", err) return } @@ -1627,7 +1633,7 @@ func TestClient_SendErrorDataWrite(t *testing.T) { message.SetMessageIDWithValue("this.is.a.message.id") message.SetGenHeader("X-Test-Header", "DATA write should fail") - client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + client, err := NewClient(TestServerAddr, WithPort(serverPort), WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), WithUsername("toni@tester.com"), WithPassword("V3ryS3cr3t+")) @@ -1657,19 +1663,16 @@ func TestClient_SendErrorDataWrite(t *testing.T) { sendErr.MessageID()) } } - - if err = client.Close(); err != nil { - t.Errorf("failed to close server connection: %s", err) - } } func TestClient_SendErrorReset(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + serverPort := TestServerPortBase + 7 featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctx, featureSet, true); err != nil { + if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { t.Errorf("failed to start test server: %s", err) return } @@ -1689,7 +1692,7 @@ func TestClient_SendErrorReset(t *testing.T) { message.SetBodyString(TypeTextPlain, "Test body") message.SetMessageIDWithValue("this.is.a.message.id") - client, err := NewClient(TestServerAddr, WithPort(TestServerPort), + client, err := NewClient(TestServerAddr, WithPort(serverPort), WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), WithUsername("toni@tester.com"), WithPassword("V3ryS3cr3t+")) @@ -2023,8 +2026,8 @@ func (f faker) SetWriteDeadline(time.Time) error { return nil } // simpleSMTPServer starts a simple TCP server that resonds to SMTP commands. // The provided featureSet represents in what the server responds to EHLO command // failReset controls if a RSET succeeds -func simpleSMTPServer(ctx context.Context, featureSet string, failReset bool) error { - listener, err := net.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, TestServerPort)) +func simpleSMTPServer(ctx context.Context, featureSet string, failReset bool, port int) error { + listener, err := net.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, port)) if err != nil { return fmt.Errorf("unable to listen on %s://%s: %w", TestServerProto, TestServerAddr, err) } From 44df830348d99ebedef2ceb356d4b4b2e4a939b2 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 17:00:08 +0200 Subject: [PATCH 026/181] Add tests for SendError MessageID and Msg methods Introduce two new tests in senderror_test.go: TestSendError_MessageID and TestSendError_Msg. These tests validate the behavior of the MessageID and Msg methods of the SendError type, ensuring correct handling of message ID and sender information. --- senderror_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/senderror_test.go b/senderror_test.go index 789b290..8b7bfb3 100644 --- a/senderror_test.go +++ b/senderror_test.go @@ -90,7 +90,55 @@ func TestSendError_IsTempNil(t *testing.T) { } } +func TestSendError_MessageID(t *testing.T) { + var se *SendError + err := returnSendError(ErrAmbiguous, false) + if !errors.As(err, &se) { + t.Errorf("error mismatch, expected error to be of type *SendError") + return + } + if errors.As(err, &se) { + if se.MessageID() == "" { + t.Errorf("sendError expected message-id, but got empty string") + } + if !strings.EqualFold(se.MessageID(), "") { + t.Errorf("sendError message-id expected: %s, but got: %s", "", + se.MessageID()) + } + } +} + +func TestSendError_Msg(t *testing.T) { + var se *SendError + err := returnSendError(ErrAmbiguous, false) + if !errors.As(err, &se) { + t.Errorf("error mismatch, expected error to be of type *SendError") + return + } + if errors.As(err, &se) { + if se.Msg() == nil { + t.Errorf("sendError expected msg pointer, but got nil") + } + from := se.Msg().GetFromString() + if len(from) == 0 { + t.Errorf("sendError expected msg from, but got empty string") + return + } + if !strings.EqualFold(from[0], "") { + t.Errorf("sendError message from expected: %s, but got: %s", "", + from[0]) + } + } +} + // returnSendError is a helper method to retunr a SendError with a specific reason func returnSendError(r SendErrReason, t bool) error { - return &SendError{Reason: r, isTemp: t} + message := NewMsg() + _ = message.From("toni.tester@domain.tld") + _ = message.To("tina.tester@domain.tld") + message.Subject("This is the subject") + message.SetBodyString(TypeTextPlain, "This is the message body") + message.SetMessageIDWithValue("this.is.a.message.id") + + return &SendError{Reason: r, isTemp: t, affectedMsg: message} } From af9915e4e7c32669dd7300b876a7c416fe39e979 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 19:45:49 +0200 Subject: [PATCH 027/181] Add test for WriteToTempFile failure scenario This new test verifies error handling for WriteToTempFile when the TMPDIR environment variable is set to an invalid directory. It ensures that the method fails as expected under these conditions, improving code robustness. --- msg_nowin_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/msg_nowin_test.go b/msg_nowin_test.go index 820be24..e338c57 100644 --- a/msg_nowin_test.go +++ b/msg_nowin_test.go @@ -61,3 +61,18 @@ func TestMsg_WriteToSendmail(t *testing.T) { t.Errorf("WriteToSendmail failed: %s", err) } } + +func TestMsg_WriteToTempFileFailed(t *testing.T) { + m := NewMsg() + _ = m.From("Toni Tester ") + _ = m.To("Ellenor Tester ") + m.SetBodyString(TypeTextPlain, "This is a test") + + if err := os.Setenv("TMPDIR", "/invalid/directory/that/does/not/exist"); err != nil { + t.Errorf("failed to set TMPDIR environment variable: %s", err) + } + _, err := m.WriteToTempFile() + if err == nil { + t.Errorf("WriteToTempFile() did not fail as expected") + } +} From 19330fc1081f641442312c96ed3197e2f9e129ce Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 20:30:23 +0200 Subject: [PATCH 028/181] Refactor random number generation and add version-specific implementations Removed error handling from `randNum` in `random_test.go` and introduced new `randNum` implementations for different Go versions (1.19, 1.20-1.21, 1.22+). This ensures compatibility with different versions of Go and utilizes the appropriate version-specific random number generation methods. We are using math/rand instead crypto/rand. For our needs this should be sufficient. --- random.go | 22 ---------------------- random_119.go | 22 ++++++++++++++++++++++ random_121.go | 20 ++++++++++++++++++++ random_122.go | 22 ++++++++++++++++++++++ random_test.go | 8 +------- 5 files changed, 65 insertions(+), 29 deletions(-) create mode 100644 random_119.go create mode 100644 random_121.go create mode 100644 random_122.go diff --git a/random.go b/random.go index 831b118..3a3f16b 100644 --- a/random.go +++ b/random.go @@ -7,8 +7,6 @@ package mail import ( "crypto/rand" "encoding/binary" - "fmt" - "math/big" "strings" ) @@ -52,23 +50,3 @@ func randomStringSecure(length int) (string, error) { return randString.String(), nil } - -// randNum returns a random number with a maximum value of length -func randNum(length int) (int, error) { - if length <= 0 { - return 0, fmt.Errorf("provided number is <= 0: %d", length) - } - length64 := big.NewInt(int64(length)) - if !length64.IsUint64() { - return 0, fmt.Errorf("big.NewInt() generation returned negative value: %d", length64) - } - randNum64, err := rand.Int(rand.Reader, length64) - if err != nil { - return 0, err - } - randomNum := int(randNum64.Int64()) - if randomNum < 0 { - return 0, fmt.Errorf("generated random number does not fit as int64: %d", randNum64) - } - return randomNum, nil -} diff --git a/random_119.go b/random_119.go new file mode 100644 index 0000000..b084305 --- /dev/null +++ b/random_119.go @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors +// +// SPDX-License-Identifier: MIT + +//go:build go1.19 && !go1.20 +// +build go1.19,!go1.20 + +package mail + +import ( + "math/rand" + "time" +) + +// randNum returns a random number with a maximum value of length +func randNum(maxval int) int { + if maxval <= 0 { + return 0 + } + rand.Seed(time.Now().UnixNano()) + return rand.Intn(maxval) +} diff --git a/random_121.go b/random_121.go new file mode 100644 index 0000000..b401bc8 --- /dev/null +++ b/random_121.go @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors +// +// SPDX-License-Identifier: MIT + +//go:build go1.20 && !go1.22 +// +build go1.20,!go1.22 + +package mail + +import ( + "math/rand" +) + +// randNum returns a random number with a maximum value of length +func randNum(maxval int) int { + if maxval <= 0 { + return 0 + } + return rand.Intn(maxval) +} diff --git a/random_122.go b/random_122.go new file mode 100644 index 0000000..9a6132b --- /dev/null +++ b/random_122.go @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors +// +// SPDX-License-Identifier: MIT + +//go:build go1.22 +// +build go1.22 + +package mail + +import ( + "math/rand/v2" +) + +// randNum returns a random number with a maximum value of maxval. +// go-mail compiled with Go 1.22+ will make use of the novel math/rand/v2 interface +// Older versions of Go will use math/rand +func randNum(maxval int) int { + if maxval <= 0 { + return 0 + } + return rand.IntN(maxval) +} diff --git a/random_test.go b/random_test.go index 51e3ba6..caa5e58 100644 --- a/random_test.go +++ b/random_test.go @@ -55,13 +55,7 @@ func TestRandomNum(t *testing.T) { for _, tc := range tt { t.Run(tc.testName, func(t *testing.T) { - rn, err := randNum(tc.max) - if err != nil { - t.Errorf("random number generation failed: %s", err) - } - if rn < 0 { - t.Errorf("random number generation failed: %d is smaller than zero", rn) - } + rn := randNum(tc.max) if rn > tc.max { t.Errorf("random number generation failed: %d is bigger than given value %d", rn, tc.max) } From f5d4cdafeace52dfb353b097cd340dd282dbd18c Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 20:39:30 +0200 Subject: [PATCH 029/181] Increase test iterations for SetMessageID randomness check Updated the TestMsg_SetMessageIDRandomness test to run 50,000 iterations instead of 100, ensuring a more robust evaluation of the Msg.SetMessageID method's randomness. Additionally, streamlined the retrieval of Message ID using GetMessageID. --- msg_test.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/msg_test.go b/msg_test.go index 0656ff2..f570b02 100644 --- a/msg_test.go +++ b/msg_test.go @@ -786,13 +786,11 @@ func TestMsg_SetMessageIDWithValue(t *testing.T) { // TestMsg_SetMessageIDRandomness tests the randomness of Msg.SetMessageID methods func TestMsg_SetMessageIDRandomness(t *testing.T) { var mids []string - for i := 0; i < 100; i++ { + for i := 0; i < 50_000; i++ { m := NewMsg() m.SetMessageID() - mid := m.GetGenHeader(HeaderMessageID) - if len(mid) > 0 { - mids = append(mids, mid[0]) - } + mid := m.GetMessageID() + mids = append(mids, mid) } c := make(map[string]int) for i := range mids { From 77920be1a116f9cb0810a581d50f2db699731e15 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 20:39:50 +0200 Subject: [PATCH 030/181] Remove unnecessary error handling Eliminated redundant error handling for random number generation as errors from these functions do not require special attention. This reduces code complexity and improves readability. --- msg.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/msg.go b/msg.go index 11d8ab1..b66bffe 100644 --- a/msg.go +++ b/msg.go @@ -467,8 +467,8 @@ func (m *Msg) SetMessageID() { if err != nil { hostname = "localhost.localdomain" } - randNumPrimary, _ := randNum(100000000) - randNumSecondary, _ := randNum(10000) + randNumPrimary := randNum(100000000) + randNumSecondary := randNum(10000) randString, _ := randomStringSecure(17) procID := os.Getpid() * randNumSecondary messageID := fmt.Sprintf("%d.%d%d.%s@%s", procID, randNumPrimary, randNumSecondary, From 52061f97c654834982f714df316bb76c759f7ae8 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 20:58:29 +0200 Subject: [PATCH 031/181] Add tests for nil SendError MessageID and Msg These tests ensure that accessing MessageID and Msg methods on a nil SendError pointer returns the expected values (empty string and nil respectively). This helps in validating the error handling logic and avoiding potential nil pointer dereference issues. --- senderror_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/senderror_test.go b/senderror_test.go index 8b7bfb3..bf823e9 100644 --- a/senderror_test.go +++ b/senderror_test.go @@ -108,6 +108,13 @@ func TestSendError_MessageID(t *testing.T) { } } +func TestSendError_MessageIDNil(t *testing.T) { + var se *SendError + if se.MessageID() != "" { + t.Error("expected empty string on nil-senderror") + } +} + func TestSendError_Msg(t *testing.T) { var se *SendError err := returnSendError(ErrAmbiguous, false) @@ -131,6 +138,13 @@ func TestSendError_Msg(t *testing.T) { } } +func TestSendError_MsgNil(t *testing.T) { + var se *SendError + if se.Msg() != nil { + t.Error("expected nil on nil-senderror") + } +} + // returnSendError is a helper method to retunr a SendError with a specific reason func returnSendError(r SendErrReason, t bool) error { message := NewMsg() From f3633e1913b923e68b6fd004a02a95c9744a54ee Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 21:11:19 +0200 Subject: [PATCH 032/181] Add tests for SendError functionalities Introduce tests for `SendError` to verify behavior of `errors.Is` and string representation. `TestSendError_IsFail` checks for error mismatch, and `TestSendError_ErrorMulti` verifies the formatted error message. --- senderror_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/senderror_test.go b/senderror_test.go index bf823e9..9a9b1ec 100644 --- a/senderror_test.go +++ b/senderror_test.go @@ -145,6 +145,24 @@ func TestSendError_MsgNil(t *testing.T) { } } +func TestSendError_IsFail(t *testing.T) { + err1 := returnSendError(ErrAmbiguous, false) + err2 := returnSendError(ErrSMTPMailFrom, false) + if errors.Is(err1, err2) { + t.Errorf("error mismatch, ErrAmbiguous should not be equal to ErrSMTPMailFrom") + } +} + +func TestSendError_ErrorMulti(t *testing.T) { + expected := `ambiguous reason, check Msg.SendError for message specific reasons, ` + + `affected recipient(s): , ` + err := &SendError{Reason: ErrAmbiguous, isTemp: false, affectedMsg: nil, + rcpt: []string{"", ""}} + if err.Error() != expected { + t.Errorf("error mismatch, expected: %s, got: %s", expected, err.Error()) + } +} + // returnSendError is a helper method to retunr a SendError with a specific reason func returnSendError(r SendErrReason, t bool) error { message := NewMsg() From 40534570200ed066977283bb3c207752566b1c6a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 21:15:13 +0200 Subject: [PATCH 033/181] Refactor TestSendError_ErrorMulti formatting Reformatted the instantiation of the SendError struct to improve readability. This helps maintain consistency and clarity in the code base for better collaboration and future maintenance. --- senderror_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/senderror_test.go b/senderror_test.go index 9a9b1ec..e04b7ee 100644 --- a/senderror_test.go +++ b/senderror_test.go @@ -156,8 +156,10 @@ func TestSendError_IsFail(t *testing.T) { func TestSendError_ErrorMulti(t *testing.T) { expected := `ambiguous reason, check Msg.SendError for message specific reasons, ` + `affected recipient(s): , ` - err := &SendError{Reason: ErrAmbiguous, isTemp: false, affectedMsg: nil, - rcpt: []string{"", ""}} + err := &SendError{ + Reason: ErrAmbiguous, isTemp: false, affectedMsg: nil, + rcpt: []string{"", ""}, + } if err.Error() != expected { t.Errorf("error mismatch, expected: %s, got: %s", expected, err.Error()) } From 0b9a215e7d37c7fe52d96303798c6b2fa04ad9a4 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 21:42:10 +0200 Subject: [PATCH 034/181] Add deferred TMPDIR restoration in test setup Store and restore the original TMPDIR value using a deferred function in `msg_nowin_test.go`. This ensures the TMPDIR environment variable is restored after the test runs, preventing potential side effects on other tests or processes. --- msg_nowin_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/msg_nowin_test.go b/msg_nowin_test.go index e338c57..6cde71a 100644 --- a/msg_nowin_test.go +++ b/msg_nowin_test.go @@ -68,6 +68,13 @@ func TestMsg_WriteToTempFileFailed(t *testing.T) { _ = m.To("Ellenor Tester ") m.SetBodyString(TypeTextPlain, "This is a test") + curTmpDir := os.Getenv("TMPDIR") + defer func() { + if err := os.Setenv("TMPDIR", curTmpDir); err != nil { + t.Errorf("failed to set TMPDIR environment variable: %s", err) + } + }() + if err := os.Setenv("TMPDIR", "/invalid/directory/that/does/not/exist"); err != nil { t.Errorf("failed to set TMPDIR environment variable: %s", err) } From 3f0ac027e2d9fa315121ffcc9f79763448312c7c Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 20 Sep 2024 22:06:11 +0200 Subject: [PATCH 035/181] Add test for zero input in randNum This commit introduces a new test case in random_test.go to ensure that the randNum function returns zero when given an input of zero. This helps validate the correctness of edge case handling in the random number generation logic. --- random_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/random_test.go b/random_test.go index caa5e58..e69b4e7 100644 --- a/random_test.go +++ b/random_test.go @@ -62,3 +62,10 @@ func TestRandomNum(t *testing.T) { }) } } + +func TestRandomNumZero(t *testing.T) { + rn := randNum(0) + if rn != 0 { + t.Errorf("random number generation failed: %d is not zero", rn) + } +} From 5ec8a5a5feef5a66cf4617f1e94097952a1e15ae Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 22 Sep 2024 20:48:09 +0200 Subject: [PATCH 036/181] Add initial connection pool interface Introduces a new `connpool.go` file implementing a connection pool interface for managing network connections. This interface includes methods to get and close connections, as well as to retrieve the current pool size. The implementation is initially based on a fork of code from the Fatih Arslan GitHub repository. --- connpool.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 connpool.go diff --git a/connpool.go b/connpool.go new file mode 100644 index 0000000..8eabbad --- /dev/null +++ b/connpool.go @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2022-2024 The go-mail Authors +// +// SPDX-License-Identifier: MIT + +package mail + +import "net" + +// Parts of the connection pool code is forked from https://github.com/fatih/pool/ +// Thanks to Fatih Arslan and the project contributors for providing this great +// concurrency template. + +// Pool interface describes a connection pool implementation. A Pool is +// thread-/go-routine safe. +type Pool interface { + // Get returns a new connection from the pool. Closing the connections returns + // it back into the Pool. Closing a connection when the Pool is destroyed or + // full will be counted as an error. + Get() (net.Conn, error) + + // Close closes the pool and all its connections. After Close() the pool is + // no longer usable. + Close() + + // Len returns the current number of connections of the pool. + Len() int +} From 26ff177fb0f91f2afc71c6692b95726e477ec02a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 22 Sep 2024 21:12:59 +0200 Subject: [PATCH 037/181] Add connPool implementation and connection pool errors Introduce connPool struct and implement the Pool interface. Add error handling for invalid pool capacity settings and provide a constructor for creating new connection pools with specified capacities. --- connpool.go | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/connpool.go b/connpool.go index 8eabbad..96ba915 100644 --- a/connpool.go +++ b/connpool.go @@ -4,12 +4,20 @@ package mail -import "net" +import ( + "errors" + "net" + "sync" +) // Parts of the connection pool code is forked from https://github.com/fatih/pool/ // Thanks to Fatih Arslan and the project contributors for providing this great // concurrency template. +var ( + ErrPoolInvalidCap = errors.New("invalid connection pool capacity settings") +) + // Pool interface describes a connection pool implementation. A Pool is // thread-/go-routine safe. type Pool interface { @@ -25,3 +33,57 @@ type Pool interface { // Len returns the current number of connections of the pool. Len() int } + +// connPool implements the Pool interface +type connPool struct { + // mutex is used to synchronize access to the connection pool to ensure thread-safe operations + mutex sync.RWMutex + // conns is a channel used to manage and distribute net.Conn objects within the connection pool + conns chan net.Conn + // dialCtx represents the actual net.Conn returned by the DialContextFunc + dialCtx DialContextFunc +} + +// NewConnPool returns a new pool based on buffered channels with an initial +// capacity and maximum capacity. The DialContextFunc is used when the initial +// capacity is greater than zero to fill the pool. A zero initialCap doesn't +// fill the Pool until a new Get() is called. During a Get(), if there is no +// new connection available in the pool, a new connection will be created via +// the corresponding DialContextFunc() method. +func NewConnPool(initialCap, maxCap int, dialCtxFunc DialContextFunc) (Pool, error) { + if initialCap < 0 || maxCap <= 0 || initialCap > maxCap { + return nil, ErrPoolInvalidCap + } + + pool := &connPool{ + conns: make(chan net.Conn, maxCap), + dialCtx: dialCtxFunc, + } + + // create initial connections, if something goes wrong, + // just close the pool error out. + for i := 0; i < initialCap; i++ { + /* + conn, err := dialCtxFunc() + if err != nil { + pool.Close() + return nil, fmt.Errorf("factory is not able to fill the pool: %s", err) + } + c.conns <- conn + + */ + } + + return pool, nil +} + +func (c *connPool) Get() (net.Conn, error) { + return nil, nil +} +func (c *connPool) Close() { + return +} + +func (c *connPool) Len() int { + return 0 +} From 1394f1fc2099fc9d8f1476f406e0346ef24075f0 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 23 Sep 2024 09:56:23 +0200 Subject: [PATCH 038/181] Add context management and error handling to connection pool Introduced context support and enhanced error handling in connpool.go. Added detailed comments for better maintainability and introduced a wrapper for net.Conn to manage connection close behavior. The changes improve the robustness and clarity of the connection pool's operation. --- connpool.go | 134 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 110 insertions(+), 24 deletions(-) diff --git a/connpool.go b/connpool.go index 96ba915..23ed5d0 100644 --- a/connpool.go +++ b/connpool.go @@ -5,7 +5,9 @@ package mail import ( + "context" "errors" + "fmt" "net" "sync" ) @@ -15,7 +17,11 @@ import ( // concurrency template. var ( + // ErrPoolInvalidCap is returned when the connection pool's capacity settings are + // invalid (e.g., initial capacity is negative). ErrPoolInvalidCap = errors.New("invalid connection pool capacity settings") + // ErrClosed is returned when an operation is attempted on a closed connection pool. + ErrClosed = errors.New("connection pool is closed") ) // Pool interface describes a connection pool implementation. A Pool is @@ -36,12 +42,28 @@ type Pool interface { // connPool implements the Pool interface type connPool struct { - // mutex is used to synchronize access to the connection pool to ensure thread-safe operations + // mutex is used to synchronize access to the connection pool to ensure thread-safe operations. mutex sync.RWMutex - // conns is a channel used to manage and distribute net.Conn objects within the connection pool + // conns is a channel used to manage and distribute net.Conn objects within the connection pool. conns chan net.Conn - // dialCtx represents the actual net.Conn returned by the DialContextFunc - dialCtx DialContextFunc + + // dialCtxFunc represents the actual net.Conn returned by the DialContextFunc. + dialCtxFunc DialContextFunc + // dialContext is the context used for dialing new network connections within the connection pool. + dialContext context.Context + // dialNetwork specifies the network type (e.g., "tcp", "udp") used to establish connections in + // the connection pool. + dialNetwork string + // dialAddress specifies the address used to establish network connections within the connection pool. + dialAddress string +} + +// PoolConn is a wrapper around net.Conn to modify the the behavior of net.Conn's Close() method. +type PoolConn struct { + net.Conn + mutex sync.RWMutex + pool *connPool + unusable bool } // NewConnPool returns a new pool based on buffered channels with an initial @@ -50,40 +72,104 @@ type connPool struct { // fill the Pool until a new Get() is called. During a Get(), if there is no // new connection available in the pool, a new connection will be created via // the corresponding DialContextFunc() method. -func NewConnPool(initialCap, maxCap int, dialCtxFunc DialContextFunc) (Pool, error) { +func NewConnPool(ctx context.Context, initialCap, maxCap int, dialCtxFunc DialContextFunc, + network, address string) (Pool, error) { if initialCap < 0 || maxCap <= 0 || initialCap > maxCap { return nil, ErrPoolInvalidCap } pool := &connPool{ - conns: make(chan net.Conn, maxCap), - dialCtx: dialCtxFunc, + conns: make(chan net.Conn, maxCap), + dialCtxFunc: dialCtxFunc, + dialContext: ctx, + dialAddress: address, + dialNetwork: network, } - // create initial connections, if something goes wrong, - // just close the pool error out. + // Initial connections for the pool. Pool will be closed on connection error for i := 0; i < initialCap; i++ { - /* - conn, err := dialCtxFunc() - if err != nil { - pool.Close() - return nil, fmt.Errorf("factory is not able to fill the pool: %s", err) - } - c.conns <- conn + conn, err := dialCtxFunc(ctx, network, address) + if err != nil { + pool.Close() + return nil, fmt.Errorf("dialContextFunc is not able to fill the connection pool: %s", err) + } + pool.conns <- conn - */ } return pool, nil } -func (c *connPool) Get() (net.Conn, error) { - return nil, nil -} -func (c *connPool) Close() { - return +// Get satisfies the Get() method of the Pool inteface. If there is no new +// connection available in the Pool, a new connection will be created via the +// DialContextFunc() method. +func (p *connPool) Get() (net.Conn, error) { + ctx, conns, dialCtxFunc := p.getConnsAndDialContext() + if conns == nil { + return nil, ErrClosed + } + + // wrap the connections into the custom net.Conn implementation that puts + // connections back to the pool + select { + case <-ctx.Done(): + return nil, ctx.Err() + case conn := <-conns: + if conn == nil { + return nil, ErrClosed + } + return p.wrapConn(conn), nil + default: + conn, err := dialCtxFunc(ctx, p.dialNetwork, p.dialAddress) + if err != nil { + return nil, err + } + return p.wrapConn(conn), nil + } } -func (c *connPool) Len() int { - return 0 +// Close terminates all connections in the pool and frees associated resources. Once closed, +// the pool is no longer usable. +func (p *connPool) Close() { + p.mutex.Lock() + conns := p.conns + p.conns = nil + p.dialCtxFunc = nil + p.dialContext = nil + p.dialAddress = "" + p.dialNetwork = "" + p.mutex.Unlock() + + if conns == nil { + return + } + + close(conns) + for conn := range conns { + _ = conn.Close() + } +} + +// Len returns the current number of connections in the connection pool. +func (p *connPool) Len() int { + _, conns, _ := p.getConnsAndDialContext() + return len(conns) +} + +// getConnsAndDialContext returns the connection channel and the DialContext function for the +// connection pool. +func (p *connPool) getConnsAndDialContext() (context.Context, chan net.Conn, DialContextFunc) { + p.mutex.RLock() + conns := p.conns + dialCtxFunc := p.dialCtxFunc + ctx := p.dialContext + p.mutex.RUnlock() + return ctx, conns, dialCtxFunc +} + +// wrapConn wraps a given net.Conn with a PoolConn, modifying the net.Conn's Close() method. +func (p *connPool) wrapConn(conn net.Conn) net.Conn { + poolconn := &PoolConn{pool: p} + poolconn.Conn = conn + return poolconn } From d6e5034bba6b439353ccde76899649996ced08c5 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 23 Sep 2024 10:09:38 +0200 Subject: [PATCH 039/181] Add new error handling and connection management in connpool Introduce ErrClosed and ErrNilConn errors for better error handling. Implement Close and MarkUnusable methods for improved connection lifecycle management. Add put method to return connections to the pool or close them if necessary. --- connpool.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/connpool.go b/connpool.go index 23ed5d0..b22019c 100644 --- a/connpool.go +++ b/connpool.go @@ -17,11 +17,13 @@ import ( // concurrency template. var ( + // ErrClosed is returned when an operation is attempted on a closed connection pool. + ErrClosed = errors.New("connection pool is closed") + // ErrNilConn is returned when a nil connection is passed back to the connection pool. + ErrNilConn = errors.New("connection is nil") // ErrPoolInvalidCap is returned when the connection pool's capacity settings are // invalid (e.g., initial capacity is negative). ErrPoolInvalidCap = errors.New("invalid connection pool capacity settings") - // ErrClosed is returned when an operation is attempted on a closed connection pool. - ErrClosed = errors.New("connection pool is closed") ) // Pool interface describes a connection pool implementation. A Pool is @@ -66,6 +68,28 @@ type PoolConn struct { unusable bool } +// Close puts a given pool connection back to the pool instead of closing it. +func (c *PoolConn) Close() error { + c.mutex.RLock() + defer c.mutex.RUnlock() + + if c.unusable { + if c.Conn != nil { + return c.Conn.Close() + } + return nil + } + return c.pool.put(c.Conn) +} + +// MarkUnusable marks the connection not usable any more, to let the pool close it instead +// of returning it to pool. +func (c *PoolConn) MarkUnusable() { + c.mutex.Lock() + c.unusable = true + c.mutex.Unlock() +} + // NewConnPool returns a new pool based on buffered channels with an initial // capacity and maximum capacity. The DialContextFunc is used when the initial // capacity is greater than zero to fill the pool. A zero initialCap doesn't @@ -167,6 +191,28 @@ func (p *connPool) getConnsAndDialContext() (context.Context, chan net.Conn, Dia return ctx, conns, dialCtxFunc } +// put puts a passed connection back into the pool. If the pool is full or closed, +// conn is simply closed. A nil conn will be rejected with an error. +func (p *connPool) put(conn net.Conn) error { + if conn == nil { + return ErrNilConn + } + + p.mutex.RLock() + defer p.mutex.RUnlock() + + if p.conns == nil { + return conn.Close() + } + + select { + case p.conns <- conn: + return nil + default: + return conn.Close() + } +} + // wrapConn wraps a given net.Conn with a PoolConn, modifying the net.Conn's Close() method. func (p *connPool) wrapConn(conn net.Conn) net.Conn { poolconn := &PoolConn{pool: p} From 33d4eb5b21342dfacd99f12f2a6da2b8b36d362f Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 23 Sep 2024 10:33:06 +0200 Subject: [PATCH 040/181] Add unit tests for connection pool and rename Len to Size Introduced unit tests for the connection pool to ensure robust functionality. Also, renamed the Len method to Size in the Pool interface and its implementation for better clarity and consistency. --- connpool.go | 8 +++---- connpool_test.go | 57 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 connpool_test.go diff --git a/connpool.go b/connpool.go index b22019c..f774806 100644 --- a/connpool.go +++ b/connpool.go @@ -38,8 +38,8 @@ type Pool interface { // no longer usable. Close() - // Len returns the current number of connections of the pool. - Len() int + // Size returns the current number of connections of the pool. + Size() int } // connPool implements the Pool interface @@ -174,8 +174,8 @@ func (p *connPool) Close() { } } -// Len returns the current number of connections in the connection pool. -func (p *connPool) Len() int { +// Size returns the current number of connections in the connection pool. +func (p *connPool) Size() int { _, conns, _ := p.getConnsAndDialContext() return len(conns) } diff --git a/connpool_test.go b/connpool_test.go new file mode 100644 index 0000000..589ad68 --- /dev/null +++ b/connpool_test.go @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2022-2024 The go-mail Authors +// +// SPDX-License-Identifier: MIT + +package mail + +import ( + "context" + "fmt" + "net" + "testing" + "time" +) + +func TestNewConnPool(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + serverPort := TestServerPortBase + 10 + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + pool, err := newConnPool(serverPort) + if err != nil { + t.Errorf("failed to create connection pool: %s", err) + } + if pool == nil { + t.Errorf("connection pool is nil") + return + } + if pool.Size() != 5 { + t.Errorf("expected 5 connections, got %d", pool.Size()) + } + for i := 0; i < 5; i++ { + go func() { + conn, err := pool.Get() + if err != nil { + t.Errorf("failed to get connection: %s", err) + } + if _, err := conn.Write([]byte("EHLO test.localhost.localdomain\r\nQUIT\r\n")); err != nil { + t.Errorf("failed to write quit command to first connection: %s", err) + } + }() + } +} + +func newConnPool(port int) (Pool, error) { + netDialer := net.Dialer{} + return NewConnPool(context.Background(), 5, 30, netDialer.DialContext, "tcp", + fmt.Sprintf("127.0.0.1:%d", port)) +} From 9a9e0c936d23bca6f1789f2add62d4a3666452b6 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 23 Sep 2024 11:09:03 +0200 Subject: [PATCH 041/181] Remove redundant error handling in test code The check for io.EOF and the associated print statement were unnecessary because the loop breaks on any error. This change simplifies the error handling logic in the `client_test.go` file and avoids redundant code. --- client_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client_test.go b/client_test.go index 3d50a2d..fdc486a 100644 --- a/client_test.go +++ b/client_test.go @@ -2100,10 +2100,6 @@ func handleTestServerConnection(connection net.Conn, featureSet string, failRese for { data, err = reader.ReadString('\n') if err != nil { - if errors.Is(err, io.EOF) { - break - } - fmt.Println("Error reading data:", err) break } From 2cbd0c4aefa628093584e9b608044c0f58e9373e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 23 Sep 2024 11:09:11 +0200 Subject: [PATCH 042/181] Add test cases for connection pool functionality Added new test cases `TestConnPool_Get_Type` and `TestConnPool_Get` to verify connection pool operations. These tests ensure proper connection type and handling of pool size after connection retrieval and usage. --- connpool_test.go | 102 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 98 insertions(+), 4 deletions(-) diff --git a/connpool_test.go b/connpool_test.go index 589ad68..67a988f 100644 --- a/connpool_test.go +++ b/connpool_test.go @@ -8,6 +8,7 @@ import ( "context" "fmt" "net" + "sync" "testing" "time" ) @@ -30,6 +31,7 @@ func TestNewConnPool(t *testing.T) { if err != nil { t.Errorf("failed to create connection pool: %s", err) } + defer pool.Close() if pool == nil { t.Errorf("connection pool is nil") return @@ -37,17 +39,109 @@ func TestNewConnPool(t *testing.T) { if pool.Size() != 5 { t.Errorf("expected 5 connections, got %d", pool.Size()) } - for i := 0; i < 5; i++ { + conn, err := pool.Get() + if err != nil { + t.Errorf("failed to get connection: %s", err) + } + if _, err := conn.Write([]byte("EHLO test.localhost.localdomain\r\nQUIT\r\n")); err != nil { + t.Errorf("failed to write quit command to first connection: %s", err) + } +} + +func TestConnPool_Get_Type(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + serverPort := TestServerPortBase + 11 + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + pool, err := newConnPool(serverPort) + if err != nil { + t.Errorf("failed to create connection pool: %s", err) + } + defer pool.Close() + + conn, err := pool.Get() + if err != nil { + t.Errorf("failed to get new connection from pool: %s", err) + return + } + + _, ok := conn.(*PoolConn) + if !ok { + t.Error("received connection from pool is not of type PoolConn") + return + } + if _, err := conn.Write([]byte("EHLO test.localhost.localdomain\r\nQUIT\r\n")); err != nil { + t.Errorf("failed to write quit command to first connection: %s", err) + } +} + +func TestConnPool_Get(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + serverPort := TestServerPortBase + 12 + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + p, _ := newConnPool(serverPort) + defer p.Close() + + conn, err := p.Get() + if err != nil { + t.Errorf("failed to get new connection from pool: %s", err) + return + } + if _, err = conn.Write([]byte("EHLO test.localhost.localdomain\r\nQUIT\r\n")); err != nil { + t.Errorf("failed to write quit command to first connection: %s", err) + } + + if p.Size() != 4 { + t.Errorf("getting new connection from pool failed. Expected pool size: 4, got %d", p.Size()) + } + + var wg sync.WaitGroup + for i := 0; i < 4; i++ { + wg.Add(1) go func() { - conn, err := pool.Get() + defer wg.Done() + wgconn, err := p.Get() if err != nil { - t.Errorf("failed to get connection: %s", err) + t.Errorf("failed to get new connection from pool: %s", err) } - if _, err := conn.Write([]byte("EHLO test.localhost.localdomain\r\nQUIT\r\n")); err != nil { + if _, err = wgconn.Write([]byte("EHLO test.localhost.localdomain\r\nQUIT\r\n")); err != nil { t.Errorf("failed to write quit command to first connection: %s", err) } }() } + wg.Wait() + + if p.Size() != 0 { + t.Errorf("Get error. Expecting 0, got %d", p.Size()) + } + + conn, err = p.Get() + if err != nil { + t.Errorf("failed to get new connection from pool: %s", err) + } + if _, err = conn.Write([]byte("EHLO test.localhost.localdomain\r\nQUIT\r\n")); err != nil { + t.Errorf("failed to write quit command to first connection: %s", err) + } + p.Close() } func newConnPool(port int) (Pool, error) { From f1188bdad7d2a494597e96b5d651b07e8c522a15 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 23 Sep 2024 11:15:56 +0200 Subject: [PATCH 043/181] Add test for PoolConn Close method This commit introduces a new test, `TestPoolConn_Close`, to verify that connections are correctly closed and returned to the pool. It sets up a simple SMTP server, creates a connection pool, tests writing to and closing connections, and checks the pool size to ensure proper behavior. --- connpool_test.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/connpool_test.go b/connpool_test.go index 67a988f..536dffc 100644 --- a/connpool_test.go +++ b/connpool_test.go @@ -144,6 +144,59 @@ func TestConnPool_Get(t *testing.T) { p.Close() } +func TestPoolConn_Close(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + serverPort := TestServerPortBase + 13 + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + netDialer := net.Dialer{} + p, err := NewConnPool(context.Background(), 0, 30, netDialer.DialContext, "tcp", + fmt.Sprintf("127.0.0.1:%d", serverPort)) + if err != nil { + t.Errorf("failed to create connection pool: %s", err) + } + defer p.Close() + + conns := make([]net.Conn, 30) + for i := 0; i < 30; i++ { + conn, _ := p.Get() + if _, err = conn.Write([]byte("EHLO test.localhost.localdomain\r\nQUIT\r\n")); err != nil { + t.Errorf("failed to write quit command to first connection: %s", err) + } + conns[i] = conn + } + for _, conn := range conns { + conn.Close() + } + + if p.Size() != 30 { + t.Errorf("failed to return all connections to pool. Expected pool size: 30, got %d", p.Size()) + } + + conn, err := p.Get() + if err != nil { + t.Errorf("failed to get new connection from pool: %s", err) + } + if _, err = conn.Write([]byte("EHLO test.localhost.localdomain\r\nQUIT\r\n")); err != nil { + t.Errorf("failed to write quit command to first connection: %s", err) + } + p.Close() + + conn.Close() + if p.Size() != 0 { + t.Errorf("closed pool shouldn't allow to put connections.") + } +} + func newConnPool(port int) (Pool, error) { netDialer := net.Dialer{} return NewConnPool(context.Background(), 5, 30, netDialer.DialContext, "tcp", From 774925078a7af5342186960eddb0c60ea1000cdd Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 23 Sep 2024 11:17:58 +0200 Subject: [PATCH 044/181] Improve error handling in connection pool tests Add error checks to Close() calls in connpool_test.go to ensure connection closures are handled properly, with descriptive error messages. Update comment in connpool.go to improve clarity on the source of code inspiration. --- connpool.go | 5 ++--- connpool_test.go | 8 ++++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/connpool.go b/connpool.go index f774806..d875793 100644 --- a/connpool.go +++ b/connpool.go @@ -12,9 +12,8 @@ import ( "sync" ) -// Parts of the connection pool code is forked from https://github.com/fatih/pool/ -// Thanks to Fatih Arslan and the project contributors for providing this great -// concurrency template. +// Parts of the connection pool code is forked/took inspiration from https://github.com/fatih/pool/ +// Thanks to Fatih Arslan and the project contributors for providing this great concurrency template. var ( // ErrClosed is returned when an operation is attempted on a closed connection pool. diff --git a/connpool_test.go b/connpool_test.go index 536dffc..4baf084 100644 --- a/connpool_test.go +++ b/connpool_test.go @@ -175,7 +175,9 @@ func TestPoolConn_Close(t *testing.T) { conns[i] = conn } for _, conn := range conns { - conn.Close() + if err = conn.Close(); err != nil { + t.Errorf("failed to close connection: %s", err) + } } if p.Size() != 30 { @@ -191,7 +193,9 @@ func TestPoolConn_Close(t *testing.T) { } p.Close() - conn.Close() + if err = conn.Close(); err != nil { + t.Errorf("failed to close connection: %s", err) + } if p.Size() != 0 { t.Errorf("closed pool shouldn't allow to put connections.") } From 5503be8451fae20a55db907b174c5dde1fbc6b84 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 23 Sep 2024 11:26:54 +0200 Subject: [PATCH 045/181] Remove redundant error print statements Removed redundant fmt.Printf error print statements for connection read and write errors. This cleans up the test output and makes error handling more streamlined. --- client_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/client_test.go b/client_test.go index fdc486a..c891a10 100644 --- a/client_test.go +++ b/client_test.go @@ -2085,7 +2085,6 @@ func handleTestServerConnection(connection net.Conn, featureSet string, failRese data, err := reader.ReadString('\n') if err != nil { - fmt.Printf("unable to read from connection: %s\n", err) return } if !strings.HasPrefix(data, "EHLO") && !strings.HasPrefix(data, "HELO") { @@ -2093,7 +2092,6 @@ func handleTestServerConnection(connection net.Conn, featureSet string, failRese return } if err = writeLine("250-localhost.localdomain\r\n" + featureSet); err != nil { - fmt.Printf("unable to write to connection: %s\n", err) return } From 2abdee743d5b3fbfad694574259a4a49bf6849e7 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 23 Sep 2024 11:27:05 +0200 Subject: [PATCH 046/181] Add unit test for marking connection as unusable Introduces `TestPoolConn_MarkUnusable` to ensure the pool maintains its integrity when a connection is marked unusable. This test validates that the connection pool size adjusts correctly after marking a connection as unusable and closing it. --- connpool_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/connpool_test.go b/connpool_test.go index 4baf084..d03062f 100644 --- a/connpool_test.go +++ b/connpool_test.go @@ -201,6 +201,60 @@ func TestPoolConn_Close(t *testing.T) { } } +func TestPoolConn_MarkUnusable(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + serverPort := TestServerPortBase + 14 + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + pool, _ := newConnPool(serverPort) + defer pool.Close() + + conn, err := pool.Get() + if err != nil { + t.Errorf("failed to get new connection from pool: %s", err) + } + if err = conn.Close(); err != nil { + t.Errorf("failed to close connection: %s", err) + } + + poolSize := pool.Size() + conn, err = pool.Get() + if err != nil { + t.Errorf("failed to get new connection from pool: %s", err) + } + if err = conn.Close(); err != nil { + t.Errorf("failed to close connection: %s", err) + } + if pool.Size() != poolSize { + t.Errorf("pool size is expected to be equal to initial size") + } + + conn, err = pool.Get() + if err != nil { + t.Errorf("failed to get new connection from pool: %s", err) + } + if pc, ok := conn.(*PoolConn); !ok { + t.Errorf("this should never happen") + } else { + pc.MarkUnusable() + } + if err = conn.Close(); err != nil { + t.Errorf("failed to close connection: %s", err) + } + if pool.Size() != poolSize-1 { + t.Errorf("pool size is expected to be: %d but got: %d", poolSize-1, pool.Size()) + } +} + func newConnPool(port int) (Pool, error) { netDialer := net.Dialer{} return NewConnPool(context.Background(), 5, 30, netDialer.DialContext, "tcp", From 07e7b17ae8a073feda2288cf38e8d865975e3a89 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 23 Sep 2024 11:45:22 +0200 Subject: [PATCH 047/181] Add tests for ConnPool close and concurrency issues This commit introduces two new tests: `TestConnPool_Close` and `TestConnPool_Concurrency`. The former ensures the proper closing of connection pool resources, while the latter checks for concurrency issues by creating and closing multiple connections in parallel. --- connpool_test.go | 100 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/connpool_test.go b/connpool_test.go index d03062f..4450e8c 100644 --- a/connpool_test.go +++ b/connpool_test.go @@ -255,6 +255,106 @@ func TestPoolConn_MarkUnusable(t *testing.T) { } } +func TestConnPool_Close(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + serverPort := TestServerPortBase + 15 + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + pool, err := newConnPool(serverPort) + if err != nil { + t.Errorf("failed to create connection pool: %s", err) + } + pool.Close() + + castPool := pool.(*connPool) + + if castPool.conns != nil { + t.Error("closing pool failed: conns channel should be nil") + } + if castPool.dialCtxFunc != nil { + t.Error("closing pool failed: dialCtxFunc should be nil") + } + if castPool.dialContext != nil { + t.Error("closing pool failed: dialContext should be nil") + } + if castPool.dialAddress != "" { + t.Error("closing pool failed: dialAddress should be empty") + } + if castPool.dialNetwork != "" { + t.Error("closing pool failed: dialNetwork should be empty") + } + + conn, err := pool.Get() + if err == nil { + t.Errorf("closing pool failed: getting new connection should return an error") + } + if conn != nil { + t.Errorf("closing pool failed: getting new connection should return a nil-connection") + } + if pool.Size() != 0 { + t.Errorf("closing pool failed: pool size should be 0, but got: %d", pool.Size()) + } +} + +func TestConnPool_Concurrency(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + serverPort := TestServerPortBase + 16 + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + pool, err := newConnPool(serverPort) + if err != nil { + t.Errorf("failed to create connection pool: %s", err) + } + defer pool.Close() + pipe := make(chan net.Conn) + + getWg := sync.WaitGroup{} + closeWg := sync.WaitGroup{} + for i := 0; i < 30; i++ { + getWg.Add(1) + closeWg.Add(1) + go func() { + conn, err := pool.Get() + if err != nil { + t.Errorf("failed to get new connection from pool: %s", err) + } + pipe <- conn + getWg.Done() + }() + + go func() { + conn := <-pipe + if conn == nil { + return + } + if err = conn.Close(); err != nil { + t.Errorf("failed to close connection: %s", err) + } + closeWg.Done() + }() + getWg.Wait() + closeWg.Wait() + } +} + func newConnPool(port int) (Pool, error) { netDialer := net.Dialer{} return NewConnPool(context.Background(), 5, 30, netDialer.DialContext, "tcp", From c8684886ed23d2a42b1ab178400c7a7633650e47 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 23 Sep 2024 13:44:03 +0200 Subject: [PATCH 048/181] Refactor Get method to include context argument Updated the Get method in connpool.go and its usage in tests to include a context argument for better cancellation and timeout handling. Removed the redundant dialContext field from the connection pool struct and added a new test to validate context timeout behavior. --- connpool.go | 27 ++++++-------- connpool_test.go | 91 ++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 87 insertions(+), 31 deletions(-) diff --git a/connpool.go b/connpool.go index d875793..f50e73d 100644 --- a/connpool.go +++ b/connpool.go @@ -31,7 +31,7 @@ type Pool interface { // Get returns a new connection from the pool. Closing the connections returns // it back into the Pool. Closing a connection when the Pool is destroyed or // full will be counted as an error. - Get() (net.Conn, error) + Get(ctx context.Context) (net.Conn, error) // Close closes the pool and all its connections. After Close() the pool is // no longer usable. @@ -50,8 +50,6 @@ type connPool struct { // dialCtxFunc represents the actual net.Conn returned by the DialContextFunc. dialCtxFunc DialContextFunc - // dialContext is the context used for dialing new network connections within the connection pool. - dialContext context.Context // dialNetwork specifies the network type (e.g., "tcp", "udp") used to establish connections in // the connection pool. dialNetwork string @@ -96,7 +94,8 @@ func (c *PoolConn) MarkUnusable() { // new connection available in the pool, a new connection will be created via // the corresponding DialContextFunc() method. func NewConnPool(ctx context.Context, initialCap, maxCap int, dialCtxFunc DialContextFunc, - network, address string) (Pool, error) { + network, address string, +) (Pool, error) { if initialCap < 0 || maxCap <= 0 || initialCap > maxCap { return nil, ErrPoolInvalidCap } @@ -104,7 +103,6 @@ func NewConnPool(ctx context.Context, initialCap, maxCap int, dialCtxFunc DialCo pool := &connPool{ conns: make(chan net.Conn, maxCap), dialCtxFunc: dialCtxFunc, - dialContext: ctx, dialAddress: address, dialNetwork: network, } @@ -114,10 +112,9 @@ func NewConnPool(ctx context.Context, initialCap, maxCap int, dialCtxFunc DialCo conn, err := dialCtxFunc(ctx, network, address) if err != nil { pool.Close() - return nil, fmt.Errorf("dialContextFunc is not able to fill the connection pool: %s", err) + return nil, fmt.Errorf("dialContextFunc is not able to fill the connection pool: %w", err) } pool.conns <- conn - } return pool, nil @@ -126,8 +123,8 @@ func NewConnPool(ctx context.Context, initialCap, maxCap int, dialCtxFunc DialCo // Get satisfies the Get() method of the Pool inteface. If there is no new // connection available in the Pool, a new connection will be created via the // DialContextFunc() method. -func (p *connPool) Get() (net.Conn, error) { - ctx, conns, dialCtxFunc := p.getConnsAndDialContext() +func (p *connPool) Get(ctx context.Context) (net.Conn, error) { + conns, dialCtxFunc := p.getConnsAndDialContext() if conns == nil { return nil, ErrClosed } @@ -136,7 +133,7 @@ func (p *connPool) Get() (net.Conn, error) { // connections back to the pool select { case <-ctx.Done(): - return nil, ctx.Err() + return nil, fmt.Errorf("failed to get connection: %w", ctx.Err()) case conn := <-conns: if conn == nil { return nil, ErrClosed @@ -145,7 +142,7 @@ func (p *connPool) Get() (net.Conn, error) { default: conn, err := dialCtxFunc(ctx, p.dialNetwork, p.dialAddress) if err != nil { - return nil, err + return nil, fmt.Errorf("dialContextFunc failed: %w", err) } return p.wrapConn(conn), nil } @@ -158,7 +155,6 @@ func (p *connPool) Close() { conns := p.conns p.conns = nil p.dialCtxFunc = nil - p.dialContext = nil p.dialAddress = "" p.dialNetwork = "" p.mutex.Unlock() @@ -175,19 +171,18 @@ func (p *connPool) Close() { // Size returns the current number of connections in the connection pool. func (p *connPool) Size() int { - _, conns, _ := p.getConnsAndDialContext() + conns, _ := p.getConnsAndDialContext() return len(conns) } // getConnsAndDialContext returns the connection channel and the DialContext function for the // connection pool. -func (p *connPool) getConnsAndDialContext() (context.Context, chan net.Conn, DialContextFunc) { +func (p *connPool) getConnsAndDialContext() (chan net.Conn, DialContextFunc) { p.mutex.RLock() conns := p.conns dialCtxFunc := p.dialCtxFunc - ctx := p.dialContext p.mutex.RUnlock() - return ctx, conns, dialCtxFunc + return conns, dialCtxFunc } // put puts a passed connection back into the pool. If the pool is full or closed, diff --git a/connpool_test.go b/connpool_test.go index 4450e8c..1f42ff6 100644 --- a/connpool_test.go +++ b/connpool_test.go @@ -39,7 +39,7 @@ func TestNewConnPool(t *testing.T) { if pool.Size() != 5 { t.Errorf("expected 5 connections, got %d", pool.Size()) } - conn, err := pool.Get() + conn, err := pool.Get(context.Background()) if err != nil { t.Errorf("failed to get connection: %s", err) } @@ -68,7 +68,7 @@ func TestConnPool_Get_Type(t *testing.T) { } defer pool.Close() - conn, err := pool.Get() + conn, err := pool.Get(context.Background()) if err != nil { t.Errorf("failed to get new connection from pool: %s", err) return @@ -101,7 +101,7 @@ func TestConnPool_Get(t *testing.T) { p, _ := newConnPool(serverPort) defer p.Close() - conn, err := p.Get() + conn, err := p.Get(context.Background()) if err != nil { t.Errorf("failed to get new connection from pool: %s", err) return @@ -119,7 +119,7 @@ func TestConnPool_Get(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - wgconn, err := p.Get() + wgconn, err := p.Get(context.Background()) if err != nil { t.Errorf("failed to get new connection from pool: %s", err) } @@ -134,7 +134,7 @@ func TestConnPool_Get(t *testing.T) { t.Errorf("Get error. Expecting 0, got %d", p.Size()) } - conn, err = p.Get() + conn, err = p.Get(context.Background()) if err != nil { t.Errorf("failed to get new connection from pool: %s", err) } @@ -168,7 +168,7 @@ func TestPoolConn_Close(t *testing.T) { conns := make([]net.Conn, 30) for i := 0; i < 30; i++ { - conn, _ := p.Get() + conn, _ := p.Get(context.Background()) if _, err = conn.Write([]byte("EHLO test.localhost.localdomain\r\nQUIT\r\n")); err != nil { t.Errorf("failed to write quit command to first connection: %s", err) } @@ -184,7 +184,7 @@ func TestPoolConn_Close(t *testing.T) { t.Errorf("failed to return all connections to pool. Expected pool size: 30, got %d", p.Size()) } - conn, err := p.Get() + conn, err := p.Get(context.Background()) if err != nil { t.Errorf("failed to get new connection from pool: %s", err) } @@ -218,7 +218,7 @@ func TestPoolConn_MarkUnusable(t *testing.T) { pool, _ := newConnPool(serverPort) defer pool.Close() - conn, err := pool.Get() + conn, err := pool.Get(context.Background()) if err != nil { t.Errorf("failed to get new connection from pool: %s", err) } @@ -227,7 +227,7 @@ func TestPoolConn_MarkUnusable(t *testing.T) { } poolSize := pool.Size() - conn, err = pool.Get() + conn, err = pool.Get(context.Background()) if err != nil { t.Errorf("failed to get new connection from pool: %s", err) } @@ -238,7 +238,7 @@ func TestPoolConn_MarkUnusable(t *testing.T) { t.Errorf("pool size is expected to be equal to initial size") } - conn, err = pool.Get() + conn, err = pool.Get(context.Background()) if err != nil { t.Errorf("failed to get new connection from pool: %s", err) } @@ -283,9 +283,6 @@ func TestConnPool_Close(t *testing.T) { if castPool.dialCtxFunc != nil { t.Error("closing pool failed: dialCtxFunc should be nil") } - if castPool.dialContext != nil { - t.Error("closing pool failed: dialContext should be nil") - } if castPool.dialAddress != "" { t.Error("closing pool failed: dialAddress should be empty") } @@ -293,7 +290,7 @@ func TestConnPool_Close(t *testing.T) { t.Error("closing pool failed: dialNetwork should be empty") } - conn, err := pool.Get() + conn, err := pool.Get(context.Background()) if err == nil { t.Errorf("closing pool failed: getting new connection should return an error") } @@ -332,7 +329,7 @@ func TestConnPool_Concurrency(t *testing.T) { getWg.Add(1) closeWg.Add(1) go func() { - conn, err := pool.Get() + conn, err := pool.Get(context.Background()) if err != nil { t.Errorf("failed to get new connection from pool: %s", err) } @@ -355,6 +352,70 @@ func TestConnPool_Concurrency(t *testing.T) { } } +func TestConnPool_GetContextTimeout(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + serverPort := TestServerPortBase + 17 + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + p, err := newConnPool(serverPort) + if err != nil { + t.Errorf("failed to create connection pool: %s", err) + } + defer p.Close() + + connCtx, connCancel := context.WithCancel(context.Background()) + defer connCancel() + + conn, err := p.Get(connCtx) + if err != nil { + t.Errorf("failed to get new connection from pool: %s", err) + return + } + if _, err = conn.Write([]byte("EHLO test.localhost.localdomain\r\nQUIT\r\n")); err != nil { + t.Errorf("failed to write quit command to first connection: %s", err) + } + + if p.Size() != 4 { + t.Errorf("getting new connection from pool failed. Expected pool size: 4, got %d", p.Size()) + } + + var wg sync.WaitGroup + for i := 0; i < 4; i++ { + wg.Add(1) + go func() { + defer wg.Done() + wgconn, err := p.Get(connCtx) + if err != nil { + t.Errorf("failed to get new connection from pool: %s", err) + } + if _, err = wgconn.Write([]byte("EHLO test.localhost.localdomain\r\nQUIT\r\n")); err != nil { + t.Errorf("failed to write quit command to first connection: %s", err) + } + }() + } + wg.Wait() + + if p.Size() != 0 { + t.Errorf("Get error. Expecting 0, got %d", p.Size()) + } + + connCancel() + _, err = p.Get(connCtx) + if err == nil { + t.Errorf("getting new connection on canceled context should fail, but didn't") + } + p.Close() +} + func newConnPool(port int) (Pool, error) { netDialer := net.Dialer{} return NewConnPool(context.Background(), 5, 30, netDialer.DialContext, "tcp", From 4f6224131ef450d62ece29e47dc1fd0b6ba8c52d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 23 Sep 2024 13:46:41 +0200 Subject: [PATCH 049/181] Rename test for accurate context cancellation Updated the test name from `TestConnPool_GetContextTimeout` to `TestConnPool_GetContextCancel` to better reflect its functionality. This change improves test readability and maintains consistency with the context usage in the test. --- connpool_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connpool_test.go b/connpool_test.go index 1f42ff6..42b26c8 100644 --- a/connpool_test.go +++ b/connpool_test.go @@ -352,7 +352,7 @@ func TestConnPool_Concurrency(t *testing.T) { } } -func TestConnPool_GetContextTimeout(t *testing.T) { +func TestConnPool_GetContextCancel(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() From fd115d5173d7811397ef32a1aac158a44b00c3fb Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 23 Sep 2024 14:15:43 +0200 Subject: [PATCH 050/181] Remove typo from comment in smtp_ehlo_117.go Fixed a typo in the backward compatibility comment for Go 1.16/1.17 in smtp_ehlo_117.go. This ensures clarity and correctness in documentation. --- smtp/smtp_ehlo_117.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smtp/smtp_ehlo_117.go b/smtp/smtp_ehlo_117.go index 429f30a..c516a36 100644 --- a/smtp/smtp_ehlo_117.go +++ b/smtp/smtp_ehlo_117.go @@ -22,7 +22,7 @@ import "strings" // should be the preferred greeting for servers that support it. // // Backport of: https://github.com/golang/go/commit/4d8db00641cc9ff4f44de7df9b8c4f4a4f9416ee#diff-4f6f6bdb9891d4dd271f9f31430420a2e44018fe4ee539576faf458bebb3cee4 -// to guarantee backwards compatibility with Go 1.16/1.17:w +// to guarantee backwards compatibility with Go 1.16/1.17 func (c *Client) ehlo() error { _, msg, err := c.cmd(250, "EHLO %s", c.localName) if err != nil { From d6725b2d630a97ae559f93d76542d3d478d12069 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:48:36 +0000 Subject: [PATCH 051/181] Bump sonarsource/sonarqube-scan-action Bumps [sonarsource/sonarqube-scan-action](https://github.com/sonarsource/sonarqube-scan-action) from 0c0f3958d90fc466625f1d1af1f47bddd4cc6bd1 to f885e52a7572cf7943f28637e75730227df2dbf2. - [Release notes](https://github.com/sonarsource/sonarqube-scan-action/releases) - [Commits](https://github.com/sonarsource/sonarqube-scan-action/compare/0c0f3958d90fc466625f1d1af1f47bddd4cc6bd1...f885e52a7572cf7943f28637e75730227df2dbf2) --- updated-dependencies: - dependency-name: sonarsource/sonarqube-scan-action dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- .github/workflows/sonarqube.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index df060c3..2c549f0 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -44,7 +44,7 @@ jobs: run: | go test -v -race --coverprofile=./cov.out ./... - - uses: sonarsource/sonarqube-scan-action@0c0f3958d90fc466625f1d1af1f47bddd4cc6bd1 # master + - uses: sonarsource/sonarqube-scan-action@f885e52a7572cf7943f28637e75730227df2dbf2 # master env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} From d75d9901248e75c4dfe9b08a91560105c8d64092 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Sep 2024 13:58:36 +0000 Subject: [PATCH 052/181] Bump sonarsource/sonarqube-scan-action Bumps [sonarsource/sonarqube-scan-action](https://github.com/sonarsource/sonarqube-scan-action) from f885e52a7572cf7943f28637e75730227df2dbf2 to 884b79409bbd464b2a59edc326a4b77dc56b2195. - [Release notes](https://github.com/sonarsource/sonarqube-scan-action/releases) - [Commits](https://github.com/sonarsource/sonarqube-scan-action/compare/f885e52a7572cf7943f28637e75730227df2dbf2...884b79409bbd464b2a59edc326a4b77dc56b2195) --- updated-dependencies: - dependency-name: sonarsource/sonarqube-scan-action dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- .github/workflows/sonarqube.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 2c549f0..0f54b09 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -44,7 +44,7 @@ jobs: run: | go test -v -race --coverprofile=./cov.out ./... - - uses: sonarsource/sonarqube-scan-action@f885e52a7572cf7943f28637e75730227df2dbf2 # master + - uses: sonarsource/sonarqube-scan-action@884b79409bbd464b2a59edc326a4b77dc56b2195 # master env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} From b97073db199c825aaab609d5e314e5e5e20ffc28 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Sep 2024 13:58:44 +0000 Subject: [PATCH 053/181] Bump github/codeql-action from 3.26.8 to 3.26.9 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.8 to 3.26.9. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/294a9d92911152fe08befb9ec03e240add280cb3...461ef6c76dfe95d5c364de2f431ddbd31a417628) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index cac000e..4c0c477 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -54,7 +54,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 + uses: github/codeql-action/init@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 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@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 + uses: github/codeql-action/autobuild@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 # ℹī¸ 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@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 + uses: github/codeql-action/analyze@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 8bbf2c6..5d17aa0 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -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@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 + uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 with: sarif_file: results.sarif From 8683917c3dec1316b9349ca8546eeb9d5d40d3f6 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 26 Sep 2024 11:49:48 +0200 Subject: [PATCH 054/181] Delete connection pool implementation and tests Remove `connpool.go` and `connpool_test.go`. This eliminates the connection pool feature from the codebase, including associated functionality and tests. The connection pool feature is much to complex and doesn't provide the benefits expected by the concurrency feature --- connpool.go | 215 ------------------------ connpool_test.go | 423 ----------------------------------------------- 2 files changed, 638 deletions(-) delete mode 100644 connpool.go delete mode 100644 connpool_test.go diff --git a/connpool.go b/connpool.go deleted file mode 100644 index f50e73d..0000000 --- a/connpool.go +++ /dev/null @@ -1,215 +0,0 @@ -// SPDX-FileCopyrightText: 2022-2024 The go-mail Authors -// -// SPDX-License-Identifier: MIT - -package mail - -import ( - "context" - "errors" - "fmt" - "net" - "sync" -) - -// Parts of the connection pool code is forked/took inspiration from https://github.com/fatih/pool/ -// Thanks to Fatih Arslan and the project contributors for providing this great concurrency template. - -var ( - // ErrClosed is returned when an operation is attempted on a closed connection pool. - ErrClosed = errors.New("connection pool is closed") - // ErrNilConn is returned when a nil connection is passed back to the connection pool. - ErrNilConn = errors.New("connection is nil") - // ErrPoolInvalidCap is returned when the connection pool's capacity settings are - // invalid (e.g., initial capacity is negative). - ErrPoolInvalidCap = errors.New("invalid connection pool capacity settings") -) - -// Pool interface describes a connection pool implementation. A Pool is -// thread-/go-routine safe. -type Pool interface { - // Get returns a new connection from the pool. Closing the connections returns - // it back into the Pool. Closing a connection when the Pool is destroyed or - // full will be counted as an error. - Get(ctx context.Context) (net.Conn, error) - - // Close closes the pool and all its connections. After Close() the pool is - // no longer usable. - Close() - - // Size returns the current number of connections of the pool. - Size() int -} - -// connPool implements the Pool interface -type connPool struct { - // mutex is used to synchronize access to the connection pool to ensure thread-safe operations. - mutex sync.RWMutex - // conns is a channel used to manage and distribute net.Conn objects within the connection pool. - conns chan net.Conn - - // dialCtxFunc represents the actual net.Conn returned by the DialContextFunc. - dialCtxFunc DialContextFunc - // dialNetwork specifies the network type (e.g., "tcp", "udp") used to establish connections in - // the connection pool. - dialNetwork string - // dialAddress specifies the address used to establish network connections within the connection pool. - dialAddress string -} - -// PoolConn is a wrapper around net.Conn to modify the the behavior of net.Conn's Close() method. -type PoolConn struct { - net.Conn - mutex sync.RWMutex - pool *connPool - unusable bool -} - -// Close puts a given pool connection back to the pool instead of closing it. -func (c *PoolConn) Close() error { - c.mutex.RLock() - defer c.mutex.RUnlock() - - if c.unusable { - if c.Conn != nil { - return c.Conn.Close() - } - return nil - } - return c.pool.put(c.Conn) -} - -// MarkUnusable marks the connection not usable any more, to let the pool close it instead -// of returning it to pool. -func (c *PoolConn) MarkUnusable() { - c.mutex.Lock() - c.unusable = true - c.mutex.Unlock() -} - -// NewConnPool returns a new pool based on buffered channels with an initial -// capacity and maximum capacity. The DialContextFunc is used when the initial -// capacity is greater than zero to fill the pool. A zero initialCap doesn't -// fill the Pool until a new Get() is called. During a Get(), if there is no -// new connection available in the pool, a new connection will be created via -// the corresponding DialContextFunc() method. -func NewConnPool(ctx context.Context, initialCap, maxCap int, dialCtxFunc DialContextFunc, - network, address string, -) (Pool, error) { - if initialCap < 0 || maxCap <= 0 || initialCap > maxCap { - return nil, ErrPoolInvalidCap - } - - pool := &connPool{ - conns: make(chan net.Conn, maxCap), - dialCtxFunc: dialCtxFunc, - dialAddress: address, - dialNetwork: network, - } - - // Initial connections for the pool. Pool will be closed on connection error - for i := 0; i < initialCap; i++ { - conn, err := dialCtxFunc(ctx, network, address) - if err != nil { - pool.Close() - return nil, fmt.Errorf("dialContextFunc is not able to fill the connection pool: %w", err) - } - pool.conns <- conn - } - - return pool, nil -} - -// Get satisfies the Get() method of the Pool inteface. If there is no new -// connection available in the Pool, a new connection will be created via the -// DialContextFunc() method. -func (p *connPool) Get(ctx context.Context) (net.Conn, error) { - conns, dialCtxFunc := p.getConnsAndDialContext() - if conns == nil { - return nil, ErrClosed - } - - // wrap the connections into the custom net.Conn implementation that puts - // connections back to the pool - select { - case <-ctx.Done(): - return nil, fmt.Errorf("failed to get connection: %w", ctx.Err()) - case conn := <-conns: - if conn == nil { - return nil, ErrClosed - } - return p.wrapConn(conn), nil - default: - conn, err := dialCtxFunc(ctx, p.dialNetwork, p.dialAddress) - if err != nil { - return nil, fmt.Errorf("dialContextFunc failed: %w", err) - } - return p.wrapConn(conn), nil - } -} - -// Close terminates all connections in the pool and frees associated resources. Once closed, -// the pool is no longer usable. -func (p *connPool) Close() { - p.mutex.Lock() - conns := p.conns - p.conns = nil - p.dialCtxFunc = nil - p.dialAddress = "" - p.dialNetwork = "" - p.mutex.Unlock() - - if conns == nil { - return - } - - close(conns) - for conn := range conns { - _ = conn.Close() - } -} - -// Size returns the current number of connections in the connection pool. -func (p *connPool) Size() int { - conns, _ := p.getConnsAndDialContext() - return len(conns) -} - -// getConnsAndDialContext returns the connection channel and the DialContext function for the -// connection pool. -func (p *connPool) getConnsAndDialContext() (chan net.Conn, DialContextFunc) { - p.mutex.RLock() - conns := p.conns - dialCtxFunc := p.dialCtxFunc - p.mutex.RUnlock() - return conns, dialCtxFunc -} - -// put puts a passed connection back into the pool. If the pool is full or closed, -// conn is simply closed. A nil conn will be rejected with an error. -func (p *connPool) put(conn net.Conn) error { - if conn == nil { - return ErrNilConn - } - - p.mutex.RLock() - defer p.mutex.RUnlock() - - if p.conns == nil { - return conn.Close() - } - - select { - case p.conns <- conn: - return nil - default: - return conn.Close() - } -} - -// wrapConn wraps a given net.Conn with a PoolConn, modifying the net.Conn's Close() method. -func (p *connPool) wrapConn(conn net.Conn) net.Conn { - poolconn := &PoolConn{pool: p} - poolconn.Conn = conn - return poolconn -} diff --git a/connpool_test.go b/connpool_test.go deleted file mode 100644 index 42b26c8..0000000 --- a/connpool_test.go +++ /dev/null @@ -1,423 +0,0 @@ -// SPDX-FileCopyrightText: 2022-2024 The go-mail Authors -// -// SPDX-License-Identifier: MIT - -package mail - -import ( - "context" - "fmt" - "net" - "sync" - "testing" - "time" -) - -func TestNewConnPool(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - serverPort := TestServerPortBase + 10 - featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" - go func() { - if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 300) - - pool, err := newConnPool(serverPort) - if err != nil { - t.Errorf("failed to create connection pool: %s", err) - } - defer pool.Close() - if pool == nil { - t.Errorf("connection pool is nil") - return - } - if pool.Size() != 5 { - t.Errorf("expected 5 connections, got %d", pool.Size()) - } - conn, err := pool.Get(context.Background()) - if err != nil { - t.Errorf("failed to get connection: %s", err) - } - if _, err := conn.Write([]byte("EHLO test.localhost.localdomain\r\nQUIT\r\n")); err != nil { - t.Errorf("failed to write quit command to first connection: %s", err) - } -} - -func TestConnPool_Get_Type(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - serverPort := TestServerPortBase + 11 - featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" - go func() { - if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 300) - - pool, err := newConnPool(serverPort) - if err != nil { - t.Errorf("failed to create connection pool: %s", err) - } - defer pool.Close() - - conn, err := pool.Get(context.Background()) - if err != nil { - t.Errorf("failed to get new connection from pool: %s", err) - return - } - - _, ok := conn.(*PoolConn) - if !ok { - t.Error("received connection from pool is not of type PoolConn") - return - } - if _, err := conn.Write([]byte("EHLO test.localhost.localdomain\r\nQUIT\r\n")); err != nil { - t.Errorf("failed to write quit command to first connection: %s", err) - } -} - -func TestConnPool_Get(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - serverPort := TestServerPortBase + 12 - featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" - go func() { - if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 300) - - p, _ := newConnPool(serverPort) - defer p.Close() - - conn, err := p.Get(context.Background()) - if err != nil { - t.Errorf("failed to get new connection from pool: %s", err) - return - } - if _, err = conn.Write([]byte("EHLO test.localhost.localdomain\r\nQUIT\r\n")); err != nil { - t.Errorf("failed to write quit command to first connection: %s", err) - } - - if p.Size() != 4 { - t.Errorf("getting new connection from pool failed. Expected pool size: 4, got %d", p.Size()) - } - - var wg sync.WaitGroup - for i := 0; i < 4; i++ { - wg.Add(1) - go func() { - defer wg.Done() - wgconn, err := p.Get(context.Background()) - if err != nil { - t.Errorf("failed to get new connection from pool: %s", err) - } - if _, err = wgconn.Write([]byte("EHLO test.localhost.localdomain\r\nQUIT\r\n")); err != nil { - t.Errorf("failed to write quit command to first connection: %s", err) - } - }() - } - wg.Wait() - - if p.Size() != 0 { - t.Errorf("Get error. Expecting 0, got %d", p.Size()) - } - - conn, err = p.Get(context.Background()) - if err != nil { - t.Errorf("failed to get new connection from pool: %s", err) - } - if _, err = conn.Write([]byte("EHLO test.localhost.localdomain\r\nQUIT\r\n")); err != nil { - t.Errorf("failed to write quit command to first connection: %s", err) - } - p.Close() -} - -func TestPoolConn_Close(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - serverPort := TestServerPortBase + 13 - featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" - go func() { - if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 300) - - netDialer := net.Dialer{} - p, err := NewConnPool(context.Background(), 0, 30, netDialer.DialContext, "tcp", - fmt.Sprintf("127.0.0.1:%d", serverPort)) - if err != nil { - t.Errorf("failed to create connection pool: %s", err) - } - defer p.Close() - - conns := make([]net.Conn, 30) - for i := 0; i < 30; i++ { - conn, _ := p.Get(context.Background()) - if _, err = conn.Write([]byte("EHLO test.localhost.localdomain\r\nQUIT\r\n")); err != nil { - t.Errorf("failed to write quit command to first connection: %s", err) - } - conns[i] = conn - } - for _, conn := range conns { - if err = conn.Close(); err != nil { - t.Errorf("failed to close connection: %s", err) - } - } - - if p.Size() != 30 { - t.Errorf("failed to return all connections to pool. Expected pool size: 30, got %d", p.Size()) - } - - conn, err := p.Get(context.Background()) - if err != nil { - t.Errorf("failed to get new connection from pool: %s", err) - } - if _, err = conn.Write([]byte("EHLO test.localhost.localdomain\r\nQUIT\r\n")); err != nil { - t.Errorf("failed to write quit command to first connection: %s", err) - } - p.Close() - - if err = conn.Close(); err != nil { - t.Errorf("failed to close connection: %s", err) - } - if p.Size() != 0 { - t.Errorf("closed pool shouldn't allow to put connections.") - } -} - -func TestPoolConn_MarkUnusable(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - serverPort := TestServerPortBase + 14 - featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" - go func() { - if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 300) - - pool, _ := newConnPool(serverPort) - defer pool.Close() - - conn, err := pool.Get(context.Background()) - if err != nil { - t.Errorf("failed to get new connection from pool: %s", err) - } - if err = conn.Close(); err != nil { - t.Errorf("failed to close connection: %s", err) - } - - poolSize := pool.Size() - conn, err = pool.Get(context.Background()) - if err != nil { - t.Errorf("failed to get new connection from pool: %s", err) - } - if err = conn.Close(); err != nil { - t.Errorf("failed to close connection: %s", err) - } - if pool.Size() != poolSize { - t.Errorf("pool size is expected to be equal to initial size") - } - - conn, err = pool.Get(context.Background()) - if err != nil { - t.Errorf("failed to get new connection from pool: %s", err) - } - if pc, ok := conn.(*PoolConn); !ok { - t.Errorf("this should never happen") - } else { - pc.MarkUnusable() - } - if err = conn.Close(); err != nil { - t.Errorf("failed to close connection: %s", err) - } - if pool.Size() != poolSize-1 { - t.Errorf("pool size is expected to be: %d but got: %d", poolSize-1, pool.Size()) - } -} - -func TestConnPool_Close(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - serverPort := TestServerPortBase + 15 - featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" - go func() { - if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 300) - - pool, err := newConnPool(serverPort) - if err != nil { - t.Errorf("failed to create connection pool: %s", err) - } - pool.Close() - - castPool := pool.(*connPool) - - if castPool.conns != nil { - t.Error("closing pool failed: conns channel should be nil") - } - if castPool.dialCtxFunc != nil { - t.Error("closing pool failed: dialCtxFunc should be nil") - } - if castPool.dialAddress != "" { - t.Error("closing pool failed: dialAddress should be empty") - } - if castPool.dialNetwork != "" { - t.Error("closing pool failed: dialNetwork should be empty") - } - - conn, err := pool.Get(context.Background()) - if err == nil { - t.Errorf("closing pool failed: getting new connection should return an error") - } - if conn != nil { - t.Errorf("closing pool failed: getting new connection should return a nil-connection") - } - if pool.Size() != 0 { - t.Errorf("closing pool failed: pool size should be 0, but got: %d", pool.Size()) - } -} - -func TestConnPool_Concurrency(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - serverPort := TestServerPortBase + 16 - featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" - go func() { - if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 300) - - pool, err := newConnPool(serverPort) - if err != nil { - t.Errorf("failed to create connection pool: %s", err) - } - defer pool.Close() - pipe := make(chan net.Conn) - - getWg := sync.WaitGroup{} - closeWg := sync.WaitGroup{} - for i := 0; i < 30; i++ { - getWg.Add(1) - closeWg.Add(1) - go func() { - conn, err := pool.Get(context.Background()) - if err != nil { - t.Errorf("failed to get new connection from pool: %s", err) - } - pipe <- conn - getWg.Done() - }() - - go func() { - conn := <-pipe - if conn == nil { - return - } - if err = conn.Close(); err != nil { - t.Errorf("failed to close connection: %s", err) - } - closeWg.Done() - }() - getWg.Wait() - closeWg.Wait() - } -} - -func TestConnPool_GetContextCancel(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - serverPort := TestServerPortBase + 17 - featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" - go func() { - if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 300) - - p, err := newConnPool(serverPort) - if err != nil { - t.Errorf("failed to create connection pool: %s", err) - } - defer p.Close() - - connCtx, connCancel := context.WithCancel(context.Background()) - defer connCancel() - - conn, err := p.Get(connCtx) - if err != nil { - t.Errorf("failed to get new connection from pool: %s", err) - return - } - if _, err = conn.Write([]byte("EHLO test.localhost.localdomain\r\nQUIT\r\n")); err != nil { - t.Errorf("failed to write quit command to first connection: %s", err) - } - - if p.Size() != 4 { - t.Errorf("getting new connection from pool failed. Expected pool size: 4, got %d", p.Size()) - } - - var wg sync.WaitGroup - for i := 0; i < 4; i++ { - wg.Add(1) - go func() { - defer wg.Done() - wgconn, err := p.Get(connCtx) - if err != nil { - t.Errorf("failed to get new connection from pool: %s", err) - } - if _, err = wgconn.Write([]byte("EHLO test.localhost.localdomain\r\nQUIT\r\n")); err != nil { - t.Errorf("failed to write quit command to first connection: %s", err) - } - }() - } - wg.Wait() - - if p.Size() != 0 { - t.Errorf("Get error. Expecting 0, got %d", p.Size()) - } - - connCancel() - _, err = p.Get(connCtx) - if err == nil { - t.Errorf("getting new connection on canceled context should fail, but didn't") - } - p.Close() -} - -func newConnPool(port int) (Pool, error) { - netDialer := net.Dialer{} - return NewConnPool(context.Background(), 5, 30, netDialer.DialContext, "tcp", - fmt.Sprintf("127.0.0.1:%d", port)) -} From 3871b2be44124484903057d4973ddf05488be071 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 26 Sep 2024 11:51:30 +0200 Subject: [PATCH 055/181] Lock client connections and update deadline handling Add mutex locking for client connections to ensure thread safety. Introduce `HasConnection` method to check active connections and `UpdateDeadline` method to handle timeout updates. Refactor connection handling in `checkConn` and `tls` methods accordingly. --- client.go | 18 +++++++++++------- client_120.go | 2 ++ smtp/smtp.go | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/client.go b/client.go index fac9a34..77f9751 100644 --- a/client.go +++ b/client.go @@ -12,6 +12,7 @@ import ( "net" "os" "strings" + "sync" "time" "github.com/wneessen/go-mail/log" @@ -87,6 +88,7 @@ type DialContextFunc func(ctx context.Context, network, address string) (net.Con // Client is the SMTP client struct type Client struct { + mutex sync.RWMutex // connection is the net.Conn that the smtp.Client is based on connection net.Conn @@ -589,6 +591,9 @@ func (c *Client) setDefaultHelo() error { // DialWithContext establishes a connection to the SMTP server with a given context.Context func (c *Client) DialWithContext(dialCtx context.Context) error { + c.mutex.Lock() + defer c.mutex.Unlock() + ctx, cancel := context.WithDeadline(dialCtx, time.Now().Add(c.connTimeout)) defer cancel() @@ -602,17 +607,16 @@ func (c *Client) DialWithContext(dialCtx context.Context) error { c.dialContextFunc = tlsDialer.DialContext } } - var err error - c.connection, err = c.dialContextFunc(ctx, "tcp", c.ServerAddr()) + connection, err := c.dialContextFunc(ctx, "tcp", c.ServerAddr()) if err != nil && c.fallbackPort != 0 { // TODO: should we somehow log or append the previous error? - c.connection, err = c.dialContextFunc(ctx, "tcp", c.serverFallbackAddr()) + connection, err = c.dialContextFunc(ctx, "tcp", c.serverFallbackAddr()) } if err != nil { return err } - client, err := smtp.NewClient(c.connection, c.host) + client, err := smtp.NewClient(connection, c.host) if err != nil { return err } @@ -691,7 +695,7 @@ func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) e // checkConn makes sure that a required server connection is available and extends the // connection deadline func (c *Client) checkConn() error { - if c.connection == nil { + if !c.smtpClient.HasConnection() { return ErrNoActiveConnection } @@ -701,7 +705,7 @@ func (c *Client) checkConn() error { } } - if err := c.connection.SetDeadline(time.Now().Add(c.connTimeout)); err != nil { + if err := c.smtpClient.UpdateDeadline(c.connTimeout); err != nil { return ErrDeadlineExtendFailed } return nil @@ -715,7 +719,7 @@ func (c *Client) serverFallbackAddr() string { // tls tries to make sure that the STARTTLS requirements are satisfied func (c *Client) tls() error { - if c.connection == nil { + if !c.smtpClient.HasConnection() { return ErrNoActiveConnection } if !c.useSSL && c.tlspolicy != NoTLS { diff --git a/client_120.go b/client_120.go index 4f82aa7..729069b 100644 --- a/client_120.go +++ b/client_120.go @@ -13,6 +13,8 @@ import ( // Send sends out the mail message func (c *Client) Send(messages ...*Msg) (returnErr error) { + c.mutex.Lock() + defer c.mutex.Unlock() if err := c.checkConn(); err != nil { returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} return diff --git a/smtp/smtp.go b/smtp/smtp.go index d2a0e64..5f5484a 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -30,6 +30,7 @@ import ( "net/textproto" "os" "strings" + "time" "github.com/wneessen/go-mail/log" ) @@ -472,6 +473,19 @@ func (c *Client) SetDSNRcptNotifyOption(d string) { c.dsnrntype = d } +// HasConnection checks if the client has an active connection. +// Returns true if the `conn` field is not nil, indicating an active connection. +func (c *Client) HasConnection() bool { + return c.conn != nil +} + +func (c *Client) UpdateDeadline(timeout time.Duration) error { + if err := c.conn.SetDeadline(time.Now().Add(timeout)); err != nil { + return fmt.Errorf("smtp: failed to update deadline: %w", err) + } + return nil +} + // debugLog checks if the debug flag is set and if so logs the provided message to // the log.Logger interface func (c *Client) debugLog(d log.Direction, f string, a ...interface{}) { From 371b950bc76884ca6b6700afad29f9a876ec3752 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 27 Sep 2024 10:33:19 +0200 Subject: [PATCH 056/181] Refactor Client struct for better readability and organization Reordered and grouped fields in the Client struct for clarity. The reorganization separates logical groups of fields, making it easier to understand and maintain the code. This includes proper grouping of TLS parameters, DSN options, and debug settings. --- smtp/smtp.go | 55 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/smtp/smtp.go b/smtp/smtp.go index 5f5484a..787006c 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -37,28 +37,45 @@ import ( // A Client represents a client connection to an SMTP server. type Client struct { - // Text is the textproto.Conn used by the Client. It is exported to allow for - // clients to add extensions. + // Text is the textproto.Conn used by the Client. It is exported to allow for clients to add extensions. Text *textproto.Conn - // keep a reference to the connection so it can be used to create a TLS - // connection later + + // auth supported auth mechanisms + auth []string + + // keep a reference to the connection so it can be used to create a TLS connection later conn net.Conn - // whether the Client is using TLS - tls bool - serverName string - // map of supported extensions + + // debug logging is enabled + debug bool + + // didHello indicates whether we've said HELO/EHLO + didHello bool + + // dsnmrtype defines the mail return option in case DSN is enabled + dsnmrtype string + + // dsnrntype defines the recipient notify option in case DSN is enabled + dsnrntype string + + // ext is a map of supported extensions ext map[string]string - // supported auth mechanisms - auth []string - localName string // the name to use in HELO/EHLO - didHello bool // whether we've said HELO/EHLO - helloError error // the error from the hello - // debug logging - debug bool // debug logging is enabled - logger log.Logger // logger will be used for debug logging - // DSN support - dsnmrtype string // dsnmrtype defines the mail return option in case DSN is enabled - dsnrntype string // dsnrntype defines the recipient notify option in case DSN is enabled + + // helloError is the error from the hello + helloError error + + // localName is the name to use in HELO/EHLO + localName string // the name to use in HELO/EHLO + + // logger will be used for debug logging + logger log.Logger + + // tls indicates whether the Client is using TLS + tls bool + + // serverName denotes the name of the server to which the application will connect. Used for + // identification and routing. + serverName string } // Dial returns a new [Client] connected to an SMTP server at addr. From 23c71d608f5540da3120176e004448cf833241e6 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 27 Sep 2024 10:33:28 +0200 Subject: [PATCH 057/181] Lock mutex before checking connection in Send method Added mutex locking in the `Send` method for both `client_120.go` and `client_119.go`. This ensures thread-safe access to the connection checks and prevents potential race conditions. --- client_119.go | 3 +++ client_120.go | 1 + 2 files changed, 4 insertions(+) diff --git a/client_119.go b/client_119.go index 7de5d59..0b05061 100644 --- a/client_119.go +++ b/client_119.go @@ -11,6 +11,9 @@ import "errors" // Send sends out the mail message func (c *Client) Send(messages ...*Msg) error { + c.mutex.Lock() + defer c.mutex.Unlock() + if err := c.checkConn(); err != nil { return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} } diff --git a/client_120.go b/client_120.go index 729069b..5bb291d 100644 --- a/client_120.go +++ b/client_120.go @@ -15,6 +15,7 @@ import ( func (c *Client) Send(messages ...*Msg) (returnErr error) { c.mutex.Lock() defer c.mutex.Unlock() + if err := c.checkConn(); err != nil { returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} return From 2084526c772f02a3e29ce5dac3f69c5b10f4afdf Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 27 Sep 2024 10:36:09 +0200 Subject: [PATCH 058/181] Refactor Client struct to improve organization and clarity Rearranged and grouped struct fields more logically within Client. Introduced the dialContextFunc and fallbackPort fields to enhance connection flexibility. Minor code style adjustments were also made for better readability. --- client.go | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/client.go b/client.go index 77f9751..d898708 100644 --- a/client.go +++ b/client.go @@ -88,13 +88,15 @@ type DialContextFunc func(ctx context.Context, network, address string) (net.Con // Client is the SMTP client struct type Client struct { - mutex sync.RWMutex // connection is the net.Conn that the smtp.Client is based on connection net.Conn // Timeout for the SMTP server connection connTimeout time.Duration + // dialContextFunc is a custom DialContext function to dial target SMTP server + dialContextFunc DialContextFunc + // dsn indicates that we want to use DSN for the Client dsn bool @@ -104,11 +106,9 @@ type Client struct { // dsnrntype defines the DSNRcptNotifyOption in case DSN is enabled dsnrntype []string - // isEncrypted indicates if a Client connection is encrypted or not - isEncrypted bool - - // noNoop indicates the Noop is to be skipped - noNoop bool + // fallbackPort is used as an alternative port number in case the primary port is unavailable or + // fails to bind. + fallbackPort int // HELO/EHLO string for the greeting the target SMTP server helo string @@ -116,12 +116,24 @@ type Client struct { // Hostname of the target SMTP server to connect to host string + // isEncrypted indicates if a Client connection is encrypted or not + isEncrypted bool + + // logger is a logger that implements the log.Logger interface + logger log.Logger + + // mutex is used to synchronize access to shared resources, ensuring that only one goroutine can + // modify them at a time. + mutex sync.RWMutex + + // noNoop indicates the Noop is to be skipped + noNoop bool + // pass is the corresponding SMTP AUTH password pass string - // Port of the SMTP server to connect to - port int - fallbackPort int + // port specifies the network port number on which the server listens for incoming connections. + port int // smtpAuth is a pointer to smtp.Auth smtpAuth smtp.Auth @@ -132,26 +144,20 @@ type Client struct { // smtpClient is the smtp.Client that is set up when using the Dial*() methods smtpClient *smtp.Client - // Use SSL for the connection - useSSL bool - // tlspolicy sets the client to use the provided TLSPolicy for the STARTTLS protocol tlspolicy TLSPolicy // tlsconfig represents the tls.Config setting for the STARTTLS connection tlsconfig *tls.Config - // user is the SMTP AUTH username - user string - // useDebugLog enables the debug logging on the SMTP client useDebugLog bool - // logger is a logger that implements the log.Logger interface - logger log.Logger + // user is the SMTP AUTH username + user string - // dialContextFunc is a custom DialContext function to dial target SMTP server - dialContextFunc DialContextFunc + // Use SSL for the connection + useSSL bool } // Option returns a function that can be used for grouping Client options From fec2f2075aefe22e399921b89fc57f9f4a8e18ae Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 27 Sep 2024 10:52:30 +0200 Subject: [PATCH 059/181] Update build tags to support future Go versions Modified the build tags to exclude Go 1.20 and above instead of targeting only Go 1.19. This change ensures the code is compatible with future versions of Go by not restricting it to a specific minor version. --- random_119.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/random_119.go b/random_119.go index b084305..4b45c55 100644 --- a/random_119.go +++ b/random_119.go @@ -2,8 +2,8 @@ // // SPDX-License-Identifier: MIT -//go:build go1.19 && !go1.20 -// +build go1.19,!go1.20 +//go:build !go1.20 +// +build !go1.20 package mail From fdb80ad9ddc2dc267805fe3201645dc4ca0b72c2 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 27 Sep 2024 11:10:23 +0200 Subject: [PATCH 060/181] Add mutex to Client for thread-safe operations This commit introduces a RWMutex to the Client struct in the smtp package to ensure thread-safe access to shared resources. Critical sections in methods like Close, StartTLS, and cmd are now protected with appropriate locking mechanisms. This change helps prevent potential race conditions, ensuring consistent and reliable behavior in concurrent environments. --- smtp/smtp.go | 41 +++++++++++++++++++++++++++++++++++++++++ smtp/smtp_ehlo.go | 3 +++ smtp/smtp_ehlo_117.go | 3 +++ 3 files changed, 47 insertions(+) diff --git a/smtp/smtp.go b/smtp/smtp.go index 787006c..1f1d603 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -30,6 +30,7 @@ import ( "net/textproto" "os" "strings" + "sync" "time" "github.com/wneessen/go-mail/log" @@ -70,6 +71,10 @@ type Client struct { // logger will be used for debug logging logger log.Logger + // mutex is used to synchronize access to shared resources, ensuring that only one goroutine can access + // the resource at a time. + mutex sync.RWMutex + // tls indicates whether the Client is using TLS tls bool @@ -112,6 +117,9 @@ func NewClient(conn net.Conn, host string) (*Client, error) { // Close closes the connection. func (c *Client) Close() error { + c.mutex.Lock() + defer c.mutex.Unlock() + return c.Text.Close() } @@ -139,12 +147,19 @@ func (c *Client) Hello(localName string) error { if c.didHello { return errors.New("smtp: Hello called after other methods") } + + c.mutex.Lock() c.localName = localName + c.mutex.Unlock() + return c.hello() } // cmd is a convenience function that sends a command and returns the response func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) { + c.mutex.Lock() + defer c.mutex.Unlock() + c.debugLog(log.DirClientToServer, format, args...) id, err := c.Text.Cmd(format, args...) if err != nil { @@ -160,7 +175,10 @@ func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, s // helo sends the HELO greeting to the server. It should be used only when the // server does not support ehlo. func (c *Client) helo() error { + c.mutex.Lock() c.ext = nil + c.mutex.Unlock() + _, _, err := c.cmd(250, "HELO %s", c.localName) return err } @@ -175,9 +193,13 @@ func (c *Client) StartTLS(config *tls.Config) error { if err != nil { return err } + + c.mutex.Lock() c.conn = tls.Client(c.conn, config) c.Text = textproto.NewConn(c.conn) c.tls = true + c.mutex.Unlock() + return c.ehlo() } @@ -185,6 +207,9 @@ func (c *Client) StartTLS(config *tls.Config) error { // The return values are their zero values if [Client.StartTLS] did // not succeed. func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) { + c.mutex.RLock() + defer c.mutex.RUnlock() + tc, ok := c.conn.(*tls.Conn) if !ok { return @@ -249,7 +274,9 @@ func (c *Client) Auth(a Auth) error { // abort the AUTH. Not required for XOAUTH2 _, _, _ = c.cmd(501, "*") } + c.mutex.Lock() _ = c.Quit() + c.mutex.Unlock() break } if resp == nil { @@ -275,6 +302,8 @@ func (c *Client) Mail(from string) error { return err } cmdStr := "MAIL FROM:<%s>" + + c.mutex.RLock() if c.ext != nil { if _, ok := c.ext["8BITMIME"]; ok { cmdStr += " BODY=8BITMIME" @@ -287,6 +316,8 @@ func (c *Client) Mail(from string) error { cmdStr += fmt.Sprintf(" RET=%s", c.dsnmrtype) } } + c.mutex.RUnlock() + _, _, err := c.cmd(250, cmdStr, from) return err } @@ -298,7 +329,11 @@ func (c *Client) Rcpt(to string) error { if err := validateLine(to); err != nil { return err } + + c.mutex.RLock() _, ok := c.ext["DSN"] + c.mutex.RUnlock() + if ok && c.dsnrntype != "" { _, _, err := c.cmd(25, "RCPT TO:<%s> NOTIFY=%s", to, c.dsnrntype) return err @@ -423,6 +458,9 @@ func (c *Client) Extension(ext string) (bool, string) { return false, "" } ext = strings.ToUpper(ext) + + c.mutex.RLock() + defer c.mutex.RUnlock() param, ok := c.ext[ext] return ok, param } @@ -497,6 +535,9 @@ func (c *Client) HasConnection() bool { } func (c *Client) UpdateDeadline(timeout time.Duration) error { + c.mutex.Lock() + defer c.mutex.Unlock() + if err := c.conn.SetDeadline(time.Now().Add(timeout)); err != nil { return fmt.Errorf("smtp: failed to update deadline: %w", err) } diff --git a/smtp/smtp_ehlo.go b/smtp/smtp_ehlo.go index ae80a62..457be57 100644 --- a/smtp/smtp_ehlo.go +++ b/smtp/smtp_ehlo.go @@ -25,6 +25,9 @@ func (c *Client) ehlo() error { if err != nil { return err } + + c.mutex.Lock() + defer c.mutex.Unlock() ext := make(map[string]string) extList := strings.Split(msg, "\n") if len(extList) > 1 { diff --git a/smtp/smtp_ehlo_117.go b/smtp/smtp_ehlo_117.go index c516a36..c40297f 100644 --- a/smtp/smtp_ehlo_117.go +++ b/smtp/smtp_ehlo_117.go @@ -28,6 +28,9 @@ func (c *Client) ehlo() error { if err != nil { return err } + + c.mutex.Lock() + defer c.mutex.Unlock() ext := make(map[string]string) extList := strings.Split(msg, "\n") if len(extList) > 1 { From 2234f0c5bc67916863de1858834bf26a72432a25 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 27 Sep 2024 11:43:22 +0200 Subject: [PATCH 061/181] Remove connection field from Client struct This commit removes the 'connection' field from the 'Client' struct and updates the related test logic accordingly. By using 'smtpClient.HasConnection()' to check for connections, code readability and maintainability are improved. All necessary test cases have been adjusted to reflect this change. --- client.go | 3 -- client_test.go | 77 +++++++++++++++++++++++++------------------------- 2 files changed, 38 insertions(+), 42 deletions(-) diff --git a/client.go b/client.go index d898708..81ff065 100644 --- a/client.go +++ b/client.go @@ -88,9 +88,6 @@ type DialContextFunc func(ctx context.Context, network, address string) (net.Con // Client is the SMTP client struct type Client struct { - // connection is the net.Conn that the smtp.Client is based on - connection net.Conn - // Timeout for the SMTP server connection connTimeout time.Duration diff --git a/client_test.go b/client_test.go index c891a10..7706b0f 100644 --- a/client_test.go +++ b/client_test.go @@ -623,11 +623,12 @@ func TestClient_DialWithContext(t *testing.T) { t.Errorf("failed to dial with context: %s", err) return } - if c.connection == nil { - t.Errorf("DialWithContext didn't fail but no connection found.") - } if c.smtpClient == nil { t.Errorf("DialWithContext didn't fail but no SMTP client found.") + return + } + if !c.smtpClient.HasConnection() { + t.Errorf("DialWithContext didn't fail but no connection found.") } if err := c.Close(); err != nil { t.Errorf("failed to close connection: %s", err) @@ -644,17 +645,18 @@ func TestClient_DialWithContext_Fallback(t *testing.T) { c.SetTLSPortPolicy(TLSOpportunistic) c.port = 999 ctx := context.Background() - if err := c.DialWithContext(ctx); err != nil { + if err = c.DialWithContext(ctx); err != nil { t.Errorf("failed to dial with context: %s", err) return } - if c.connection == nil { - t.Errorf("DialWithContext didn't fail but no connection found.") - } if c.smtpClient == nil { t.Errorf("DialWithContext didn't fail but no SMTP client found.") + return } - if err := c.Close(); err != nil { + if !c.smtpClient.HasConnection() { + t.Errorf("DialWithContext didn't fail but no connection found.") + } + if err = c.Close(); err != nil { t.Errorf("failed to close connection: %s", err) } @@ -674,18 +676,19 @@ func TestClient_DialWithContext_Debug(t *testing.T) { t.Skipf("failed to create test client: %s. Skipping tests", err) } ctx := context.Background() - if err := c.DialWithContext(ctx); err != nil { + if err = c.DialWithContext(ctx); err != nil { t.Errorf("failed to dial with context: %s", err) return } - if c.connection == nil { - t.Errorf("DialWithContext didn't fail but no connection found.") - } if c.smtpClient == nil { t.Errorf("DialWithContext didn't fail but no SMTP client found.") + return + } + if !c.smtpClient.HasConnection() { + t.Errorf("DialWithContext didn't fail but no connection found.") } c.SetDebugLog(true) - if err := c.Close(); err != nil { + if err = c.Close(); err != nil { t.Errorf("failed to close connection: %s", err) } } @@ -698,19 +701,20 @@ func TestClient_DialWithContext_Debug_custom(t *testing.T) { t.Skipf("failed to create test client: %s. Skipping tests", err) } ctx := context.Background() - if err := c.DialWithContext(ctx); err != nil { + if err = c.DialWithContext(ctx); err != nil { t.Errorf("failed to dial with context: %s", err) return } - if c.connection == nil { - t.Errorf("DialWithContext didn't fail but no connection found.") - } if c.smtpClient == nil { t.Errorf("DialWithContext didn't fail but no SMTP client found.") + return + } + if !c.smtpClient.HasConnection() { + t.Errorf("DialWithContext didn't fail but no connection found.") } c.SetDebugLog(true) c.SetLogger(log.New(os.Stderr, log.LevelDebug)) - if err := c.Close(); err != nil { + if err = c.Close(); err != nil { t.Errorf("failed to close connection: %s", err) } } @@ -722,10 +726,9 @@ func TestClient_DialWithContextInvalidHost(t *testing.T) { if err != nil { t.Skipf("failed to create test client: %s. Skipping tests", err) } - c.connection = nil c.host = "invalid.addr" 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") return } @@ -738,10 +741,9 @@ func TestClient_DialWithContextInvalidHELO(t *testing.T) { if err != nil { t.Skipf("failed to create test client: %s. Skipping tests", err) } - c.connection = nil c.helo = "" 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") return } @@ -758,7 +760,7 @@ func TestClient_DialWithContextInvalidAuth(t *testing.T) { c.pass = "invalid" c.SetSMTPAuthCustom(smtp.LoginAuth("invalid", "invalid", "invalid")) 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") return } @@ -770,8 +772,7 @@ func TestClient_checkConn(t *testing.T) { if err != nil { t.Skipf("failed to create test client: %s. Skipping tests", err) } - c.connection = nil - if err := c.checkConn(); err == nil { + if err = c.checkConn(); err == nil { t.Errorf("connCheck() should fail but succeeded") } } @@ -802,21 +803,23 @@ func TestClient_DialWithContextOptions(t *testing.T) { } ctx := context.Background() - if err := c.DialWithContext(ctx); err != nil && !tt.sf { + if err = c.DialWithContext(ctx); err != nil && !tt.sf { t.Errorf("failed to dial with context: %s", err) return } if !tt.sf { - if c.connection == nil && !tt.sf { - t.Errorf("DialWithContext didn't fail but no connection found.") - } if c.smtpClient == nil && !tt.sf { t.Errorf("DialWithContext didn't fail but no SMTP client found.") + return } - if err := c.Reset(); err != nil { + if !c.smtpClient.HasConnection() && !tt.sf { + t.Errorf("DialWithContext didn't fail but no connection found.") + return + } + if err = c.Reset(); err != nil { t.Errorf("failed to reset connection: %s", err) } - if err := c.Close(); err != nil { + if err = c.Close(); err != nil { t.Errorf("failed to close connection: %s", err) } } @@ -1011,17 +1014,15 @@ func TestClient_DialSendCloseBroken(t *testing.T) { } if tt.closestart { _ = c.smtpClient.Close() - _ = c.connection.Close() } - if err := c.Send(m); err != nil && !tt.sf { + if err = c.Send(m); err != nil && !tt.sf { t.Errorf("Send() failed: %s", err) return } if tt.closeearly { _ = c.smtpClient.Close() - _ = c.connection.Close() } - if err := c.Close(); err != nil && !tt.sf { + if err = c.Close(); err != nil && !tt.sf { t.Errorf("Close() failed: %s", err) return } @@ -1071,17 +1072,15 @@ func TestClient_DialSendCloseBrokenWithDSN(t *testing.T) { } if tt.closestart { _ = c.smtpClient.Close() - _ = c.connection.Close() } - if err := c.Send(m); err != nil && !tt.sf { + if err = c.Send(m); err != nil && !tt.sf { t.Errorf("Send() failed: %s", err) return } if tt.closeearly { _ = c.smtpClient.Close() - _ = c.connection.Close() } - if err := c.Close(); err != nil && !tt.sf { + if err = c.Close(); err != nil && !tt.sf { t.Errorf("Close() failed: %s", err) return } From f59aa23ed8658c9758cd3fe17165cd1fa18e0aed Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 27 Sep 2024 11:58:08 +0200 Subject: [PATCH 062/181] Add mutex locking to SetTLSConfig This change ensures that the SetTLSConfig method is thread-safe by adding a mutex lock. The lock is acquired before any changes to the TLS configuration and released afterward to prevent concurrent access issues. --- client.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client.go b/client.go index 81ff065..48548ce 100644 --- a/client.go +++ b/client.go @@ -555,6 +555,9 @@ func (c *Client) SetLogger(logger log.Logger) { // SetTLSConfig overrides the current *tls.Config with the given *tls.Config value func (c *Client) SetTLSConfig(tlsconfig *tls.Config) error { + c.mutex.Lock() + defer c.mutex.Unlock() + if tlsconfig == nil { return ErrInvalidTLSConfig } From 6bd9a9c73584d3a9faee41f7f870718e9e28f94d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 27 Sep 2024 14:03:26 +0200 Subject: [PATCH 063/181] Refactor mutex usage for connection safety This commit revises locking mechanism usage around connection operations to avoid potential deadlocks and improve code clarity. Specifically, defer statements were removed and explicit unlocks were added to ensure that mutexes are properly released after critical sections. This change affects several methods, including `Close`, `cmd`, `TLSConnectionState`, `UpdateDeadline`, and newly introduced locking for concurrent data writes and reads in `dataCloser`. --- smtp/smtp.go | 49 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/smtp/smtp.go b/smtp/smtp.go index 1f1d603..379f5fe 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -118,9 +118,9 @@ func NewClient(conn net.Conn, host string) (*Client, error) { // Close closes the connection. func (c *Client) Close() error { c.mutex.Lock() - defer c.mutex.Unlock() - - return c.Text.Close() + err := c.Text.Close() + c.mutex.Unlock() + return err } // hello runs a hello exchange if needed. @@ -158,17 +158,18 @@ func (c *Client) Hello(localName string) error { // cmd is a convenience function that sends a command and returns the response func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) { c.mutex.Lock() - defer c.mutex.Unlock() c.debugLog(log.DirClientToServer, format, args...) id, err := c.Text.Cmd(format, args...) if err != nil { + c.mutex.Unlock() return 0, "", err } c.Text.StartResponse(id) - defer c.Text.EndResponse(id) code, msg, err := c.Text.ReadResponse(expectCode) c.debugLog(log.DirServerToClient, "%d %s", code, msg) + c.Text.EndResponse(id) + c.mutex.Unlock() return code, msg, err } @@ -208,13 +209,14 @@ func (c *Client) StartTLS(config *tls.Config) error { // not succeed. func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) { c.mutex.RLock() - defer c.mutex.RUnlock() tc, ok := c.conn.(*tls.Conn) if !ok { return } - return tc.ConnectionState(), true + state, ok = tc.ConnectionState(), true + c.mutex.RUnlock() + return } // Verify checks the validity of an email address on the server. @@ -274,9 +276,7 @@ func (c *Client) Auth(a Auth) error { // abort the AUTH. Not required for XOAUTH2 _, _, _ = c.cmd(501, "*") } - c.mutex.Lock() _ = c.Quit() - c.mutex.Unlock() break } if resp == nil { @@ -347,12 +347,23 @@ type dataCloser struct { io.WriteCloser } +// Close releases the lock, closes the WriteCloser, waits for a response, and then returns any error encountered. func (d *dataCloser) Close() error { + d.c.mutex.Lock() _ = d.WriteCloser.Close() _, _, err := d.c.Text.ReadResponse(250) + d.c.mutex.Unlock() return err } +// Write writes data to the underlying WriteCloser while ensuring thread-safety by locking and unlocking a mutex. +func (d *dataCloser) Write(p []byte) (n int, err error) { + d.c.mutex.Lock() + n, err = d.WriteCloser.Write(p) + d.c.mutex.Unlock() + return +} + // Data issues a DATA command to the server and returns a writer that // can be used to write the mail headers and body. The caller should // close the writer before calling any more methods on c. A call to @@ -362,7 +373,14 @@ func (c *Client) Data() (io.WriteCloser, error) { if err != nil { return nil, err } - return &dataCloser{c, c.Text.DotWriter()}, nil + datacloser := &dataCloser{} + + c.mutex.Lock() + datacloser.c = c + datacloser.WriteCloser = c.Text.DotWriter() + c.mutex.Unlock() + + return datacloser, nil } var testHookStartTLS func(*tls.Config) // nil, except for tests @@ -460,8 +478,8 @@ func (c *Client) Extension(ext string) (bool, string) { ext = strings.ToUpper(ext) c.mutex.RLock() - defer c.mutex.RUnlock() param, ok := c.ext[ext] + c.mutex.RUnlock() return ok, param } @@ -494,7 +512,11 @@ func (c *Client) Quit() error { if err != nil { return err } - return c.Text.Close() + c.mutex.Lock() + err = c.Text.Close() + c.mutex.Unlock() + + return err } // SetDebugLog enables the debug logging for incoming and outgoing SMTP messages @@ -536,11 +558,10 @@ func (c *Client) HasConnection() bool { func (c *Client) UpdateDeadline(timeout time.Duration) error { c.mutex.Lock() - defer c.mutex.Unlock() - if err := c.conn.SetDeadline(time.Now().Add(timeout)); err != nil { return fmt.Errorf("smtp: failed to update deadline: %w", err) } + c.mutex.Unlock() return nil } From 253d065c83bf5e1011509b73e3020a9d4e3ade76 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 27 Sep 2024 14:03:50 +0200 Subject: [PATCH 064/181] Move mutex lock to sendSingleMsg method Mutex locking was relocated from the Send method in client_120.go and client_119.go to sendSingleMsg in client.go. This ensures thread-safety specifically during the message transmission process. --- client.go | 3 +++ client_119.go | 3 --- client_120.go | 3 --- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/client.go b/client.go index 48548ce..6557913 100644 --- a/client.go +++ b/client.go @@ -801,6 +801,9 @@ func (c *Client) auth() error { // sendSingleMsg sends out a single message and returns an error if the transmission/delivery fails. // It is invoked by the public Send methods func (c *Client) sendSingleMsg(message *Msg) error { + c.mutex.Lock() + defer c.mutex.Unlock() + if message.encoding == NoEncoding { if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok { return &SendError{Reason: ErrNoUnencoded, isTemp: false, affectedMsg: message} diff --git a/client_119.go b/client_119.go index 0b05061..7de5d59 100644 --- a/client_119.go +++ b/client_119.go @@ -11,9 +11,6 @@ import "errors" // Send sends out the mail message func (c *Client) Send(messages ...*Msg) error { - c.mutex.Lock() - defer c.mutex.Unlock() - if err := c.checkConn(); err != nil { return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} } diff --git a/client_120.go b/client_120.go index 5bb291d..4f82aa7 100644 --- a/client_120.go +++ b/client_120.go @@ -13,9 +13,6 @@ import ( // Send sends out the mail message func (c *Client) Send(messages ...*Msg) (returnErr error) { - c.mutex.Lock() - defer c.mutex.Unlock() - if err := c.checkConn(); err != nil { returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} return From 2d98c40cb6de7629a65136a0508ea4d4317612bd Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 27 Sep 2024 14:04:02 +0200 Subject: [PATCH 065/181] Add concurrent send tests for Client Introduced TestClient_DialSendConcurrent_online and TestClient_DialSendConcurrent_local to validate concurrent sending of messages. These tests ensure that the Client's send functionality works correctly under concurrent conditions, both in an online environment and using a local test server. --- client_test.go | 110 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/client_test.go b/client_test.go index 7706b0f..7055a10 100644 --- a/client_test.go +++ b/client_test.go @@ -15,6 +15,7 @@ import ( "os" "strconv" "strings" + "sync" "testing" "time" @@ -1727,6 +1728,114 @@ func TestClient_SendErrorReset(t *testing.T) { } } +func TestClient_DialSendConcurrent_online(t *testing.T) { + if os.Getenv("TEST_ALLOW_SEND") == "" { + t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test") + } + + client, err := getTestConnection(true) + if err != nil { + t.Errorf("unable to create new client: %s", err) + } + + var messages []*Msg + for i := 0; i < 10; i++ { + message := NewMsg() + if err := message.FromFormat("go-mail Test Mailer", os.Getenv("TEST_FROM")); err != nil { + t.Errorf("failed to set FROM address: %s", err) + return + } + if err := message.To(TestRcpt); err != nil { + t.Errorf("failed to set TO address: %s", err) + return + } + message.Subject(fmt.Sprintf("Test subject for mail %d", i)) + message.SetBodyString(TypeTextPlain, fmt.Sprintf("This is the test body of the mail no. %d", i)) + message.SetMessageID() + messages = append(messages, message) + } + + if err = client.DialWithContext(context.Background()); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + + wg := sync.WaitGroup{} + for id, message := range messages { + wg.Add(1) + go func(curMsg *Msg, curID int) { + defer wg.Done() + if goroutineErr := client.Send(curMsg); err != nil { + t.Errorf("failed to send message with ID %d: %s", curID, goroutineErr) + } + }(message, id) + } + wg.Wait() + + if err = client.Close(); err != nil { + t.Errorf("failed to close server connection: %s", err) + } +} + +func TestClient_DialSendConcurrent_local(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + serverPort := TestServerPortBase + 20 + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 500) + + client, err := NewClient(TestServerAddr, WithPort(serverPort), + WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), + WithUsername("toni@tester.com"), + WithPassword("V3ryS3cr3t+")) + if err != nil { + t.Errorf("unable to create new client: %s", err) + } + + var messages []*Msg + for i := 0; i < 50; i++ { + message := NewMsg() + if err := message.From("valid-from@domain.tld"); err != nil { + t.Errorf("failed to set FROM address: %s", err) + return + } + if err := message.To("valid-to@domain.tld"); err != nil { + t.Errorf("failed to set TO address: %s", err) + return + } + message.Subject("Test subject") + message.SetBodyString(TypeTextPlain, "Test body") + message.SetMessageIDWithValue("this.is.a.message.id") + messages = append(messages, message) + } + + if err = client.DialWithContext(context.Background()); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + + wg := sync.WaitGroup{} + for id, message := range messages { + wg.Add(1) + go func(curMsg *Msg, curID int) { + defer wg.Done() + if goroutineErr := client.Send(curMsg); err != nil { + t.Errorf("failed to send message with ID %d: %s", curID, goroutineErr) + } + }(message, id) + } + wg.Wait() + + if err = client.Close(); err != nil { + t.Errorf("failed to close server connection: %s", err) + } +} + // getTestConnection takes environment variables to establish a connection to a real // SMTP server to test all functionality that requires a connection func getTestConnection(auth bool) (*Client, error) { @@ -2099,6 +2208,7 @@ func handleTestServerConnection(connection net.Conn, featureSet string, failRese if err != nil { break } + time.Sleep(time.Millisecond) var datastring string data = strings.TrimSpace(data) From 8791ce5a3354005905f05398c027149261a5220d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 27 Sep 2024 17:00:07 +0200 Subject: [PATCH 066/181] Fix deferred mutex unlock in TLSConnectionState Correct the sequence of mutex unlocking in TLSConnectionState to ensure the mutex is always released properly. This prevents potential deadlocks and ensures the function behaves as expected in a concurrent context. --- smtp/smtp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smtp/smtp.go b/smtp/smtp.go index 379f5fe..4ea1a3d 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -209,13 +209,13 @@ func (c *Client) StartTLS(config *tls.Config) error { // not succeed. func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) { c.mutex.RLock() + defer c.mutex.RUnlock() tc, ok := c.conn.(*tls.Conn) if !ok { return } state, ok = tc.ConnectionState(), true - c.mutex.RUnlock() return } From 6e98d7e47d8ed22002c72482f821f8f15db7c587 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 27 Sep 2024 17:00:21 +0200 Subject: [PATCH 067/181] Reduce message loop iterations and add XOAUTH2 tests Loop iterations in `client_test.go` were reduced from 50 to 20 for efficiency. Added new tests to verify XOAUTH2 authentication support and error handling by simulating SMTP server responses. --- client_test.go | 77 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/client_test.go b/client_test.go index 7055a10..c764263 100644 --- a/client_test.go +++ b/client_test.go @@ -1799,7 +1799,7 @@ func TestClient_DialSendConcurrent_local(t *testing.T) { } var messages []*Msg - for i := 0; i < 50; i++ { + for i := 0; i < 20; i++ { message := NewMsg() if err := message.From("valid-from@domain.tld"); err != nil { t.Errorf("failed to set FROM address: %s", err) @@ -2021,6 +2021,72 @@ func getTestConnectionWithDSN(auth bool) (*Client, error) { } func TestXOAuth2OK(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + serverPort := TestServerPortBase + 30 + featureSet := "250-AUTH XOAUTH2\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 500) + + c, err := NewClient("127.0.0.1", + WithPort(serverPort), + WithTLSPortPolicy(TLSOpportunistic), + WithSMTPAuth(SMTPAuthXOAUTH2), + WithUsername("user"), + WithPassword("token")) + if err != nil { + t.Fatalf("unable to create new client: %v", err) + } + if err = c.DialWithContext(context.Background()); err != nil { + t.Fatalf("unexpected dial error: %v", err) + } + if err = c.Close(); err != nil { + t.Fatalf("disconnect from test server failed: %v", err) + } +} + +func TestXOAuth2Unsupported(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + serverPort := TestServerPortBase + 31 + featureSet := "250-AUTH LOGIN PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 500) + + c, err := NewClient("127.0.0.1", + WithPort(serverPort), + WithTLSPolicy(TLSOpportunistic), + WithSMTPAuth(SMTPAuthXOAUTH2), + WithUsername("user"), + WithPassword("token")) + if err != nil { + t.Fatalf("unable to create new client: %v", err) + } + if err = c.DialWithContext(context.Background()); err == nil { + t.Fatal("expected dial error got nil") + } else { + if !errors.Is(err, ErrXOauth2AuthNotSupported) { + t.Fatalf("expected %v; got %v", ErrXOauth2AuthNotSupported, err) + } + } + if err = c.Close(); err != nil { + t.Fatalf("disconnect from test server failed: %v", err) + } +} + +func TestXOAuth2OK_faker(t *testing.T) { server := []string{ "220 Fake server ready ESMTP", "250-fake.server", @@ -2060,7 +2126,7 @@ func TestXOAuth2OK(t *testing.T) { } } -func TestXOAuth2Unsupported(t *testing.T) { +func TestXOAuth2Unsupported_faker(t *testing.T) { server := []string{ "220 Fake server ready ESMTP", "250-fake.server", @@ -2231,6 +2297,13 @@ func handleTestServerConnection(connection net.Conn, featureSet string, failRese break } writeOK() + case strings.HasPrefix(data, "AUTH XOAUTH2"): + auth := strings.TrimPrefix(data, "AUTH XOAUTH2 ") + if !strings.EqualFold(auth, "dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=") { + _ = writeLine("535 5.7.8 Error: authentication failed") + break + } + _ = writeLine("235 2.7.0 Authentication successful") case strings.HasPrefix(data, "AUTH PLAIN"): auth := strings.TrimPrefix(data, "AUTH PLAIN ") if !strings.EqualFold(auth, "AHRvbmlAdGVzdGVyLmNvbQBWM3J5UzNjcjN0Kw==") { From c1f6ef07d46dbb70206c12b83ac72bed0e45fd8b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 27 Sep 2024 17:09:00 +0200 Subject: [PATCH 068/181] Skip test cases when client creation fails Updated the client creation check to skip test cases if the client cannot be created, instead of marking them as errors. This ensures tests dependent on a successful client creation do not fail unnecessarily but are instead skipped. --- client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client_test.go b/client_test.go index c764263..2d37ce0 100644 --- a/client_test.go +++ b/client_test.go @@ -1735,7 +1735,7 @@ func TestClient_DialSendConcurrent_online(t *testing.T) { client, err := getTestConnection(true) if err != nil { - t.Errorf("unable to create new client: %s", err) + t.Skipf("failed to create test client: %s. Skipping tests", err) } var messages []*Msg From 012082978d9e4dce2123e0376201eb14a45fc0ec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 13:36:41 +0000 Subject: [PATCH 069/181] Bump github/codeql-action from 3.26.9 to 3.26.10 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.9 to 3.26.10. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/461ef6c76dfe95d5c364de2f431ddbd31a417628...e2b3eafc8d227b0241d48be5f425d47c2d750a13) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 4c0c477..ea73834 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -54,7 +54,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 + uses: github/codeql-action/init@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10 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@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 + uses: github/codeql-action/autobuild@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10 # ℹī¸ 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@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 + uses: github/codeql-action/analyze@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 5d17aa0..05e5e69 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -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@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 + uses: github/codeql-action/upload-sarif@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10 with: sarif_file: results.sarif From 9069c9cdffd8f2e1a2444821c728b280269a71da Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 11:03:44 +0200 Subject: [PATCH 070/181] Add SCRAM-SHA support to SMTP authentication Introduced additional SMTP authentication mechanisms: SCRAM-SHA-1, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, and SCRAM-SHA-256-PLUS. Added corresponding error messages for unsupported authentication types. This enhances security options for SMTP connections. --- auth.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/auth.go b/auth.go index 4a59b14..d30a154 100644 --- a/auth.go +++ b/auth.go @@ -28,6 +28,11 @@ const ( // SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism. // https://developers.google.com/gmail/imap/xoauth2-protocol SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2" + + SMTPAuthSCRAMSHA1 SMTPAuthType = "SCRAM-SHA-1" + SMTPAuthSCRAMSHA1PLUS SMTPAuthType = "SCRAM-SHA-1-PLUS" + SMTPAuthSCRAMSHA256 SMTPAuthType = "SCRAM-SHA-256" + SMTPAuthSCRAMSHA256PLUS SMTPAuthType = "SCRAM-SHA-256-PLUS" ) // SMTP Auth related static errors @@ -43,4 +48,16 @@ var ( // ErrXOauth2AuthNotSupported should be used if the target server does not support the "XOAUTH2" schema ErrXOauth2AuthNotSupported = errors.New("server does not support SMTP AUTH type: XOAUTH2") + + // ErrSCRAMSHA1AuthNotSupported should be used if the target server does not support the "XOAUTH2" schema + ErrSCRAMSHA1AuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-1") + + // ErrSCRAMSHA1PLUSAuthNotSupported should be used if the target server does not support the "XOAUTH2" schema + ErrSCRAMSHA1PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-1-PLUS") + + // ErrSCRAMSHA256AuthNotSupported should be used if the target server does not support the "XOAUTH2" schema + ErrSCRAMSHA256AuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256") + + // ErrSCRAMSHA256PLUSAuthNotSupported should be used if the target server does not support the "XOAUTH2" schema + ErrSCRAMSHA256PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256-PLUS") ) From e8fc6cd78f80fdebb4439afc6f7a19946ab900fe Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 11:04:01 +0200 Subject: [PATCH 071/181] Add SCRAM-SHA support to SMTP authentication Introduced additional SMTP authentication mechanisms: SCRAM-SHA-1, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, and SCRAM-SHA-256-PLUS. Added corresponding error messages for unsupported authentication types. This enhances security options for SMTP connections. --- smtp/auth_scram.go | 280 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 smtp/auth_scram.go diff --git a/smtp/auth_scram.go b/smtp/auth_scram.go new file mode 100644 index 0000000..2b7f814 --- /dev/null +++ b/smtp/auth_scram.go @@ -0,0 +1,280 @@ +// SPDX-FileCopyrightText: Copyright (c) 2023 The go-mail Authors +// +// SPDX-License-Identifier: MIT + +package smtp + +import ( + "bytes" + "crypto/hmac" + "crypto/rand" + "crypto/sha1" + "crypto/sha256" + "crypto/tls" + "encoding/base64" + "errors" + "fmt" + "hash" + "io" + "strconv" + "strings" + + "golang.org/x/crypto/pbkdf2" + "golang.org/x/text/secure/precis" +) + +type scramAuth struct { + username, password, algorithm string + firstBareMsg, nonce, saltedPwd, authMessage []byte + iterations int + h func() hash.Hash + isPlus bool + tlsConnState *tls.ConnectionState + bindData []byte +} + +func ScramSHA256Auth(username, password string) Auth { + return &scramAuth{ + username: username, + password: password, + algorithm: "SCRAM-SHA-256", + h: sha256.New, + } +} + +func ScramSHA256PlusAuth(username, password string, tlsConnState *tls.ConnectionState) Auth { + return &scramAuth{ + username: username, + password: password, + algorithm: "SCRAM-SHA-256-PLUS", + h: sha256.New, + isPlus: true, + tlsConnState: tlsConnState, + } +} + +func ScramSHA1Auth(username, password string) Auth { + return &scramAuth{ + username: username, + password: password, + algorithm: "SCRAM-SHA-1", + h: sha1.New, + } +} + +func ScramSHA1PlusAuth(username, password string, tlsConnState *tls.ConnectionState) Auth { + return &scramAuth{ + username: username, + password: password, + algorithm: "SCRAM-SHA-1-PLUS", + h: sha1.New, + isPlus: true, + tlsConnState: tlsConnState, + } +} + +func (a *scramAuth) Start(_ *ServerInfo) (string, []byte, error) { + fmt.Printf("algo: %s\n", a.algorithm) + return a.algorithm, nil, nil +} + +func (a *scramAuth) Next(fromServer []byte, more bool) ([]byte, error) { + if more { + if len(fromServer) == 0 { + a.reset() + return a.initialClientMessage() + } + switch { + case bytes.HasPrefix(fromServer, []byte("r=")): + resp, err := a.handleServerFirstResponse(fromServer) + if err != nil { + a.reset() + return nil, err + } + return resp, nil + case bytes.HasPrefix(fromServer, []byte("v=")): + resp, err := a.handleServerValidationMessage(fromServer) + if err != nil { + a.reset() + return nil, err + } + return resp, nil + default: + a.reset() + return nil, errors.New("unexpected server response") + } + } + return nil, nil +} + +func (a *scramAuth) reset() { + a.nonce = nil + a.firstBareMsg = nil + a.saltedPwd = nil + a.authMessage = nil + a.iterations = 0 +} + +func (a *scramAuth) initialClientMessage() ([]byte, error) { + username, err := a.normalizeUsername() + if err != nil { + return nil, fmt.Errorf("username normalization failed: %w", err) + } + + nonceBuffer := make([]byte, 24) + if _, err := io.ReadFull(rand.Reader, nonceBuffer); err != nil { + return nil, fmt.Errorf("unable to generate client secret: %w", err) + } + a.nonce = make([]byte, base64.StdEncoding.EncodedLen(len(nonceBuffer))) + base64.StdEncoding.Encode(a.nonce, nonceBuffer) + + a.firstBareMsg = []byte("n=" + username + ",r=" + string(a.nonce)) + returnBytes := []byte("n,," + string(a.firstBareMsg)) + + if a.isPlus { + bindType := "tls-unique" + connState := a.tlsConnState + bindData := connState.TLSUnique + if connState.Version == tls.VersionTLS13 { + bindType = "tls-exporter" + bindData, err = connState.ExportKeyingMaterial("EXPORTER-Channel-Binding", []byte{}, 32) + if err != nil { + return nil, fmt.Errorf("unable to export keying material: %w", err) + } + } + bindData = []byte("p=" + bindType + ",," + string(bindData)) + a.bindData = make([]byte, base64.StdEncoding.EncodedLen(len(bindData))) + base64.StdEncoding.Encode(a.bindData, bindData) + returnBytes = []byte("p=" + bindType + ",," + string(a.firstBareMsg)) + } + + return returnBytes, nil +} + +func (a *scramAuth) handleServerFirstResponse(fromServer []byte) ([]byte, error) { + parts := bytes.Split(fromServer, []byte(",")) + if len(parts) < 3 { + return nil, errors.New("not enough fields in the first server response") + } + if !bytes.HasPrefix(parts[0], []byte("r=")) { + return nil, errors.New("first part of the server response does not start with r=") + } + if !bytes.HasPrefix(parts[1], []byte("s=")) { + return nil, errors.New("second part of the server response does not start with s=") + } + if !bytes.HasPrefix(parts[2], []byte("i=")) { + return nil, errors.New("third part of the server response does not start with i=") + } + + combinedNonce := parts[0][2:] + if len(a.nonce) == 0 || !bytes.HasPrefix(combinedNonce, a.nonce) { + return nil, errors.New("server nonce does not start with our nonce") + } + a.nonce = combinedNonce + + encodedSalt := parts[1][2:] + salt := make([]byte, base64.StdEncoding.DecodedLen(len(encodedSalt))) + n, err := base64.StdEncoding.Decode(salt, encodedSalt) + if err != nil { + return nil, fmt.Errorf("invalid encoded salt: %w", err) + } + salt = salt[:n] + + iterations, err := strconv.Atoi(string(parts[2][2:])) + if err != nil { + return nil, fmt.Errorf("invalid iterations: %w", err) + } + a.iterations = iterations + + password, err := a.normalizeString(a.password) + if err != nil { + return nil, fmt.Errorf("unable to normalize password: %w", err) + } + + a.saltedPwd = pbkdf2.Key([]byte(password), salt, a.iterations, a.h().Size(), a.h) + + msgWithoutProof := []byte("c=biws,r=" + string(a.nonce)) + if a.isPlus { + msgWithoutProof = []byte("c=" + string(a.bindData) + ",r=" + string(a.nonce)) + } + a.authMessage = []byte(string(a.firstBareMsg) + "," + string(fromServer) + "," + string(msgWithoutProof)) + + clientProof := a.computeClientProof() + + return []byte(string(msgWithoutProof) + ",p=" + string(clientProof)), nil +} + +func (a *scramAuth) handleServerValidationMessage(fromServer []byte) ([]byte, error) { + serverSignature := fromServer[2:] + computedServerSignature := a.computeServerSignature() + + if !hmac.Equal(serverSignature, computedServerSignature) { + return nil, errors.New("invalid server signature") + } + return []byte(""), nil +} + +func (a *scramAuth) computeHMAC(key, msg []byte) []byte { + mac := hmac.New(a.h, key) + mac.Write(msg) + return mac.Sum(nil) +} + +func (a *scramAuth) computeHash(key []byte) []byte { + hasher := a.h() + hasher.Write(key) + return hasher.Sum(nil) +} + +func (a *scramAuth) computeClientProof() []byte { + clientKey := a.computeHMAC(a.saltedPwd, []byte("Client Key")) + storedKey := a.computeHash(clientKey) + clientSignature := a.computeHMAC(storedKey[:], a.authMessage) + clientProof := make([]byte, len(clientSignature)) + for i := 0; i < len(clientSignature); i++ { + clientProof[i] = clientKey[i] ^ clientSignature[i] + } + buf := make([]byte, base64.StdEncoding.EncodedLen(len(clientProof))) + base64.StdEncoding.Encode(buf, clientProof) + return buf +} + +func (a *scramAuth) computeServerSignature() []byte { + serverKey := a.computeHMAC(a.saltedPwd, []byte("Server Key")) + serverSignature := a.computeHMAC(serverKey, a.authMessage) + buf := make([]byte, base64.StdEncoding.EncodedLen(len(serverSignature))) + base64.StdEncoding.Encode(buf, serverSignature) + return buf +} + +func (a *scramAuth) normalizeUsername() (string, error) { + // RFC 5802 section 5.1: the characters ',' or '=' in usernames are + // sent as '=2C' and '=3D' respectively. + replacer := strings.NewReplacer("=", "=3D", ",", "=2C") + username := replacer.Replace(a.username) + // RFC 5802 section 5.1: before sending the username to the server, + // the client SHOULD prepare the username using the "SASLprep" + // profile [RFC4013] of the "stringprep" algorithm [RFC3454] + // treating it as a query string (i.e., unassigned Unicode code + // points are allowed). If the preparation of the username fails or + // results in an empty string, the client SHOULD abort the + // authentication exchange. + // + // Since RFC 8265 obsoletes RFC 4013 we use it instead. + username, err := a.normalizeString(username) + if err != nil { + return "", fmt.Errorf("unable to normalize username: %w", err) + } + return username, nil +} + +func (a *scramAuth) normalizeString(s string) (string, error) { + s, err := precis.OpaqueString.String(s) + if err != nil { + return "", err + } + if s == "" { + return "", errors.New("normalized string is empty") + } + return s, nil +} From 4f1a60760dc3721b587bee2408eb36d656f5b2ea Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 11:04:16 +0200 Subject: [PATCH 072/181] Add support for SCRAM-SHA authentication methods Extended SMTP authentication to include SCRAM-SHA-1, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, and SCRAM-SHA-256-PLUS methods. This enhancement provides more secure and flexible authentication options for SMTP clients. --- client.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/client.go b/client.go index 6557913..f7ed9f1 100644 --- a/client.go +++ b/client.go @@ -785,6 +785,35 @@ func (c *Client) auth() error { return ErrXOauth2AuthNotSupported } c.smtpAuth = smtp.XOAuth2Auth(c.user, c.pass) + case SMTPAuthSCRAMSHA1: + if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA1)) { + return ErrXOauth2AuthNotSupported + } + c.smtpAuth = smtp.ScramSHA1Auth(c.user, c.pass) + case SMTPAuthSCRAMSHA1PLUS: + if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA1PLUS)) { + return ErrXOauth2AuthNotSupported + } + tlsConnState, err := c.smtpClient.GetTLSConnectionState() + if err != nil { + return err + } + c.smtpAuth = smtp.ScramSHA1PlusAuth(c.user, c.pass, tlsConnState) + case SMTPAuthSCRAMSHA256: + if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256)) { + return ErrXOauth2AuthNotSupported + } + c.smtpAuth = smtp.ScramSHA256Auth(c.user, c.pass) + case SMTPAuthSCRAMSHA256PLUS: + if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256PLUS)) { + return ErrXOauth2AuthNotSupported + } + tlsConnState, err := c.smtpClient.GetTLSConnectionState() + if err != nil { + return err + } + c.smtpAuth = smtp.ScramSHA256PlusAuth(c.user, c.pass, tlsConnState) + default: return fmt.Errorf("unsupported SMTP AUTH type %q", c.smtpAuthType) } From ebd171005d7d05213c780334ff3f03b3291b7a42 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 11:05:07 +0200 Subject: [PATCH 073/181] Update dependencies in go.mod and go.sum Added `golang.org/x/crypto v0.27.0` and `golang.org/x/text v0.18.0` to go.mod. Updated go.sum to reflect these changes for proper dependency management. --- go.mod | 5 +++++ go.sum | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/go.mod b/go.mod index b155b9b..1dcef3a 100644 --- a/go.mod +++ b/go.mod @@ -5,3 +5,8 @@ module github.com/wneessen/go-mail go 1.16 + +require ( + golang.org/x/crypto v0.27.0 + golang.org/x/text v0.18.0 +) diff --git a/go.sum b/go.sum index e69de29..78b6dba 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,66 @@ +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 738f43e289434743677372279fb9f8d7c91ee2b9 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 11:05:42 +0200 Subject: [PATCH 074/181] Add GetTLSConnectionState method to SMTP client Introduce a method to retrieve the TLS connection state of the client's current connection. This method checks if the connection uses TLS and is established, returning appropriate errors otherwise. --- smtp/smtp.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/smtp/smtp.go b/smtp/smtp.go index 4ea1a3d..e834a09 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -565,6 +565,25 @@ func (c *Client) UpdateDeadline(timeout time.Duration) error { return nil } +// GetTLSConnectionState retrieves the TLS connection state of the client's current connection. +// Returns an error if the connection is not using TLS or if the connection is not established. +func (c *Client) GetTLSConnectionState() (*tls.ConnectionState, error) { + c.mutex.RLock() + defer c.mutex.RUnlock() + + if !c.tls { + return nil, errors.New("smtp: connection is not using TLS") + } + if c.conn == nil { + return nil, errors.New("smtp: connection is not established") + } + if conn, ok := c.conn.(*tls.Conn); ok { + cstate := conn.ConnectionState() + return &cstate, nil + } + return nil, errors.New("smtp: connection is not a TLS connection") +} + // debugLog checks if the debug flag is set and if so logs the provided message to // the log.Logger interface func (c *Client) debugLog(d log.Direction, f string, a ...interface{}) { From b96badbd59dc8e9277819752697464af912cf2a3 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 15:25:51 +0200 Subject: [PATCH 075/181] Add license file for go.sum Introduce a go.sum.license file to explicitly state the licensing terms for the go.sum file. This ensures proper attribution and compliance with open-source licensing requirements. --- go.sum.license | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 go.sum.license diff --git a/go.sum.license b/go.sum.license new file mode 100644 index 0000000..615ea2b --- /dev/null +++ b/go.sum.license @@ -0,0 +1,3 @@ +// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors +// +// SPDX-License-Identifier: MIT From c797f0be179afffca5aad615430465eca345cf68 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 15:27:05 +0200 Subject: [PATCH 076/181] Add REUSE.toml Replaced deprecated .reuse/dep5 with REUSE.toml config file --- .reuse/dep5 | 10 ---------- REUSE.toml | 9 +++++++++ 2 files changed, 9 insertions(+), 10 deletions(-) delete mode 100644 .reuse/dep5 create mode 100644 REUSE.toml diff --git a/.reuse/dep5 b/.reuse/dep5 deleted file mode 100644 index 70261ce..0000000 --- a/.reuse/dep5 +++ /dev/null @@ -1,10 +0,0 @@ -Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: go-mail -Upstream-Contact: Winni Neessen -Source: https://github.com/wneessen/go-mail - -# Sample paragraph, commented out: -# -# Files: src/* -# Copyright: $YEAR $NAME <$CONTACT> -# License: ... diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 0000000..0bca544 --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors +# +# SPDX-License-Identifier: MIT + +version = 1 +SPDX-PackageName = "go-mail" +SPDX-PackageSupplier = "Winni Neessen " +SPDX-PackageDownloadLocation = "https://github.com/wneessen/go-mail" +annotations = [] From 3013975c6af6122a915dc360a16eec90bdc498ac Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 15:27:31 +0200 Subject: [PATCH 077/181] Rename and refactor SCRAM authentication methods Updated method names to more accurately reflect their authentication mechanisms (SCRAM-SHA-1, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, SCRAM-SHA-256-PLUS). Revised corresponding comments to improve clarity and maintain consistency. --- smtp/auth_scram.go | 53 +++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/smtp/auth_scram.go b/smtp/auth_scram.go index 2b7f814..14308fa 100644 --- a/smtp/auth_scram.go +++ b/smtp/auth_scram.go @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright (c) 2023 The go-mail Authors +// SPDX-FileCopyrightText: Copyright (c) 2024 The go-mail Authors // // SPDX-License-Identifier: MIT @@ -33,26 +33,8 @@ type scramAuth struct { bindData []byte } -func ScramSHA256Auth(username, password string) Auth { - return &scramAuth{ - username: username, - password: password, - algorithm: "SCRAM-SHA-256", - h: sha256.New, - } -} - -func ScramSHA256PlusAuth(username, password string, tlsConnState *tls.ConnectionState) Auth { - return &scramAuth{ - username: username, - password: password, - algorithm: "SCRAM-SHA-256-PLUS", - h: sha256.New, - isPlus: true, - tlsConnState: tlsConnState, - } -} - +// ScramSHA1Auth creates and returns a new SCRAM-SHA-1 authentication mechanism with the given +// username and password. func ScramSHA1Auth(username, password string) Auth { return &scramAuth{ username: username, @@ -62,6 +44,19 @@ func ScramSHA1Auth(username, password string) Auth { } } +// ScramSHA256Auth creates and returns a new SCRAM-SHA-256 authentication mechanism with the given +// username and password. +func ScramSHA256Auth(username, password string) Auth { + return &scramAuth{ + username: username, + password: password, + algorithm: "SCRAM-SHA-256", + h: sha256.New, + } +} + +// ScramSHA1PlusAuth returns an Auth instance configured for SCRAM-SHA-1-PLUS authentication with +// the provided username, password, and TLS connection state. func ScramSHA1PlusAuth(username, password string, tlsConnState *tls.ConnectionState) Auth { return &scramAuth{ username: username, @@ -73,11 +68,25 @@ func ScramSHA1PlusAuth(username, password string, tlsConnState *tls.ConnectionSt } } +// ScramSHA256PlusAuth returns an Auth instance configured for SCRAM-SHA-256-PLUS authentication with +// the provided username, password, and TLS connection state. +func ScramSHA256PlusAuth(username, password string, tlsConnState *tls.ConnectionState) Auth { + return &scramAuth{ + username: username, + password: password, + algorithm: "SCRAM-SHA-256-PLUS", + h: sha256.New, + isPlus: true, + tlsConnState: tlsConnState, + } +} + +// Start initializes the SCRAM authentication process and returns the selected algorithm, nil data, and no error. func (a *scramAuth) Start(_ *ServerInfo) (string, []byte, error) { - fmt.Printf("algo: %s\n", a.algorithm) return a.algorithm, nil, nil } +// Next processes the server's challenge and returns the client's response for SCRAM authentication. func (a *scramAuth) Next(fromServer []byte, more bool) ([]byte, error) { if more { if len(fromServer) == 0 { From 27838f5b1f8bde119bdc86f411e62ff9ca9511af Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 15:28:10 +0200 Subject: [PATCH 078/181] Improve TLS state handling and add SCRAM-SHA-256 auth support Replaced direct TLSConnectionState call with error handling for TLS state retrieval. Introduced SCRAM-SHA-256 support in the SMTP authentication process. --- client.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/client.go b/client.go index f7ed9f1..b692af3 100644 --- a/client.go +++ b/client.go @@ -748,7 +748,11 @@ func (c *Client) tls() error { return err } } - _, c.isEncrypted = c.smtpClient.TLSConnectionState() + tlsConnState, err := c.smtpClient.GetTLSConnectionState() + if err != nil { + return fmt.Errorf("failed to get TLS connection state: %w", err) + } + c.isEncrypted = tlsConnState.HandshakeComplete } return nil } @@ -790,6 +794,11 @@ func (c *Client) auth() error { return ErrXOauth2AuthNotSupported } c.smtpAuth = smtp.ScramSHA1Auth(c.user, c.pass) + case SMTPAuthSCRAMSHA256: + if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256)) { + return ErrXOauth2AuthNotSupported + } + c.smtpAuth = smtp.ScramSHA256Auth(c.user, c.pass) case SMTPAuthSCRAMSHA1PLUS: if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA1PLUS)) { return ErrXOauth2AuthNotSupported @@ -799,11 +808,6 @@ func (c *Client) auth() error { return err } c.smtpAuth = smtp.ScramSHA1PlusAuth(c.user, c.pass, tlsConnState) - case SMTPAuthSCRAMSHA256: - if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256)) { - return ErrXOauth2AuthNotSupported - } - c.smtpAuth = smtp.ScramSHA256Auth(c.user, c.pass) case SMTPAuthSCRAMSHA256PLUS: if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256PLUS)) { return ErrXOauth2AuthNotSupported @@ -813,7 +817,6 @@ func (c *Client) auth() error { return err } c.smtpAuth = smtp.ScramSHA256PlusAuth(c.user, c.pass, tlsConnState) - default: return fmt.Errorf("unsupported SMTP AUTH type %q", c.smtpAuthType) } From e8f3c444e6f7174a5532f08fd4e05a10a76abe15 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 15:28:53 +0200 Subject: [PATCH 079/181] Add SCRAM-SHA1-PLUS authentication tests Introduced two new unit tests for SCRAM-SHA1-PLUS authentication with TLS exporter and TLS unique options. These tests ensure proper client creation, connection, and disconnection processes are functioning as expected in online environments. --- client_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/client_test.go b/client_test.go index 2d37ce0..16ec031 100644 --- a/client_test.go +++ b/client_test.go @@ -1836,6 +1836,56 @@ func TestClient_DialSendConcurrent_local(t *testing.T) { } } +func TestClient_AuthSCRAMSHA1PLUS_tlsexporter(t *testing.T) { + if os.Getenv("TEST_ONLINE_SCRAM") == "" { + t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests") + } + hostname := os.Getenv("TEST_HOST_SCRAM") + username := os.Getenv("TEST_USER_SCRAM") + password := os.Getenv("TEST_PASS_SCRAM") + + client, err := NewClient(hostname, WithTLSPortPolicy(TLSMandatory), + WithSMTPAuth(SMTPAuthSCRAMSHA1PLUS), + WithUsername(username), WithPassword(password)) + if err != nil { + t.Errorf("unable to create new client: %s", err) + return + } + if err = client.DialWithContext(context.Background()); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Close(); err != nil { + t.Errorf("failed to close server connection: %s", err) + } +} + +func TestClient_AuthSCRAMSHA1PLUS_tlsunique(t *testing.T) { + if os.Getenv("TEST_ONLINE_SCRAM") == "" { + t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests") + } + hostname := os.Getenv("TEST_HOST_SCRAM") + username := os.Getenv("TEST_USER_SCRAM") + password := os.Getenv("TEST_PASS_SCRAM") + + tlsConfig := &tls.Config{} + tlsConfig.MaxVersion = tls.VersionTLS12 + tlsConfig.ServerName = hostname + client, err := NewClient(hostname, WithTLSPortPolicy(TLSMandatory), + WithTLSConfig(tlsConfig), + WithSMTPAuth(SMTPAuthSCRAMSHA1PLUS), + WithUsername(username), WithPassword(password)) + if err != nil { + t.Errorf("unable to create new client: %s", err) + return + } + if err = client.DialWithContext(context.Background()); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Close(); err != nil { + t.Errorf("failed to close server connection: %s", err) + } +} + // getTestConnection takes environment variables to establish a connection to a real // SMTP server to test all functionality that requires a connection func getTestConnection(auth bool) (*Client, error) { From e5b87db448f5182a55a79e91902f45897f93d987 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 15:41:26 +0200 Subject: [PATCH 080/181] Update README to clarify library features and dependencies Revised the README to provide clearer explanations of the library's origins, dependencies, and features. Added details on the small dependency footprint and enhanced SMTP Auth methods, and emphasized the concurrency-safe reuse of SMTP connections. --- README.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 260b888..cc2825f 100644 --- a/README.md +++ b/README.md @@ -18,33 +18,34 @@ SPDX-License-Identifier: CC0-1.0

go-mail logo

-The main idea of this library was to provide a simple interface to sending mails for +The main idea of this library was to provide a simple interface for sending mails to my [JS-Mailer](https://github.com/wneessen/js-mailer) project. It quickly evolved into a full-fledged mail library. -go-mail follows idiomatic Go style and best practice. It's only dependency is the Go Standard Library. It combines a lot -of functionality from the standard library to give easy and convenient access to mail and SMTP related tasks. +go-mail follows idiomatic Go style and best practice. It has a small dependency footprint by mainly relying on the +Go Standard Library and the Go extended packages. It combines a lot of functionality from the standard library to +give easy and convenient access to mail and SMTP related tasks. -Parts of this library (especially some parts of [msgwriter.go](msgwriter.go)) have been forked/ported from the -[go-mail/mail](https://github.com/go-mail/mail) respectively [go-gomail/gomail](https://github.com/go-gomail/gomail) -which both seems to not be maintained anymore. +In the early days, parts of this library (especially some parts of [msgwriter.go](msgwriter.go)) have been +forked/ported from [go-mail/mail](https://github.com/go-mail/mail) and respectively [go-gomail/gomail](https://github.com/go-gomail/gomail). In +the meantime most of the ported code has been refactored. -The smtp package of go-mail is forked from the original Go stdlib's `net/smtp` and then extended by the go-mail -team. +The smtp package of go-mail has been forked from the original Go stdlib's `net/smtp` package and has then been extended +by the go-mail team to fit the packages needs (more SMTP Auth methods, logging, concurrency-safety, etc.). ## Features -Some of the features of this library: +Here are some highlights of go-mail's featureset: -* [X] Only Standard Library dependant +* [X] Very small dependency footprint (mainly Go Stdlib and Go extended packages) * [X] Modern, idiomatic Go * [X] Sane and secure defaults * [X] Explicit SSL/TLS support * [X] Implicit StartTLS support with different policies * [X] Makes use of contexts for a better control flow and timeout/cancelation handling -* [X] SMTP Auth support (LOGIN, PLAIN, CRAM-MD, XOAUTH2) +* [X] SMTP Auth support (LOGIN, PLAIN, CRAM-MD, XOAUTH2, SCRAM-SHA-1(-PLUS), SCRAM-SHA-256(-PLUS)) * [X] RFC5322 compliant mail address validation * [X] Support for common mail header field generation (Message-ID, Date, Bulk-Precedence, Priority, etc.) -* [X] Reusing the same SMTP connection to send multiple mails +* [X] Concurrency-safe reusing the same SMTP connection to send multiple mails * [X] Support for attachments and inline embeds (from file system, `io.Reader` or `embed.FS`) * [X] Support for different encodings * [X] Middleware support for 3rd-party libraries to alter mail messages From cace4890bc57a750dfb0c7b21023cde1a5c98376 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 15:47:07 +0200 Subject: [PATCH 081/181] Update README.md wordings for clarity Refined the wording in the README.md to enhance readability and clarity. Changed some sentences to past perfect tense and added backticks around `smtp` for consistency with code references. --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cc2825f..2a4d997 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,11 @@ go-mail follows idiomatic Go style and best practice. It has a small dependency Go Standard Library and the Go extended packages. It combines a lot of functionality from the standard library to give easy and convenient access to mail and SMTP related tasks. -In the early days, parts of this library (especially some parts of [msgwriter.go](msgwriter.go)) have been -forked/ported from [go-mail/mail](https://github.com/go-mail/mail) and respectively [go-gomail/gomail](https://github.com/go-gomail/gomail). In -the meantime most of the ported code has been refactored. +In the early days, parts of this library (especially some parts of [msgwriter.go](msgwriter.go)) had been +forked/ported from [go-mail/mail](https://github.com/go-mail/mail) and respectively [go-gomail/gomail](https://github.com/go-gomail/gomail). Today +most of the ported code has been refactored. -The smtp package of go-mail has been forked from the original Go stdlib's `net/smtp` package and has then been extended +The `smtp` package of go-mail has been forked from the original Go stdlib's `net/smtp` package and has then been extended by the go-mail team to fit the packages needs (more SMTP Auth methods, logging, concurrency-safety, etc.). ## Features From 687843ee53a37cc6172db2f5e2062017d4d11bbe Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 15:57:01 +0200 Subject: [PATCH 082/181] Enhance contributors section and add sponsors acknowledgment Updated the Authors/Contributors section to include a graphical representation of contributors and added special thanks to Maria Letta for the logo design. Introduced a new Sponsors section to acknowledge the support from sponsors. --- README.md | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 2a4d997..463d1f1 100644 --- a/README.md +++ b/README.md @@ -100,15 +100,18 @@ We provide example code in both our GoDocs as well as on our official Website (s check out our [Getting started](https://go-mail.dev/getting-started/introduction/) guide. ## Authors/Contributors -go-mail was initially authored and developed by [Winni Neessen](https://github.com/wneessen/). +go-mail was initially created and developed by [Winni Neessen](https://github.com/wneessen/), but over time a lot of amazing people +contributed ot the project. Big thanks to all of them for improving the go-mail project (be it writing code, testing +code, reviewing code, writing documenation or helping to translate the website): -Big thanks to the following people, for contributing to the go-mail project (either in form of code or by -reviewing code, writing documenation or helping to translate the website): -* [Christian Vette](https://github.com/cvette) -* [Dhia Gharsallaoui](https://github.com/dhia-gharsallaoui) -* [inliquid](https://github.com/inliquid) -* [iwittkau](https://github.com/iwittkau) -* [James Elliott](https://github.com/james-d-elliott) -* [Maria Letta](https://github.com/MariaLetta) (designed the go-mail logo) -* [Nicola Murino](https://github.com/drakkan) -* [sters](https://github.com/sters) + + + + +A huge thank you also goes to [Maria Letta](https://github.com/MariaLetta) for designing our super cool go-mail logo! + +## Sponsors +We sincerely thank all our amazing sponsors for their generous support, without whom this project would not +have achieved such development and success as it has today. Your contributions do not go unnoticed! + +* [kolaente](https://github.com/kolaente) From abab0af2a378c3d85c3ded56ab7e08ac2fa3cb27 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 16:01:58 +0200 Subject: [PATCH 083/181] Simplify sponsor appreciation message Revise the sponsors section in README.md to convey gratitude more concisely. Removed redundant phrasing and made the message more direct while ensuring the intent remains clear. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 463d1f1..6a34055 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ code, reviewing code, writing documenation or helping to translate the website): A huge thank you also goes to [Maria Letta](https://github.com/MariaLetta) for designing our super cool go-mail logo! ## Sponsors -We sincerely thank all our amazing sponsors for their generous support, without whom this project would not -have achieved such development and success as it has today. Your contributions do not go unnoticed! +We sincerely thank our amazing sponsors for their generous support! Your contributions do not go unnoticed and helps +keeping up the project! * [kolaente](https://github.com/kolaente) From bcf70849821a12b3eed3345cc9dd778f63dd9d9f Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 16:39:13 +0200 Subject: [PATCH 084/181] Add detailed documentation comments for SCRAM methods Enhanced code readability and maintainability by adding comprehensive documentation comments to all methods and struct definitions in the `smtp/auth_scram.go` file. This improves clarity on the functionality and usage of the SCRAM (Salted Challenge Response Authentication Mechanism) methods and structures. --- smtp/auth_scram.go | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/smtp/auth_scram.go b/smtp/auth_scram.go index 14308fa..c70b210 100644 --- a/smtp/auth_scram.go +++ b/smtp/auth_scram.go @@ -23,6 +23,8 @@ import ( "golang.org/x/text/secure/precis" ) +// scramAuth represents a SCRAM (Salted Challenge Response Authentication Mechanism) client and +// satisfies the smtp.Auth interface. type scramAuth struct { username, password, algorithm string firstBareMsg, nonce, saltedPwd, authMessage []byte @@ -116,6 +118,7 @@ func (a *scramAuth) Next(fromServer []byte, more bool) ([]byte, error) { return nil, nil } +// reset clears all authentication-related properties in the scramAuth instance, effectively resetting its state. func (a *scramAuth) reset() { a.nonce = nil a.firstBareMsg = nil @@ -124,6 +127,8 @@ func (a *scramAuth) reset() { a.iterations = 0 } +// initialClientMessage generates the initial message for SCRAM authentication, including a nonce and +// optional channel binding. func (a *scramAuth) initialClientMessage() ([]byte, error) { username, err := a.normalizeUsername() if err != nil { @@ -140,11 +145,16 @@ func (a *scramAuth) initialClientMessage() ([]byte, error) { a.firstBareMsg = []byte("n=" + username + ",r=" + string(a.nonce)) returnBytes := []byte("n,," + string(a.firstBareMsg)) + // SCRAM-SHA-X-PLUS auth requires channel binding if a.isPlus { bindType := "tls-unique" connState := a.tlsConnState bindData := connState.TLSUnique - if connState.Version == tls.VersionTLS13 { + + // crypto/tl: no tls-unique channel binding value for this tls connection, possibly due to missing + // extended master key support and/or resumed connection + // RFC9266:122 tls-unique not defined for tls 1.3 and later + if bindData == nil || connState.Version >= tls.VersionTLS13 { bindType = "tls-exporter" bindData, err = connState.ExportKeyingMaterial("EXPORTER-Channel-Binding", []byte{}, 32) if err != nil { @@ -160,6 +170,7 @@ func (a *scramAuth) initialClientMessage() ([]byte, error) { return returnBytes, nil } +// handleServerFirstResponse processes the first response from the server in SCRAM authentication. func (a *scramAuth) handleServerFirstResponse(fromServer []byte) ([]byte, error) { parts := bytes.Split(fromServer, []byte(",")) if len(parts) < 3 { @@ -203,16 +214,19 @@ func (a *scramAuth) handleServerFirstResponse(fromServer []byte) ([]byte, error) a.saltedPwd = pbkdf2.Key([]byte(password), salt, a.iterations, a.h().Size(), a.h) msgWithoutProof := []byte("c=biws,r=" + string(a.nonce)) + + // A PLUS authentication requires the channel binding data if a.isPlus { msgWithoutProof = []byte("c=" + string(a.bindData) + ",r=" + string(a.nonce)) } - a.authMessage = []byte(string(a.firstBareMsg) + "," + string(fromServer) + "," + string(msgWithoutProof)) + a.authMessage = []byte(string(a.firstBareMsg) + "," + string(fromServer) + "," + string(msgWithoutProof)) clientProof := a.computeClientProof() return []byte(string(msgWithoutProof) + ",p=" + string(clientProof)), nil } +// handleServerValidationMessage verifies the server's signature during the SCRAM authentication process. func (a *scramAuth) handleServerValidationMessage(fromServer []byte) ([]byte, error) { serverSignature := fromServer[2:] computedServerSignature := a.computeServerSignature() @@ -223,18 +237,21 @@ func (a *scramAuth) handleServerValidationMessage(fromServer []byte) ([]byte, er return []byte(""), nil } +// computeHMAC generates a Hash-based Message Authentication Code (HMAC) using the specified key and message. func (a *scramAuth) computeHMAC(key, msg []byte) []byte { mac := hmac.New(a.h, key) mac.Write(msg) return mac.Sum(nil) } +// computeHash generates a hash of the given key using the configured hashing algorithm. func (a *scramAuth) computeHash(key []byte) []byte { hasher := a.h() hasher.Write(key) return hasher.Sum(nil) } +// computeClientProof generates the client proof as part of the SCRAM authentication process. func (a *scramAuth) computeClientProof() []byte { clientKey := a.computeHMAC(a.saltedPwd, []byte("Client Key")) storedKey := a.computeHash(clientKey) @@ -248,6 +265,8 @@ func (a *scramAuth) computeClientProof() []byte { return buf } +// computeServerSignature returns the computed base64-encoded server signature in the SCRAM +// authentication process. func (a *scramAuth) computeServerSignature() []byte { serverKey := a.computeHMAC(a.saltedPwd, []byte("Server Key")) serverSignature := a.computeHMAC(serverKey, a.authMessage) @@ -256,6 +275,9 @@ func (a *scramAuth) computeServerSignature() []byte { return buf } +// normalizeUsername replaces special characters in the username for SCRAM authentication +// and prepares it using the SASLprep profile as per RFC 8265, returning the normalized +// username or an error. func (a *scramAuth) normalizeUsername() (string, error) { // RFC 5802 section 5.1: the characters ',' or '=' in usernames are // sent as '=2C' and '=3D' respectively. @@ -277,10 +299,13 @@ func (a *scramAuth) normalizeUsername() (string, error) { return username, nil } +// normalizeString normalizes the input string according to the OpaqueString profile of the +// precis framework. It returns the normalized string or an error if normalization fails or +// results in an empty string. func (a *scramAuth) normalizeString(s string) (string, error) { s, err := precis.OpaqueString.String(s) if err != nil { - return "", err + return "", fmt.Errorf("failled to normalize string: %w", err) } if s == "" { return "", errors.New("normalized string is empty") From 324be9d0329af8934aeaad65d92cc0d5852b7ed9 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 16:43:36 +0200 Subject: [PATCH 085/181] Refactor SCRAM tests to include SHA-256-PLUS Updated `TestClient_AuthSCRAMSHA1PLUS_tlsexporter` and `TestClient_AuthSCRAMSHA1PLUS_tlsunique` to test both SCRAM-SHA-1-PLUS and SCRAM-SHA-256-PLUS authentication types. Implemented table-driven tests to improve readability and maintainability. --- client_test.go | 78 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 26 deletions(-) diff --git a/client_test.go b/client_test.go index 16ec031..1c0641a 100644 --- a/client_test.go +++ b/client_test.go @@ -1836,7 +1836,7 @@ func TestClient_DialSendConcurrent_local(t *testing.T) { } } -func TestClient_AuthSCRAMSHA1PLUS_tlsexporter(t *testing.T) { +func TestClient_AuthSCRAMSHAXPLUS_tlsexporter(t *testing.T) { if os.Getenv("TEST_ONLINE_SCRAM") == "" { t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests") } @@ -1844,45 +1844,71 @@ func TestClient_AuthSCRAMSHA1PLUS_tlsexporter(t *testing.T) { username := os.Getenv("TEST_USER_SCRAM") password := os.Getenv("TEST_PASS_SCRAM") - client, err := NewClient(hostname, WithTLSPortPolicy(TLSMandatory), - WithSMTPAuth(SMTPAuthSCRAMSHA1PLUS), - WithUsername(username), WithPassword(password)) - if err != nil { - t.Errorf("unable to create new client: %s", err) - return + tests := []struct { + name string + authtype SMTPAuthType + }{ + {"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS}, + {"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS}, } - if err = client.DialWithContext(context.Background()); err != nil { - t.Errorf("failed to dial to test server: %s", err) - } - if err = client.Close(); err != nil { - t.Errorf("failed to close server connection: %s", err) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := NewClient(hostname, + WithTLSPortPolicy(TLSMandatory), + WithSMTPAuth(tt.authtype), + WithUsername(username), WithPassword(password)) + if err != nil { + t.Errorf("unable to create new client: %s", err) + return + } + if err = client.DialWithContext(context.Background()); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Close(); err != nil { + t.Errorf("failed to close server connection: %s", err) + } + }) } } -func TestClient_AuthSCRAMSHA1PLUS_tlsunique(t *testing.T) { +func TestClient_AuthSCRAMSHAXPLUS_tlsunique(t *testing.T) { if os.Getenv("TEST_ONLINE_SCRAM") == "" { t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests") } hostname := os.Getenv("TEST_HOST_SCRAM") username := os.Getenv("TEST_USER_SCRAM") password := os.Getenv("TEST_PASS_SCRAM") - tlsConfig := &tls.Config{} tlsConfig.MaxVersion = tls.VersionTLS12 tlsConfig.ServerName = hostname - client, err := NewClient(hostname, WithTLSPortPolicy(TLSMandatory), - WithTLSConfig(tlsConfig), - WithSMTPAuth(SMTPAuthSCRAMSHA1PLUS), - WithUsername(username), WithPassword(password)) - if err != nil { - t.Errorf("unable to create new client: %s", err) - return + + tests := []struct { + name string + authtype SMTPAuthType + }{ + {"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS}, + {"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS}, } - if err = client.DialWithContext(context.Background()); err != nil { - t.Errorf("failed to dial to test server: %s", err) - } - if err = client.Close(); err != nil { - t.Errorf("failed to close server connection: %s", err) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := NewClient(hostname, + WithTLSPortPolicy(TLSMandatory), + WithTLSConfig(tlsConfig), + WithSMTPAuth(tt.authtype), + WithUsername(username), WithPassword(password)) + if err != nil { + t.Errorf("unable to create new client: %s", err) + return + } + if err = client.DialWithContext(context.Background()); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Close(); err != nil { + t.Errorf("failed to close server connection: %s", err) + } + }) } } From 7499bae3eb810264493ed3fb2737cf364859948a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 16:45:02 +0200 Subject: [PATCH 086/181] Add unit tests for SCRAM-SHA authentication methods Introduce `TestClient_AuthSCRAMSHAX` to verify SCRAM-SHA-1 and SCRAM-SHA-256 authentication. These tests validate the creation, connection, and closing of clients with the respective authentication methods using environment-configured credentials. --- client_test.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/client_test.go b/client_test.go index 1c0641a..888bf8b 100644 --- a/client_test.go +++ b/client_test.go @@ -1836,6 +1836,42 @@ func TestClient_DialSendConcurrent_local(t *testing.T) { } } +func TestClient_AuthSCRAMSHAX(t *testing.T) { + if os.Getenv("TEST_ONLINE_SCRAM") == "" { + t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests") + } + hostname := os.Getenv("TEST_HOST_SCRAM") + username := os.Getenv("TEST_USER_SCRAM") + password := os.Getenv("TEST_PASS_SCRAM") + + tests := []struct { + name string + authtype SMTPAuthType + }{ + {"SCRAM-SHA-1", SMTPAuthSCRAMSHA1}, + {"SCRAM-SHA-256", SMTPAuthSCRAMSHA256}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := NewClient(hostname, + WithTLSPortPolicy(TLSMandatory), + WithSMTPAuth(tt.authtype), + WithUsername(username), WithPassword(password)) + if err != nil { + t.Errorf("unable to create new client: %s", err) + return + } + if err = client.DialWithContext(context.Background()); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Close(); err != nil { + t.Errorf("failed to close server connection: %s", err) + } + }) + } +} + func TestClient_AuthSCRAMSHAXPLUS_tlsexporter(t *testing.T) { if os.Getenv("TEST_ONLINE_SCRAM") == "" { t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests") From b69ad27de318de9276a90ba5b2b7db2667b554b6 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 17:00:43 +0200 Subject: [PATCH 087/181] Add comments for SMTP authentication mechanisms Enhanced the documentation by adding detailed comments for each SMTP authentication type, specifying their references to RFC documents. Corrected comments for error variables to match the corresponding authentication schemas. --- auth.go | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/auth.go b/auth.go index d30a154..f1dad86 100644 --- a/auth.go +++ b/auth.go @@ -29,9 +29,20 @@ const ( // https://developers.google.com/gmail/imap/xoauth2-protocol SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2" - SMTPAuthSCRAMSHA1 SMTPAuthType = "SCRAM-SHA-1" - SMTPAuthSCRAMSHA1PLUS SMTPAuthType = "SCRAM-SHA-1-PLUS" - SMTPAuthSCRAMSHA256 SMTPAuthType = "SCRAM-SHA-256" + // SMTPAuthSCRAMSHA1 represents the SCRAM-SHA-1 SMTP authentication mechanism + // https://datatracker.ietf.org/doc/html/rfc5802 + SMTPAuthSCRAMSHA1 SMTPAuthType = "SCRAM-SHA-1" + + // SMTPAuthSCRAMSHA1PLUS represents the "SCRAM-SHA-1-PLUS" authentication mechanism for SMTP. + // https://datatracker.ietf.org/doc/html/rfc5802 + SMTPAuthSCRAMSHA1PLUS SMTPAuthType = "SCRAM-SHA-1-PLUS" + + // SMTPAuthSCRAMSHA256 represents the SCRAM-SHA-256 authentication mechanism for SMTP. + // https://datatracker.ietf.org/doc/html/rfc7677 + SMTPAuthSCRAMSHA256 SMTPAuthType = "SCRAM-SHA-256" + + // SMTPAuthSCRAMSHA256PLUS represents the "SCRAM-SHA-256-PLUS" SMTP AUTH type. + // https://datatracker.ietf.org/doc/html/rfc7677 SMTPAuthSCRAMSHA256PLUS SMTPAuthType = "SCRAM-SHA-256-PLUS" ) @@ -49,15 +60,15 @@ var ( // ErrXOauth2AuthNotSupported should be used if the target server does not support the "XOAUTH2" schema ErrXOauth2AuthNotSupported = errors.New("server does not support SMTP AUTH type: XOAUTH2") - // ErrSCRAMSHA1AuthNotSupported should be used if the target server does not support the "XOAUTH2" schema + // ErrSCRAMSHA1AuthNotSupported should be used if the target server does not support the "SCRAM-SHA-1" schema ErrSCRAMSHA1AuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-1") - // ErrSCRAMSHA1PLUSAuthNotSupported should be used if the target server does not support the "XOAUTH2" schema + // ErrSCRAMSHA1PLUSAuthNotSupported should be used if the target server does not support the "SCRAM-SHA-1-PLUS" schema ErrSCRAMSHA1PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-1-PLUS") - // ErrSCRAMSHA256AuthNotSupported should be used if the target server does not support the "XOAUTH2" schema + // ErrSCRAMSHA256AuthNotSupported should be used if the target server does not support the "SCRAM-SHA-256" schema ErrSCRAMSHA256AuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256") - // ErrSCRAMSHA256PLUSAuthNotSupported should be used if the target server does not support the "XOAUTH2" schema + // ErrSCRAMSHA256PLUSAuthNotSupported should be used if the target server does not support the "SCRAM-SHA-256-PLUS" schema ErrSCRAMSHA256PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256-PLUS") ) From 8838414c3814e7469907ae0ba137b4e0ffdf0020 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 17:00:57 +0200 Subject: [PATCH 088/181] Fix incorrect error types for unsupported SMTP authentication Corrects the error messages returned for unsupported SMTP authentication types from ErrXOauth2AuthNotSupported to specific errors like ErrSCRAMSHA1AuthNotSupported, ErrSCRAMSHA256AuthNotSupported, and so on. This change improves the accuracy of error reporting for various SMTP authentication mechanisms. --- client.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client.go b/client.go index b692af3..acfeaa2 100644 --- a/client.go +++ b/client.go @@ -791,17 +791,17 @@ func (c *Client) auth() error { c.smtpAuth = smtp.XOAuth2Auth(c.user, c.pass) case SMTPAuthSCRAMSHA1: if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA1)) { - return ErrXOauth2AuthNotSupported + return ErrSCRAMSHA1AuthNotSupported } c.smtpAuth = smtp.ScramSHA1Auth(c.user, c.pass) case SMTPAuthSCRAMSHA256: if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256)) { - return ErrXOauth2AuthNotSupported + return ErrSCRAMSHA256AuthNotSupported } c.smtpAuth = smtp.ScramSHA256Auth(c.user, c.pass) case SMTPAuthSCRAMSHA1PLUS: if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA1PLUS)) { - return ErrXOauth2AuthNotSupported + return ErrSCRAMSHA1PLUSAuthNotSupported } tlsConnState, err := c.smtpClient.GetTLSConnectionState() if err != nil { @@ -810,7 +810,7 @@ func (c *Client) auth() error { c.smtpAuth = smtp.ScramSHA1PlusAuth(c.user, c.pass, tlsConnState) case SMTPAuthSCRAMSHA256PLUS: if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256PLUS)) { - return ErrXOauth2AuthNotSupported + return ErrSCRAMSHA256PLUSAuthNotSupported } tlsConnState, err := c.smtpClient.GetTLSConnectionState() if err != nil { From 5058fd522285aecd0de64e3bc384048e525b7215 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 17:01:10 +0200 Subject: [PATCH 089/181] Add test for SCRAM-SHA authentication failure cases Implemented tests for various SCRAM-SHA authentication methods including SCRAM-SHA-1, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, and SCRAM-SHA-256-PLUS with invalid credentials. This ensures that the client correctly handles and reports authentication failures. --- client_test.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/client_test.go b/client_test.go index 888bf8b..83d710c 100644 --- a/client_test.go +++ b/client_test.go @@ -1872,6 +1872,39 @@ func TestClient_AuthSCRAMSHAX(t *testing.T) { } } +func TestClient_AuthSCRAMSHAX_fail(t *testing.T) { + if os.Getenv("TEST_ONLINE_SCRAM") == "" { + t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests") + } + hostname := os.Getenv("TEST_HOST_SCRAM") + + tests := []struct { + name string + authtype SMTPAuthType + }{ + {"SCRAM-SHA-1", SMTPAuthSCRAMSHA1}, + {"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS}, + {"SCRAM-SHA-256", SMTPAuthSCRAMSHA256}, + {"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := NewClient(hostname, + WithTLSPortPolicy(TLSMandatory), + WithSMTPAuth(tt.authtype), + WithUsername("invalid"), WithPassword("invalid")) + if err != nil { + t.Errorf("unable to create new client: %s", err) + return + } + if err = client.DialWithContext(context.Background()); err == nil { + t.Errorf("expected error but got nil") + } + }) + } +} + func TestClient_AuthSCRAMSHAXPLUS_tlsexporter(t *testing.T) { if os.Getenv("TEST_ONLINE_SCRAM") == "" { t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests") From 15b9ddf0675fbceeff3091248030eea511df2bc2 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 17:23:29 +0200 Subject: [PATCH 090/181] Refactor error handling for non-TLS SMTP connections Introduce a global error variable for non-TLS connections and update corresponding error handling across the codebase. This enhances readability and maintainability of the error management logic. --- client.go | 8 +++++++- smtp/smtp.go | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index acfeaa2..5d0e797 100644 --- a/client.go +++ b/client.go @@ -750,7 +750,13 @@ func (c *Client) tls() error { } tlsConnState, err := c.smtpClient.GetTLSConnectionState() if err != nil { - return fmt.Errorf("failed to get TLS connection state: %w", err) + switch { + case errors.Is(err, smtp.ErrNonTLSConnection): + c.isEncrypted = false + return nil + default: + return fmt.Errorf("failed to get TLS connection state: %w", err) + } } c.isEncrypted = tlsConnState.HandshakeComplete } diff --git a/smtp/smtp.go b/smtp/smtp.go index e834a09..0352133 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -36,6 +36,10 @@ import ( "github.com/wneessen/go-mail/log" ) +var ( + ErrNonTLSConnection = errors.New("connection is not using TLS") +) + // A Client represents a client connection to an SMTP server. type Client struct { // Text is the textproto.Conn used by the Client. It is exported to allow for clients to add extensions. @@ -572,7 +576,7 @@ func (c *Client) GetTLSConnectionState() (*tls.ConnectionState, error) { defer c.mutex.RUnlock() if !c.tls { - return nil, errors.New("smtp: connection is not using TLS") + return nil, ErrNonTLSConnection } if c.conn == nil { return nil, errors.New("smtp: connection is not established") From f823112a4d74d18f8837ba4e351d4a426cee1f23 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 20:32:41 +0200 Subject: [PATCH 091/181] Refactor: consolidate ErrNonTLSConnection variable The variable ErrNonTLSConnection has been simplified from a multi-line declaration to a single-line declaration. This increases code readability and maintains consistency with Go conventions. --- smtp/smtp.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/smtp/smtp.go b/smtp/smtp.go index 0352133..f9961c9 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -36,9 +36,7 @@ import ( "github.com/wneessen/go-mail/log" ) -var ( - ErrNonTLSConnection = errors.New("connection is not using TLS") -) +var ErrNonTLSConnection = errors.New("connection is not using TLS") // A Client represents a client connection to an SMTP server. type Client struct { From 986a988c5d2a3eb45a582231630b102345bcfb4c Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 20:44:50 +0200 Subject: [PATCH 092/181] Reset SMTP auth when setting SMTP auth type This change ensures that the smtpAuth field is reset to nil whenever the SMTP auth type is updated. This prevents potential issues with mismatched authentication settings. --- client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/client.go b/client.go index 5d0e797..4d1b0b1 100644 --- a/client.go +++ b/client.go @@ -578,6 +578,7 @@ func (c *Client) SetPassword(password string) { // SetSMTPAuth overrides the current SMTP AUTH type setting with the given value func (c *Client) SetSMTPAuth(authtype SMTPAuthType) { c.smtpAuthType = authtype + c.smtpAuth = nil } // SetSMTPAuthCustom overrides the current SMTP AUTH setting with the given custom smtp.Auth From 72b3f53eb788979ea1bd8f9d85621f1d2d775280 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 1 Oct 2024 20:45:07 +0200 Subject: [PATCH 093/181] Add tests for unsupported SCRAM-SHA authentications Introduce a new test case `TestClient_AuthSCRAMSHAX_unsupported` to validate handling of unsupported SCRAM-SHA authentication methods. This ensures the client returns the correct errors when setting unsupported auth types. --- client_test.go | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/client_test.go b/client_test.go index 83d710c..481b9be 100644 --- a/client_test.go +++ b/client_test.go @@ -1905,6 +1905,41 @@ func TestClient_AuthSCRAMSHAX_fail(t *testing.T) { } } +func TestClient_AuthSCRAMSHAX_unsupported(t *testing.T) { + if os.Getenv("TEST_ALLOW_SEND") == "" { + t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test") + } + + client, err := getTestConnection(true) + if err != nil { + t.Skipf("failed to create test client: %s. Skipping tests", err) + } + + tests := []struct { + name string + authtype SMTPAuthType + expErr error + }{ + {"SCRAM-SHA-1", SMTPAuthSCRAMSHA1, ErrSCRAMSHA1AuthNotSupported}, + {"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS, ErrSCRAMSHA1PLUSAuthNotSupported}, + {"SCRAM-SHA-256", SMTPAuthSCRAMSHA256, ErrSCRAMSHA256AuthNotSupported}, + {"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS, ErrSCRAMSHA256PLUSAuthNotSupported}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client.SetSMTPAuth(tt.authtype) + client.SetTLSPolicy(TLSMandatory) + if err = client.DialWithContext(context.Background()); err == nil { + t.Errorf("expected error but got nil") + } + if !errors.Is(err, tt.expErr) { + t.Errorf("expected error %s, but got %s", tt.expErr, err) + } + }) + } +} + func TestClient_AuthSCRAMSHAXPLUS_tlsexporter(t *testing.T) { if os.Getenv("TEST_ONLINE_SCRAM") == "" { t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests") @@ -2023,10 +2058,10 @@ func getTestConnection(auth bool) (*Client, error) { // We don't want to log authentication data in tests c.SetDebugLog(false) } - if err := c.DialWithContext(context.Background()); err != nil { + if err = c.DialWithContext(context.Background()); err != nil { return c, fmt.Errorf("connection to test server failed: %w", err) } - if err := c.Close(); err != nil { + if err = c.Close(); err != nil { return c, fmt.Errorf("disconnect from test server failed: %w", err) } return c, nil From 547f78dbee8fb1777b4a96350b845c702f5986eb Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 2 Oct 2024 12:37:54 +0200 Subject: [PATCH 094/181] Enhance SMTP LOGIN auth and add comprehensive tests Refactored SMTP LOGIN auth to improve compatibility with various server responses, consolidating error handling and response steps. Added extensive tests to verify successful and failed authentication across different server configurations. --- client_test.go | 158 +++++++++++++++++++++++++++++++++++++++++++++ smtp/auth_login.go | 65 ++++++++----------- 2 files changed, 185 insertions(+), 38 deletions(-) diff --git a/client_test.go b/client_test.go index 481b9be..35fca38 100644 --- a/client_test.go +++ b/client_test.go @@ -1872,6 +1872,119 @@ func TestClient_AuthSCRAMSHAX(t *testing.T) { } } +func TestClient_AuthLoginSuccess(t *testing.T) { + tests := []struct { + name string + featureSet string + }{ + {"default", "250-AUTH LOGIN\r\n250-X-DEFAULT-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"}, + {"mox server", "250-AUTH LOGIN\r\n250-X-MOX-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"}, + {"null byte", "250-AUTH LOGIN\r\n250-X-NULLBYTE-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"}, + {"bogus responses", "250-AUTH LOGIN\r\n250-X-BOGUS-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"}, + {"empty responses", "250-AUTH LOGIN\r\n250-X-EMPTY-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"}, + } + + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + serverPort := TestServerPortBase + 40 + i + go func() { + if err := simpleSMTPServer(ctx, tt.featureSet, true, serverPort); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + client, err := NewClient(TestServerAddr, + WithPort(serverPort), + WithTLSPortPolicy(NoTLS), + WithSMTPAuth(SMTPAuthLogin), + WithUsername("toni@tester.com"), + WithPassword("V3ryS3cr3t+")) + if err != nil { + t.Errorf("unable to create new client: %s", err) + return + } + if err = client.DialWithContext(context.Background()); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Close(); err != nil { + t.Errorf("failed to close server connection: %s", err) + } + }) + } +} + +func TestClient_AuthLoginFail(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + serverPort := TestServerPortBase + 50 + featureSet := "250-AUTH LOGIN\r\n250-X-DEFAULT-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + client, err := NewClient(TestServerAddr, + WithPort(serverPort), + WithTLSPortPolicy(NoTLS), + WithSMTPAuth(SMTPAuthLogin), + WithUsername("toni@tester.com"), + WithPassword("InvalidPassword")) + if err != nil { + t.Errorf("unable to create new client: %s", err) + return + } + if err = client.DialWithContext(context.Background()); err == nil { + t.Error("expected to fail to dial to test server, but it succeeded") + } +} + +func TestClient_AuthLoginFail_noTLS(t *testing.T) { + if os.Getenv("TEST_SKIP_ONLINE") != "" { + t.Skipf("env variable TEST_SKIP_ONLINE is set. Skipping online tests") + } + th := os.Getenv("TEST_HOST") + if th == "" { + t.Skipf("no host set. Skipping online tests") + } + tp := 587 + if tps := os.Getenv("TEST_PORT"); tps != "" { + tpi, err := strconv.Atoi(tps) + if err == nil { + tp = tpi + } + } + client, err := NewClient(th, WithPort(tp), WithSMTPAuth(SMTPAuthLogin), WithTLSPolicy(NoTLS)) + if err != nil { + t.Errorf("failed to create new client: %s", err) + } + u := os.Getenv("TEST_SMTPAUTH_USER") + if u != "" { + client.SetUsername(u) + } + p := os.Getenv("TEST_SMTPAUTH_PASS") + if p != "" { + client.SetPassword(p) + } + // We don't want to log authentication data in tests + client.SetDebugLog(false) + + if err = client.DialWithContext(context.Background()); err == nil { + t.Error("expected to fail to dial to test server, but it succeeded") + } + if !errors.Is(err, smtp.ErrUnencrypted) { + t.Errorf("expected error to be %s, but got %s", smtp.ErrUnencrypted, err) + } +} + func TestClient_AuthSCRAMSHAX_fail(t *testing.T) { if os.Getenv("TEST_ONLINE_SCRAM") == "" { t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests") @@ -2491,6 +2604,51 @@ func handleTestServerConnection(connection net.Conn, featureSet string, failRese break } _ = writeLine("235 2.7.0 Authentication successful") + case strings.HasPrefix(data, "AUTH LOGIN"): + var username, password string + userResp := "VXNlcm5hbWU6" + passResp := "UGFzc3dvcmQ6" + if strings.Contains(featureSet, "250-X-MOX-LOGIN") { + userResp = "" + passResp = "UGFzc3dvcmQ=" + } + if strings.Contains(featureSet, "250-X-NULLBYTE-LOGIN") { + userResp = "VXNlciBuYW1lAA==" + passResp = "UGFzc3dvcmQA" + } + if strings.Contains(featureSet, "250-X-BOGUS-LOGIN") { + userResp = "Qm9ndXM=" + passResp = "Qm9ndXM=" + } + if strings.Contains(featureSet, "250-X-EMPTY-LOGIN") { + userResp = "" + passResp = "" + } + _ = writeLine("334 " + userResp) + + ddata, derr := reader.ReadString('\n') + if derr != nil { + fmt.Printf("failed to read username data from connection: %s\n", derr) + break + } + ddata = strings.TrimSpace(ddata) + username = ddata + _ = writeLine("334 " + passResp) + + ddata, derr = reader.ReadString('\n') + if derr != nil { + fmt.Printf("failed to read password data from connection: %s\n", derr) + break + } + ddata = strings.TrimSpace(ddata) + password = ddata + + if !strings.EqualFold(username, "dG9uaUB0ZXN0ZXIuY29t") || + !strings.EqualFold(password, "VjNyeVMzY3IzdCs=") { + _ = writeLine("535 5.7.8 Error: authentication failed") + break + } + _ = writeLine("235 2.7.0 Authentication successful") case strings.EqualFold(data, "DATA"): _ = writeLine("354 End data with .") for { diff --git a/smtp/auth_login.go b/smtp/auth_login.go index aa80223..e25b3e6 100644 --- a/smtp/auth_login.go +++ b/smtp/auth_login.go @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright (c) 2022-2023 The go-mail Authors +// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors // // SPDX-License-Identifier: MIT @@ -9,57 +9,42 @@ import ( "fmt" ) +// ErrUnencrypted is an error indicating that the connection is not encrypted. +var ErrUnencrypted = errors.New("unencrypted connection") + // loginAuth is the type that satisfies the Auth interface for the "SMTP LOGIN" auth type loginAuth struct { username, password string host string + respStep uint8 } -const ( - // LoginXUsernameChallenge represents the Username Challenge response sent by the SMTP server per the AUTH LOGIN - // extension. - // - // See: https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-xlogin/. - LoginXUsernameChallenge = "Username:" - LoginXUsernameLowerChallenge = "username:" - - // LoginXPasswordChallenge represents the Password Challenge response sent by the SMTP server per the AUTH LOGIN - // extension. - // - // See: https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-xlogin/. - LoginXPasswordChallenge = "Password:" - LoginXPasswordLowerChallenge = "password:" - - // LoginXDraftUsernameChallenge represents the Username Challenge response sent by the SMTP server per the IETF - // draft AUTH LOGIN extension. It should be noted this extension is an expired draft which was never formally - // published and was deprecated in favor of the AUTH PLAIN extension. - // - // See: https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00. - LoginXDraftUsernameChallenge = "User Name\x00" - - // LoginXDraftPasswordChallenge represents the Password Challenge response sent by the SMTP server per the IETF - // draft AUTH LOGIN extension. It should be noted this extension is an expired draft which was never formally - // published and was deprecated in favor of the AUTH PLAIN extension. - // - // See: https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00. - LoginXDraftPasswordChallenge = "Password\x00" -) - // LoginAuth returns an [Auth] that implements the LOGIN authentication // mechanism as it is used by MS Outlook. The Auth works similar to PLAIN // but instead of sending all in one response, the login is handled within // 3 steps: -// - Sending AUTH LOGIN (server responds with "Username:") -// - Sending the username (server responds with "Password:") +// - Sending AUTH LOGIN (server might responds with "Username:") +// - Sending the username (server might responds with "Password:") // - Sending the password (server authenticates) +// This is the common approach as specified by Microsoft in their MS-XLOGIN spec. +// See: https://msopenspecs.azureedge.net/files/MS-XLOGIN/%5bMS-XLOGIN%5d.pdf +// Yet, there is also an old IETF draft for SMTP AUTH LOGIN that states for clients: +// "The contents of both challenges SHOULD be ignored.". +// 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 +// 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} + return &loginAuth{username, password, host, 0} } +// Start begins the SMTP authentication process by validating server's TLS status and hostname. +// Returns "LOGIN" on success. func (a *loginAuth) Start(server *ServerInfo) (string, []byte, error) { // Must have TLS, or else localhost server. // Note: If TLS is not true, then we can't trust ANYTHING in ServerInfo. @@ -67,7 +52,7 @@ func (a *loginAuth) Start(server *ServerInfo) (string, []byte, error) { // That might just be the attacker saying // "it's ok, you can trust me with your password." if !server.TLS && !isLocalhost(server.Name) { - return "", nil, errors.New("unencrypted connection") + return "", nil, ErrUnencrypted } if server.Name != a.host { return "", nil, errors.New("wrong host name") @@ -75,12 +60,16 @@ func (a *loginAuth) Start(server *ServerInfo) (string, []byte, error) { return "LOGIN", nil, nil } +// Next processes responses from the server during the SMTP authentication exchange, sending the +// username and password. func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { if more { - switch string(fromServer) { - case LoginXUsernameChallenge, LoginXUsernameLowerChallenge, LoginXDraftUsernameChallenge: + switch a.respStep { + case 0: + a.respStep++ return []byte(a.username), nil - case LoginXPasswordChallenge, LoginXPasswordLowerChallenge, LoginXDraftPasswordChallenge: + case 1: + a.respStep++ return []byte(a.password), nil default: return nil, fmt.Errorf("unexpected server response: %s", string(fromServer)) From 93752280aaa1ec6b0ae39e97858e908dc78acd7a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 2 Oct 2024 12:54:32 +0200 Subject: [PATCH 095/181] Update smtp_test.go to add more authentication test cases Enhanced the LoginAuth test coverage by adding new scenarios with different sequences and invalid cases. This ensures more robust validation and better handling of edge cases in authentication testing. --- smtp/smtp_test.go | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 6df6eeb..d5b02a7 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -57,10 +57,31 @@ var authTests = []authTest{ }, { LoginAuth("user", "pass", "testserver"), - []string{"Username:", "Password:", "User Name\x00", "Password\x00", "Invalid:"}, + []string{"Username:", "Password:"}, "LOGIN", - []string{"", "user", "pass", "user", "pass", ""}, - []bool{false, false, false, false, true}, + []string{"", "user", "pass"}, + []bool{false, false}, + }, + { + LoginAuth("user", "pass", "testserver"), + []string{"User Name\x00", "Password\x00"}, + "LOGIN", + []string{"", "user", "pass"}, + []bool{false, false}, + }, + { + LoginAuth("user", "pass", "testserver"), + []string{"Invalid", "Invalid:"}, + "LOGIN", + []string{"", "user", "pass"}, + []bool{false, false}, + }, + { + LoginAuth("user", "pass", "testserver"), + []string{"Invalid", "Invalid:", "Too many"}, + "LOGIN", + []string{"", "user", "pass", ""}, + []bool{false, false, true}, }, { CRAMMD5Auth("user", "pass"), From 9d70283af976b6db4d0a5734d0aad1d58b8f45e7 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 2 Oct 2024 13:09:55 +0200 Subject: [PATCH 096/181] Reset response step in AUTH LOGIN initialization The addition of `a.respStep = 0` resets the response step counter at the beginning of the AUTH LOGIN process. This ensures that the state starts correctly and avoids potential issues related to residual values from previous authentications. --- smtp/auth_login.go | 1 + 1 file changed, 1 insertion(+) diff --git a/smtp/auth_login.go b/smtp/auth_login.go index e25b3e6..715861c 100644 --- a/smtp/auth_login.go +++ b/smtp/auth_login.go @@ -57,6 +57,7 @@ func (a *loginAuth) Start(server *ServerInfo) (string, []byte, error) { if server.Name != a.host { return "", nil, errors.New("wrong host name") } + a.respStep = 0 return "LOGIN", nil, nil } From 761e20504969e3f0e766333853d1361b0abd1387 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 2 Oct 2024 13:10:10 +0200 Subject: [PATCH 097/181] Fix client connection test error handling Changed variable assignment in the test to fix error handling. This ensures the error is properly caught and reported during the client connection process. --- client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client_test.go b/client_test.go index 35fca38..53174f2 100644 --- a/client_test.go +++ b/client_test.go @@ -620,7 +620,7 @@ func TestClient_DialWithContext(t *testing.T) { t.Skipf("failed to create test client: %s. Skipping tests", err) } ctx := context.Background() - if err := c.DialWithContext(ctx); err != nil { + if err = c.DialWithContext(ctx); err != nil { t.Errorf("failed to dial with context: %s", err) return } From e037df43a7aeb062121f3240af83048b5e93c5d2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:45:34 +0000 Subject: [PATCH 098/181] Bump codecov/codecov-action from 4.5.0 to 4.6.0 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.5.0 to 4.6.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/e28ff129e5465c2c0dcc6f003fc735cb6ae0c673...b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/codecov.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 9ab2f52..0ed5ea2 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -58,6 +58,6 @@ jobs: go test -v -race --coverprofile=coverage.coverprofile --covermode=atomic ./... - name: Upload coverage to Codecov if: success() && matrix.go == '1.23' && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 + uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0 with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos From 3f3b21348f22542d4a75636a6ee86ab84c9a557b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:45:37 +0000 Subject: [PATCH 099/181] Bump golang/govulncheck-action from 1.0.3 to 1.0.4 Bumps [golang/govulncheck-action](https://github.com/golang/govulncheck-action) from 1.0.3 to 1.0.4. - [Release notes](https://github.com/golang/govulncheck-action/releases) - [Commits](https://github.com/golang/govulncheck-action/compare/dd0578b371c987f96d1185abb54344b44352bd58...b625fbe08f3bccbe446d94fbf87fcc875a4f50ee) --- updated-dependencies: - dependency-name: golang/govulncheck-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/govulncheck.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml index aecfaaa..9d5cdfb 100644 --- a/.github/workflows/govulncheck.yml +++ b/.github/workflows/govulncheck.yml @@ -18,4 +18,4 @@ jobs: with: egress-policy: audit - name: Run govulncheck - uses: golang/govulncheck-action@dd0578b371c987f96d1185abb54344b44352bd58 # v1.0.3 \ No newline at end of file + uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4 \ No newline at end of file From cbba4d83d1b8432ca3a3557bbe8a424bf042d2ad Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 2 Oct 2024 15:51:56 +0200 Subject: [PATCH 100/181] Add SCRAM authentication to CI workflows This commit introduces SCRAM authentication configurations to both `codecov.yml` and `sonarqube.yml` GitHub Action workflow files. The changes include new environment variables for SCRAM host, user, and password to enhance the security and flexibility of the CI processes. --- .github/workflows/codecov.yml | 4 ++++ .github/workflows/sonarqube.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 9ab2f52..a8a5a8f 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -27,6 +27,10 @@ env: TEST_SMTPAUTH_USER: ${{ secrets.TEST_USER }} TEST_SMTPAUTH_PASS: ${{ secrets.TEST_PASS }} TEST_SMTPAUTH_TYPE: "LOGIN" + TEST_ONLINE_SCRAM: "1" + TEST_HOST_SCRAM: ${{ secrets.TEST_HOST_SCRAM }} + TEST_USER_SCRAM: ${{ secrets.TEST_USER_SCRAM }} + TEST_PASS_SCRAM: ${{ secrets.TEST_USER_SCRAM }} permissions: contents: read diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 0f54b09..16ab41a 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -21,6 +21,10 @@ env: TEST_SMTPAUTH_USER: ${{ secrets.TEST_USER }} TEST_SMTPAUTH_PASS: ${{ secrets.TEST_PASS }} TEST_SMTPAUTH_TYPE: "LOGIN" + TEST_ONLINE_SCRAM: "1" + TEST_HOST_SCRAM: ${{ secrets.TEST_HOST_SCRAM }} + TEST_USER_SCRAM: ${{ secrets.TEST_USER_SCRAM }} + TEST_PASS_SCRAM: ${{ secrets.TEST_USER_SCRAM }} jobs: build: name: Build From 0c3bf239f1a0269d26be72bf3d7cafe5434d8488 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 2 Oct 2024 15:54:34 +0200 Subject: [PATCH 101/181] Add support channel information for Gophers Slack Updated the README file to include our new support and general discussion channel on the Gophers Slack. Users can now find us on both Discord and Slack for any queries or discussions related to go-mail. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6a34055..8c200c7 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,8 @@ We guarantee that go-mail will always support the last four releases of Go. With the user a timeframe of two years to update to the next or even the latest version of Go. ## Support -We have a support and general discussion channel on Discord. Find us at: [#go-mail](https://discord.gg/dbfQyC4s) +We have a support and general discussion channel on Discord. Find us at: [#go-mail](https://discord.gg/dbfQyC4s) alternatively find us +on the [Gophers Slack](https://gophers.slack.com) in #go-mail ## Middleware The goal of go-mail is to keep it free from 3rd party dependencies and only focus on things a mail library should From c8a8e9772a1ebadf24ddacb306d5c5310d58f9c2 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 2 Oct 2024 16:25:41 +0200 Subject: [PATCH 102/181] Update test recipient email in client tests Changed the test email address from go-mail@mytrashmailer.com to couttifaddebro-1473@yopmail.com. This new address is expected to be used for sending test mails. --- client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client_test.go b/client_test.go index 53174f2..b8d77ed 100644 --- a/client_test.go +++ b/client_test.go @@ -27,7 +27,7 @@ const ( // DefaultHost is used as default hostname for the Client DefaultHost = "localhost" // TestRcpt is a trash mail address to send test mails to - TestRcpt = "go-mail@mytrashmailer.com" + TestRcpt = "couttifaddebro-1473@yopmail.com" // TestServerProto is the protocol used for the simple SMTP test server TestServerProto = "tcp" // TestServerAddr is the address the simple SMTP test server listens on From a41639ec070ee9153f668abc9aa3924690afaa88 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 2 Oct 2024 16:50:45 +0200 Subject: [PATCH 103/181] Fix secret reference and improve test command options Corrected the reference for `TEST_PASS_SCRAM` in both workflows. Simplified the Go test command in `codecov.yml` and added the `shuffle=on` option for better test randomness in `sonarqube.yml`. --- .github/workflows/codecov.yml | 4 ++-- .github/workflows/sonarqube.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 5b41af2..0099f12 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -30,7 +30,7 @@ env: TEST_ONLINE_SCRAM: "1" TEST_HOST_SCRAM: ${{ secrets.TEST_HOST_SCRAM }} TEST_USER_SCRAM: ${{ secrets.TEST_USER_SCRAM }} - TEST_PASS_SCRAM: ${{ secrets.TEST_USER_SCRAM }} + TEST_PASS_SCRAM: ${{ secrets.TEST_PASS_SCRAM }} permissions: contents: read @@ -59,7 +59,7 @@ jobs: sudo apt-get -y install sendmail; which sendmail - name: Run Tests run: | - go test -v -race --coverprofile=coverage.coverprofile --covermode=atomic ./... + go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./... - name: Upload coverage to Codecov if: success() && matrix.go == '1.23' && matrix.os == 'ubuntu-latest' uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0 diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 16ab41a..3f6dc11 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -24,7 +24,7 @@ env: TEST_ONLINE_SCRAM: "1" TEST_HOST_SCRAM: ${{ secrets.TEST_HOST_SCRAM }} TEST_USER_SCRAM: ${{ secrets.TEST_USER_SCRAM }} - TEST_PASS_SCRAM: ${{ secrets.TEST_USER_SCRAM }} + TEST_PASS_SCRAM: ${{ secrets.TEST_PASS_SCRAM }} jobs: build: name: Build @@ -46,7 +46,7 @@ jobs: - name: Run unit Tests run: | - go test -v -race --coverprofile=./cov.out ./... + go test -shuffle=on -race --coverprofile=./cov.out ./... - uses: sonarsource/sonarqube-scan-action@884b79409bbd464b2a59edc326a4b77dc56b2195 # master env: From 580981b15881f08afa832021364e35b9e4c8aa3b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 2 Oct 2024 18:02:23 +0200 Subject: [PATCH 104/181] Refactor error handling in SMTP authentication Centralized error definitions in `smtp/auth.go` and updated references in `auth_login.go` and `auth_plain.go`. This improves code maintainability and error consistency across the package. --- smtp/auth.go | 13 +++++++++++++ smtp/auth_login.go | 8 ++------ smtp/auth_plain.go | 10 +++------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/smtp/auth.go b/smtp/auth.go index 30948e1..a62e74d 100644 --- a/smtp/auth.go +++ b/smtp/auth.go @@ -13,6 +13,19 @@ package smtp +import "errors" + +var ( + // ErrUnencrypted is an error indicating that the connection is not encrypted. + ErrUnencrypted = errors.New("unencrypted connection") + // ErrUnexpectedServerChallange is an error indicating that the server issued an unexpected challenge. + ErrUnexpectedServerChallange = errors.New("unexpected server challenge") + // ErrUnexpectedServerResponse is an error indicating that the server issued an unexpected response. + ErrUnexpectedServerResponse = errors.New("unexpected server response") + // ErrWrongHostname is an error indicating that the provided hostname does not match the expected value. + ErrWrongHostname = errors.New("wrong host name") +) + // Auth is implemented by an SMTP authentication mechanism. type Auth interface { // Start begins an authentication with a server. diff --git a/smtp/auth_login.go b/smtp/auth_login.go index 715861c..847ad62 100644 --- a/smtp/auth_login.go +++ b/smtp/auth_login.go @@ -5,13 +5,9 @@ package smtp import ( - "errors" "fmt" ) -// ErrUnencrypted is an error indicating that the connection is not encrypted. -var ErrUnencrypted = errors.New("unencrypted connection") - // loginAuth is the type that satisfies the Auth interface for the "SMTP LOGIN" auth type loginAuth struct { username, password string @@ -55,7 +51,7 @@ func (a *loginAuth) Start(server *ServerInfo) (string, []byte, error) { return "", nil, ErrUnencrypted } if server.Name != a.host { - return "", nil, errors.New("wrong host name") + return "", nil, ErrWrongHostname } a.respStep = 0 return "LOGIN", nil, nil @@ -73,7 +69,7 @@ func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { a.respStep++ return []byte(a.password), nil default: - return nil, fmt.Errorf("unexpected server response: %s", string(fromServer)) + return nil, fmt.Errorf("%w: %s", ErrUnexpectedServerResponse, string(fromServer)) } } return nil, nil diff --git a/smtp/auth_plain.go b/smtp/auth_plain.go index 2430c96..e6e0ad9 100644 --- a/smtp/auth_plain.go +++ b/smtp/auth_plain.go @@ -13,10 +13,6 @@ package smtp -import ( - "errors" -) - // plainAuth is the type that satisfies the Auth interface for the "SMTP PLAIN" auth type plainAuth struct { identity, username, password string @@ -42,10 +38,10 @@ func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) { // That might just be the attacker saying // "it's ok, you can trust me with your password." if !server.TLS && !isLocalhost(server.Name) { - return "", nil, errors.New("unencrypted connection") + return "", nil, ErrUnencrypted } if server.Name != a.host { - return "", nil, errors.New("wrong host name") + return "", nil, ErrWrongHostname } resp := []byte(a.identity + "\x00" + a.username + "\x00" + a.password) return "PLAIN", resp, nil @@ -54,7 +50,7 @@ func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) { func (a *plainAuth) Next(_ []byte, more bool) ([]byte, error) { if more { // We've already sent everything. - return nil, errors.New("unexpected server challenge") + return nil, ErrUnexpectedServerChallange } return nil, nil } From e4dd62475a2acacd1a431cd1feff866231b225ee Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 2 Oct 2024 18:02:34 +0200 Subject: [PATCH 105/181] Improve error handling in SCRAM-SHA-X-PLUS authentication Refactor error return to include more specific information and add a check for TLS connection state in SCRAM-SHA-X-PLUS authentication flow. This ensures clearer error messages and verifies essential prerequisites for secure authentication. --- smtp/auth_scram.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/smtp/auth_scram.go b/smtp/auth_scram.go index c70b210..a21aef5 100644 --- a/smtp/auth_scram.go +++ b/smtp/auth_scram.go @@ -112,7 +112,7 @@ func (a *scramAuth) Next(fromServer []byte, more bool) ([]byte, error) { return resp, nil default: a.reset() - return nil, errors.New("unexpected server response") + return nil, fmt.Errorf("%w: %s", ErrUnexpectedServerResponse, string(fromServer)) } } return nil, nil @@ -147,6 +147,9 @@ func (a *scramAuth) initialClientMessage() ([]byte, error) { // SCRAM-SHA-X-PLUS auth requires channel binding if a.isPlus { + if a.tlsConnState == nil { + return nil, errors.New("tls connection state is required for SCRAM-SHA-X-PLUS") + } bindType := "tls-unique" connState := a.tlsConnState bindData := connState.TLSUnique From a8e89a125829f3643fdbe45140437856e15b206a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 2 Oct 2024 18:02:46 +0200 Subject: [PATCH 106/181] Add support for SCRAM-SHA authentication mechanisms Introduced new test cases for SCRAM-SHA-1, SCRAM-SHA-256, and their PLUS variants in `smtp_test.go`. Updated the authTest structure to include a `hasNonce` flag and implemented logic to handle nonce validation and success message processing. --- smtp/smtp_test.go | 67 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index d5b02a7..1848b5c 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -38,6 +38,7 @@ type authTest struct { name string responses []string sf []bool + hasNonce bool } var authTests = []authTest{ @@ -47,6 +48,7 @@ var authTests = []authTest{ "PLAIN", []string{"\x00user\x00pass"}, []bool{false, false}, + false, }, { PlainAuth("foo", "bar", "baz", "testserver"), @@ -54,6 +56,15 @@ var authTests = []authTest{ "PLAIN", []string{"foo\x00bar\x00baz"}, []bool{false, false}, + false, + }, + { + PlainAuth("foo", "bar", "baz", "testserver"), + []string{"foo"}, + "PLAIN", + []string{"foo\x00bar\x00baz", ""}, + []bool{true}, + false, }, { LoginAuth("user", "pass", "testserver"), @@ -61,6 +72,7 @@ var authTests = []authTest{ "LOGIN", []string{"", "user", "pass"}, []bool{false, false}, + false, }, { LoginAuth("user", "pass", "testserver"), @@ -68,6 +80,7 @@ var authTests = []authTest{ "LOGIN", []string{"", "user", "pass"}, []bool{false, false}, + false, }, { LoginAuth("user", "pass", "testserver"), @@ -75,6 +88,7 @@ var authTests = []authTest{ "LOGIN", []string{"", "user", "pass"}, []bool{false, false}, + false, }, { LoginAuth("user", "pass", "testserver"), @@ -82,6 +96,7 @@ var authTests = []authTest{ "LOGIN", []string{"", "user", "pass", ""}, []bool{false, false, true}, + false, }, { CRAMMD5Auth("user", "pass"), @@ -89,6 +104,7 @@ var authTests = []authTest{ "CRAM-MD5", []string{"", "user 287eb355114cf5c471c26a875f1ca4ae"}, []bool{false, false}, + false, }, { XOAuth2Auth("username", "token"), @@ -96,6 +112,47 @@ var authTests = []authTest{ "XOAUTH2", []string{"user=username\x01auth=Bearer token\x01\x01", ""}, []bool{false}, + false, + }, + { + ScramSHA1Auth("username", "password"), + []string{"", "r=foo"}, + "SCRAM-SHA-1", + []string{"", "n,,n=username,r=", ""}, + []bool{false, true}, + true, + }, + { + ScramSHA1Auth("username", "password"), + []string{"", "v=foo"}, + "SCRAM-SHA-1", + []string{"", "n,,n=username,r=", ""}, + []bool{false, true}, + true, + }, + { + ScramSHA256Auth("username", "password"), + []string{""}, + "SCRAM-SHA-256", + []string{"", "n,,n=username,r=", ""}, + []bool{false}, + true, + }, + { + ScramSHA1PlusAuth("username", "password", nil), + []string{""}, + "SCRAM-SHA-1-PLUS", + []string{"", "", ""}, + []bool{true}, + true, + }, + { + ScramSHA256PlusAuth("username", "password", nil), + []string{""}, + "SCRAM-SHA-256-PLUS", + []string{"", "", ""}, + []bool{true}, + true, }, } @@ -121,10 +178,20 @@ testLoop: t.Errorf("#%d error: %s", i, err) continue testLoop } + if test.hasNonce { + if !bytes.HasPrefix(resp, expected) { + t.Errorf("#%d got response: %s, expected response to start with: %s", i, resp, expected) + } + continue testLoop + } if !bytes.Equal(resp, expected) { t.Errorf("#%d got %s, expected %s", i, resp, expected) continue testLoop } + _, err = test.auth.Next([]byte("2.7.0 Authentication successful"), false) + if err != nil { + t.Errorf("#%d success message error: %s", i, err) + } } } } From 03062c5183b71f247820ab8e6b0edcb1a823e47b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 3 Oct 2024 12:32:06 +0200 Subject: [PATCH 107/181] Add SCRAM-SHA authentication tests for SMTP Introduce new unit tests to verify SCRAM-SHA-1 and SCRAM-SHA-256 authentication for the SMTP client. These tests cover both successful and failing authentication cases, and include a mock SMTP server to facilitate testing. --- smtp/smtp_test.go | 288 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 288 insertions(+) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 1848b5c..dc8dbdb 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -18,6 +18,7 @@ import ( "bytes" "crypto/tls" "crypto/x509" + "encoding/base64" "flag" "fmt" "io" @@ -368,6 +369,106 @@ func TestXOAuth2Error(t *testing.T) { } } +func TestAuthSCRAMSHA1_OK(t *testing.T) { + hostname := "127.0.0.1" + port := "2585" + + go func() { + startSMTPServer(false, hostname, port) + }() + time.Sleep(time.Millisecond * 500) + + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%s", hostname, port)) + if err != nil { + t.Errorf("failed to dial server: %v", err) + } + client, err := NewClient(conn, hostname) + if err != nil { + t.Errorf("failed to create client: %v", err) + } + if err = client.Hello(hostname); err != nil { + t.Errorf("failed to send HELO: %v", err) + } + if err = client.Auth(ScramSHA1Auth("username", "password")); err != nil { + t.Errorf("failed to authenticate: %v", err) + } +} + +func TestAuthSCRAMSHA256_OK(t *testing.T) { + hostname := "127.0.0.1" + port := "2586" + + go func() { + startSMTPServer(false, hostname, port) + }() + time.Sleep(time.Millisecond * 500) + + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%s", hostname, port)) + if err != nil { + t.Errorf("failed to dial server: %v", err) + } + client, err := NewClient(conn, hostname) + if err != nil { + t.Errorf("failed to create client: %v", err) + } + if err = client.Hello(hostname); err != nil { + t.Errorf("failed to send HELO: %v", err) + } + if err = client.Auth(ScramSHA256Auth("username", "password")); err != nil { + t.Errorf("failed to authenticate: %v", err) + } +} + +func TestAuthSCRAMSHA1_fail(t *testing.T) { + hostname := "127.0.0.1" + port := "2587" + + go func() { + startSMTPServer(true, hostname, port) + }() + time.Sleep(time.Millisecond * 500) + + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%s", hostname, port)) + if err != nil { + t.Errorf("failed to dial server: %v", err) + } + client, err := NewClient(conn, hostname) + if err != nil { + t.Errorf("failed to create client: %v", err) + } + if err = client.Hello(hostname); err != nil { + t.Errorf("failed to send HELO: %v", err) + } + if err = client.Auth(ScramSHA1Auth("username", "password")); err == nil { + t.Errorf("expected auth error, got nil") + } +} + +func TestAuthSCRAMSHA256_fail(t *testing.T) { + hostname := "127.0.0.1" + port := "2588" + + go func() { + startSMTPServer(true, hostname, port) + }() + time.Sleep(time.Millisecond * 500) + + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%s", hostname, port)) + if err != nil { + t.Errorf("failed to dial server: %v", err) + } + client, err := NewClient(conn, hostname) + if err != nil { + t.Errorf("failed to create client: %v", err) + } + if err = client.Hello(hostname); err != nil { + t.Errorf("failed to send HELO: %v", err) + } + if err = client.Auth(ScramSHA256Auth("username", "password")); err == nil { + t.Errorf("expected auth error, got nil") + } +} + // Issue 17794: don't send a trailing space on AUTH command when there's no password. func TestClientAuthTrimSpace(t *testing.T) { server := "220 hello world\r\n" + @@ -1541,3 +1642,190 @@ func SkipFlaky(t testing.TB, issue int) { t.Skipf("skipping known flaky test without the -flaky flag; see golang.org/issue/%d", issue) } } + +// 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 +// fields are present. We have actual real authentication tests for all SCRAM modes in the +// go-mail client_test.go +type testSCRAMSMTPServer struct { + authMechanism string + nonce string + hostname string + port string + shouldFail bool +} + +func (s *testSCRAMSMTPServer) handleConnection(conn net.Conn) { + defer func() { + _ = conn.Close() + }() + + reader := bufio.NewReader(conn) + writer := bufio.NewWriter(conn) + writeLine := func(data string) error { + _, err := writer.WriteString(data + "\r\n") + if err != nil { + return fmt.Errorf("unable to write line: %w", err) + } + return writer.Flush() + } + writeOK := func() { + _ = writeLine("250 2.0.0 OK") + } + + if err := writeLine("220 go-mail test server ready ESMTP"); err != nil { + return + } + + data, err := reader.ReadString('\n') + if err != nil { + return + } + data = strings.TrimSpace(data) + if strings.HasPrefix(data, "EHLO") { + _ = writeLine(fmt.Sprintf("250-%s", s.hostname)) + _ = writeLine("250-AUTH SCRAM-SHA-1 SCRAM-SHA-256") + writeOK() + } else { + _ = writeLine("500 Invalid command") + return + } + + for { + data, err = reader.ReadString('\n') + if err != nil { + fmt.Printf("failed to read data: %v", err) + } + data = strings.TrimSpace(data) + if strings.HasPrefix(data, "AUTH") { + parts := strings.Split(data, " ") + if len(parts) < 2 { + _ = writeLine("500 Syntax error") + return + } + + authMechanism := parts[1] + if authMechanism != "SCRAM-SHA-1" && authMechanism != "SCRAM-SHA-256" { + _ = writeLine("504 Unrecognized authentication mechanism") + return + } + s.authMechanism = authMechanism + _ = writeLine("334 ") + s.handleSCRAMAuth(conn) + return + } else { + _ = writeLine("500 Invalid command") + } + } +} + +func (s *testSCRAMSMTPServer) handleSCRAMAuth(conn net.Conn) { + reader := bufio.NewReader(conn) + writer := bufio.NewWriter(conn) + writeLine := func(data string) error { + _, err := writer.WriteString(data + "\r\n") + if err != nil { + return fmt.Errorf("unable to write line: %w", err) + } + return writer.Flush() + } + + data, err := reader.ReadString('\n') + clientMessage := strings.TrimSpace(data) + decodedMessage, err := base64.StdEncoding.DecodeString(clientMessage) + if err != nil { + _ = writeLine("535 Authentication failed") + return + } + splits := strings.Split(string(decodedMessage), ",") + if len(splits) != 4 { + _ = writeLine("535 Authentication failed - expected 4 parts") + return + } + if splits[0] != "n" { + _ = writeLine("535 Authentication failed - expected n to be in the first part") + return + } + if splits[2] != "n=username" { + _ = writeLine("535 Authentication failed - expected n=username to be in the third part") + return + } + if !strings.HasPrefix(splits[3], "r=") { + _ = writeLine("535 Authentication failed - expected r= to be in the fourth part") + return + } + clientNonce := s.extractNonce(string(decodedMessage)) + if clientNonce == "" { + _ = writeLine("535 Authentication failed") + return + } + + s.nonce = clientNonce + "server_nonce" + serverFirstMessage := fmt.Sprintf("r=%s,s=%s,i=0", s.nonce, "salt") + _ = writeLine(fmt.Sprintf("334 %s", base64.StdEncoding.EncodeToString([]byte(serverFirstMessage)))) + + data, err = reader.ReadString('\n') + clientFinalMessage := strings.TrimSpace(data) + decodedFinalMessage, err := base64.StdEncoding.DecodeString(clientFinalMessage) + if err != nil { + _ = writeLine("535 Authentication failed") + return + } + splits = strings.Split(string(decodedFinalMessage), ",") + if splits[0] != "c=biws" { + _ = writeLine("535 Authentication failed - expected c=biws to be in the first part") + return + } + if !strings.HasPrefix(splits[1], "r=") { + _ = writeLine("535 Authentication failed - expected r to be in the second part") + return + } + if !strings.Contains(splits[1], "server_nonce") { + _ = writeLine("535 Authentication failed - expected server_nonce to be in the second part") + return + } + if !strings.HasPrefix(splits[2], "p=") { + _ = writeLine("535 Authentication failed - expected p to be in the third part") + return + } + + if s.shouldFail { + _ = writeLine("535 Authentication failed") + return + } + _ = writeLine("235 Authentication successful") + return +} + +func (s *testSCRAMSMTPServer) extractNonce(message string) string { + parts := strings.Split(message, ",") + for _, part := range parts { + if strings.HasPrefix(part, "r=") { + return part[2:] + } + } + return "" +} + +func startSMTPServer(shouldFail bool, hostname, port string) { + server := &testSCRAMSMTPServer{ + hostname: hostname, + port: port, + shouldFail: shouldFail, + } + listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", hostname, port)) + if err != nil { + fmt.Printf("Failed to start SMTP server: %v", err) + } + defer func() { + _ = listener.Close() + }() + for { + conn, err := listener.Accept() + if err != nil { + fmt.Printf("Failed to accept connection: %v", err) + continue + } + go server.handleConnection(conn) + } +} From 4c8c0d855e206ea3135960f8fc403e093763205d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 3 Oct 2024 12:38:39 +0200 Subject: [PATCH 108/181] Handle read errors in SMTP authentication flow Add checks to handle errors when reading client messages. This ensures that an appropriate error message is sent back to the client if reading fails, improving the robustness of the SMTP authentication process. --- smtp/smtp_test.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index dc8dbdb..0d47760 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -1731,8 +1731,12 @@ func (s *testSCRAMSMTPServer) handleSCRAMAuth(conn net.Conn) { } data, err := reader.ReadString('\n') - clientMessage := strings.TrimSpace(data) - decodedMessage, err := base64.StdEncoding.DecodeString(clientMessage) + if err != nil { + _ = writeLine("535 Authentication failed") + return + } + data = strings.TrimSpace(data) + decodedMessage, err := base64.StdEncoding.DecodeString(data) if err != nil { _ = writeLine("535 Authentication failed") return @@ -1765,8 +1769,12 @@ func (s *testSCRAMSMTPServer) handleSCRAMAuth(conn net.Conn) { _ = writeLine(fmt.Sprintf("334 %s", base64.StdEncoding.EncodeToString([]byte(serverFirstMessage)))) data, err = reader.ReadString('\n') - clientFinalMessage := strings.TrimSpace(data) - decodedFinalMessage, err := base64.StdEncoding.DecodeString(clientFinalMessage) + if err != nil { + _ = writeLine("535 Authentication failed") + return + } + data = strings.TrimSpace(data) + decodedFinalMessage, err := base64.StdEncoding.DecodeString(data) if err != nil { _ = writeLine("535 Authentication failed") return @@ -1794,7 +1802,6 @@ func (s *testSCRAMSMTPServer) handleSCRAMAuth(conn net.Conn) { return } _ = writeLine("235 Authentication successful") - return } func (s *testSCRAMSMTPServer) extractNonce(message string) string { From 94ed5646c5fa5c8976b85346aa7f3077e5fa13ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Oct 2024 13:39:45 +0000 Subject: [PATCH 109/181] Bump golangci/golangci-lint-action from 6.1.0 to 6.1.1 Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6.1.0 to 6.1.1. - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/aaa42aa0628b4ae2578232a66b541047968fac86...971e284b6050e8a5849b72094c50ab08da042db8) --- updated-dependencies: - dependency-name: golangci/golangci-lint-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/golangci-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 5c08b40..616964e 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -29,7 +29,7 @@ jobs: go-version: '1.23' - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - name: golangci-lint - uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0 + uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version version: latest From 8f596ffae712f7b27c07ab236bf3134dd9b9af16 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 3 Oct 2024 16:00:58 +0200 Subject: [PATCH 110/181] Add offline tests workflow and clean up SonarQube config Introduce a new offline tests workflow to validate Go code across multiple OS and Go versions. This commit also removes unused environment variables and updates the Go version syntax in the SonarQube workflow. --- .github/workflows/offline-tests.yml | 47 +++++++++++++++++++++++++++++ .github/workflows/sonarqube.yml | 13 +------- 2 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/offline-tests.yml diff --git a/.github/workflows/offline-tests.yml b/.github/workflows/offline-tests.yml new file mode 100644 index 0000000..7ca3ea2 --- /dev/null +++ b/.github/workflows/offline-tests.yml @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: 2022 Winni Neessen +# +# SPDX-License-Identifier: CC0-1.0 + +name: Offline tests workflow +on: + push: + branches: + - main + paths: + - '**.go' + - 'go.*' + - '.github/**' + - 'codecov.yml' + pull_request: + branches: + - main + paths: + - '**.go' + - 'go.*' + - '.github/**' + - 'codecov.yml' +permissions: + contents: read + +jobs: + run: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + go: ['1.19', '1.20', '1.21', '1.22', '1.23'] + steps: + - name: Harden Runner + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + with: + egress-policy: audit + + - name: Checkout Code + uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master + - name: Setup go + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + with: + go-version: ${{ matrix.go }} + - name: Run Tests + run: | + go test -race -shuffle=on ./... \ No newline at end of file diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 3f6dc11..dd91da2 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -14,17 +14,6 @@ on: pull_request: branches: - main # or the name of your main branch -env: - TEST_HOST: ${{ secrets.TEST_HOST }} - TEST_FROM: ${{ secrets.TEST_USER }} - TEST_ALLOW_SEND: "1" - TEST_SMTPAUTH_USER: ${{ secrets.TEST_USER }} - TEST_SMTPAUTH_PASS: ${{ secrets.TEST_PASS }} - TEST_SMTPAUTH_TYPE: "LOGIN" - TEST_ONLINE_SCRAM: "1" - TEST_HOST_SCRAM: ${{ secrets.TEST_HOST_SCRAM }} - TEST_USER_SCRAM: ${{ secrets.TEST_USER_SCRAM }} - TEST_PASS_SCRAM: ${{ secrets.TEST_PASS_SCRAM }} jobs: build: name: Build @@ -42,7 +31,7 @@ jobs: - name: Setup Go uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: - go-version: '1.23.x' + go-version: '1.23' - name: Run unit Tests run: | From 6f10892d0b81ceaab4abb7a77b7a6b78cd80f289 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 3 Oct 2024 16:01:58 +0200 Subject: [PATCH 111/181] Reduce Go versions in Codecov workflow to only 1.23 This commit updates the Codecov GitHub Actions workflow to run only on Go version 1.23, removing support for 1.19 and 1.20. Simplifying to a single Go version aims to streamline the testing process and reduce potential compatibility issues. --- .github/workflows/codecov.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 0099f12..60679ee 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -40,7 +40,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - go: ['1.19', '1.20', '1.23'] + go: ['1.23'] steps: - name: Harden Runner uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 From 19dcba620aa6d47a1903d2a469a77470ea538e2b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 3 Oct 2024 16:15:07 +0200 Subject: [PATCH 112/181] Remove redundant steps from Cirrus CI configuration Eliminated unnecessary environment variables and pkg update step to streamline the CI process. Simplified the test script by removing verbose flag from the go test command. --- .cirrus.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index f240ff5..8ca7373 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -14,12 +14,10 @@ freebsd_task: image_family: freebsd-14-0 env: - TEST_ALLOW_SEND: 0 TEST_SKIP_SENDMAIL: 1 pkginstall_script: - - pkg update -f - pkg install -y go test_script: - - go test -v -race -cover -shuffle=on ./... \ No newline at end of file + - go test -race -cover -shuffle=on ./... \ No newline at end of file From fe36f3b294908690038bd2ca0e6dd544a1f3440e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Oct 2024 13:17:57 +0000 Subject: [PATCH 113/181] Bump github/codeql-action from 3.26.10 to 3.26.11 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.10 to 3.26.11. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/e2b3eafc8d227b0241d48be5f425d47c2d750a13...6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ea73834..d5668f3 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -54,7 +54,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10 + uses: github/codeql-action/init@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 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@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10 + uses: github/codeql-action/autobuild@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 # ℹī¸ 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@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10 + uses: github/codeql-action/analyze@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 05e5e69..b2e161f 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -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@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10 + uses: github/codeql-action/upload-sarif@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 with: sarif_file: results.sarif From d931050a6f8f4a62077e7e9fe6f1757f7c235c9c Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Oct 2024 15:28:22 +0200 Subject: [PATCH 114/181] Update GitHub Actions paths for Go and workflow files This commit refines the paths in GitHub Actions workflows to more precisely track changes in Go-related files and specific workflow files. General `.github/**` paths have been replaced with explicit references to relevant workflow files within `.github/workflows`. --- .github/workflows/codecov.yml | 4 ++-- .github/workflows/offline-tests.yml | 6 ++---- .github/workflows/sonarqube.yml | 12 ++++++++++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 60679ee..ed971d1 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -10,7 +10,7 @@ on: paths: - '**.go' - 'go.*' - - '.github/**' + - '.github/workflows/codecov.yml' - 'codecov.yml' pull_request: branches: @@ -18,7 +18,7 @@ on: paths: - '**.go' - 'go.*' - - '.github/**' + - '.github/workflows/codecov.yml' - 'codecov.yml' env: TEST_HOST: ${{ secrets.TEST_HOST }} diff --git a/.github/workflows/offline-tests.yml b/.github/workflows/offline-tests.yml index 7ca3ea2..2660646 100644 --- a/.github/workflows/offline-tests.yml +++ b/.github/workflows/offline-tests.yml @@ -10,16 +10,14 @@ on: paths: - '**.go' - 'go.*' - - '.github/**' - - 'codecov.yml' + - '.github/workflows/offline-tests.yml' pull_request: branches: - main paths: - '**.go' - 'go.*' - - '.github/**' - - 'codecov.yml' + - '.github/workflows/offline-tests.yml' permissions: contents: read diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index dd91da2..9b2b899 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -10,10 +10,18 @@ permissions: on: push: branches: - - main # or the name of your main branch + - main + paths: + - '**.go' + - 'go.*' + - '.github/workflows/sonarqube.yml' pull_request: branches: - - main # or the name of your main branch + - main + paths: + - '**.go' + - 'go.*' + - '.github/workflows/sonarqube.yml' jobs: build: name: Build From 711ce2ac653f5385589f43e774dfb4181a25c774 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Oct 2024 18:31:58 +0200 Subject: [PATCH 115/181] Add support for SCRAM-SHA-1-PLUS and SCRAM-SHA-256-PLUS Extended SMTP tests to include SCRAM-SHA-1-PLUS and SCRAM-SHA-256-PLUS authentication mechanisms. Adjusted the `startSMTPServer` function to accept a hashing function and modified the server logic to handle TLS channel binding. --- smtp/smtp_test.go | 234 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 218 insertions(+), 16 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 0d47760..451a349 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -16,11 +16,15 @@ package smtp import ( "bufio" "bytes" + "crypto/hmac" + "crypto/sha1" + "crypto/sha256" "crypto/tls" "crypto/x509" "encoding/base64" "flag" "fmt" + "hash" "io" "net" "net/textproto" @@ -30,6 +34,8 @@ import ( "testing" "time" + "golang.org/x/crypto/pbkdf2" + "github.com/wneessen/go-mail/log" ) @@ -374,7 +380,7 @@ func TestAuthSCRAMSHA1_OK(t *testing.T) { port := "2585" go func() { - startSMTPServer(false, hostname, port) + startSMTPServer(false, hostname, port, sha1.New) }() time.Sleep(time.Millisecond * 500) @@ -399,7 +405,7 @@ func TestAuthSCRAMSHA256_OK(t *testing.T) { port := "2586" go func() { - startSMTPServer(false, hostname, port) + startSMTPServer(false, hostname, port, sha256.New) }() time.Sleep(time.Millisecond * 500) @@ -419,12 +425,80 @@ func TestAuthSCRAMSHA256_OK(t *testing.T) { } } +func TestAuthSCRAMSHA1PLUS_OK(t *testing.T) { + hostname := "127.0.0.1" + port := "2590" + + go func() { + startSMTPServer(true, hostname, port, sha1.New) + }() + time.Sleep(time.Millisecond * 500) + + cert, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + fmt.Printf("error creating TLS cert: %s", err) + return + } + tlsConfig := tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true} + + conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%s", hostname, port), &tlsConfig) + if err != nil { + t.Errorf("failed to dial server: %v", err) + } + client, err := NewClient(conn, hostname) + if err != nil { + t.Errorf("failed to create client: %v", err) + } + if err = client.Hello(hostname); err != nil { + t.Errorf("failed to send HELO: %v", err) + } + + tlsConnState := conn.ConnectionState() + if err = client.Auth(ScramSHA1PlusAuth("username", "password", &tlsConnState)); err != nil { + t.Errorf("failed to authenticate: %v", err) + } +} + +func TestAuthSCRAMSHA256PLUS_OK(t *testing.T) { + hostname := "127.0.0.1" + port := "2591" + + go func() { + startSMTPServer(true, hostname, port, sha256.New) + }() + time.Sleep(time.Millisecond * 500) + + cert, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + fmt.Printf("error creating TLS cert: %s", err) + return + } + tlsConfig := tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true} + + conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%s", hostname, port), &tlsConfig) + if err != nil { + t.Errorf("failed to dial server: %v", err) + } + client, err := NewClient(conn, hostname) + if err != nil { + t.Errorf("failed to create client: %v", err) + } + if err = client.Hello(hostname); err != nil { + t.Errorf("failed to send HELO: %v", err) + } + + tlsConnState := conn.ConnectionState() + if err = client.Auth(ScramSHA256PlusAuth("username", "password", &tlsConnState)); err != nil { + t.Errorf("failed to authenticate: %v", err) + } +} + func TestAuthSCRAMSHA1_fail(t *testing.T) { hostname := "127.0.0.1" port := "2587" go func() { - startSMTPServer(true, hostname, port) + startSMTPServer(false, hostname, port, sha1.New) }() time.Sleep(time.Millisecond * 500) @@ -439,7 +513,7 @@ func TestAuthSCRAMSHA1_fail(t *testing.T) { if err = client.Hello(hostname); err != nil { t.Errorf("failed to send HELO: %v", err) } - if err = client.Auth(ScramSHA1Auth("username", "password")); err == nil { + if err = client.Auth(ScramSHA1Auth("username", "invalid")); err == nil { t.Errorf("expected auth error, got nil") } } @@ -449,7 +523,7 @@ func TestAuthSCRAMSHA256_fail(t *testing.T) { port := "2588" go func() { - startSMTPServer(true, hostname, port) + startSMTPServer(false, hostname, port, sha256.New) }() time.Sleep(time.Millisecond * 500) @@ -464,7 +538,73 @@ func TestAuthSCRAMSHA256_fail(t *testing.T) { if err = client.Hello(hostname); err != nil { t.Errorf("failed to send HELO: %v", err) } - if err = client.Auth(ScramSHA256Auth("username", "password")); err == nil { + if err = client.Auth(ScramSHA256Auth("username", "invalid")); err == nil { + t.Errorf("expected auth error, got nil") + } +} + +func TestAuthSCRAMSHA1PLUS_fail(t *testing.T) { + hostname := "127.0.0.1" + port := "2592" + + go func() { + startSMTPServer(true, hostname, port, sha1.New) + }() + time.Sleep(time.Millisecond * 500) + + cert, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + fmt.Printf("error creating TLS cert: %s", err) + return + } + tlsConfig := tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true} + + conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%s", hostname, port), &tlsConfig) + if err != nil { + t.Errorf("failed to dial server: %v", err) + } + client, err := NewClient(conn, hostname) + if err != nil { + t.Errorf("failed to create client: %v", err) + } + if err = client.Hello(hostname); err != nil { + t.Errorf("failed to send HELO: %v", err) + } + tlsConnState := conn.ConnectionState() + if err = client.Auth(ScramSHA1PlusAuth("username", "invalid", &tlsConnState)); err == nil { + t.Errorf("expected auth error, got nil") + } +} + +func TestAuthSCRAMSHA256PLUS_fail(t *testing.T) { + hostname := "127.0.0.1" + port := "2593" + + go func() { + startSMTPServer(true, hostname, port, sha1.New) + }() + time.Sleep(time.Millisecond * 500) + + cert, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + fmt.Printf("error creating TLS cert: %s", err) + return + } + tlsConfig := tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true} + + conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%s", hostname, port), &tlsConfig) + if err != nil { + t.Errorf("failed to dial server: %v", err) + } + client, err := NewClient(conn, hostname) + if err != nil { + t.Errorf("failed to create client: %v", err) + } + if err = client.Hello(hostname); err != nil { + t.Errorf("failed to send HELO: %v", err) + } + tlsConnState := conn.ConnectionState() + if err = client.Auth(ScramSHA256PlusAuth("username", "invalid", &tlsConnState)); err == nil { t.Errorf("expected auth error, got nil") } } @@ -1652,7 +1792,8 @@ type testSCRAMSMTPServer struct { nonce string hostname string port string - shouldFail bool + tlsServer bool + h func() hash.Hash } func (s *testSCRAMSMTPServer) handleConnection(conn net.Conn) { @@ -1705,7 +1846,8 @@ func (s *testSCRAMSMTPServer) handleConnection(conn net.Conn) { } authMechanism := parts[1] - if authMechanism != "SCRAM-SHA-1" && authMechanism != "SCRAM-SHA-256" { + if authMechanism != "SCRAM-SHA-1" && authMechanism != "SCRAM-SHA-256" && + authMechanism != "SCRAM-SHA-1-PLUS" && authMechanism != "SCRAM-SHA-256-PLUS" { _ = writeLine("504 Unrecognized authentication mechanism") return } @@ -1729,6 +1871,7 @@ func (s *testSCRAMSMTPServer) handleSCRAMAuth(conn net.Conn) { } return writer.Flush() } + var authMsg string data, err := reader.ReadString('\n') if err != nil { @@ -1746,10 +1889,14 @@ func (s *testSCRAMSMTPServer) handleSCRAMAuth(conn net.Conn) { _ = writeLine("535 Authentication failed - expected 4 parts") return } - if splits[0] != "n" { + if !s.tlsServer && splits[0] != "n" { _ = writeLine("535 Authentication failed - expected n to be in the first part") return } + if s.tlsServer && !strings.HasPrefix(splits[0], "p=") { + _ = writeLine("535 Authentication failed - expected p= to be in the first part") + return + } if splits[2] != "n=username" { _ = writeLine("535 Authentication failed - expected n=username to be in the third part") return @@ -1758,6 +1905,8 @@ func (s *testSCRAMSMTPServer) handleSCRAMAuth(conn net.Conn) { _ = writeLine("535 Authentication failed - expected r= to be in the fourth part") return } + authMsg = splits[2] + "," + splits[3] + clientNonce := s.extractNonce(string(decodedMessage)) if clientNonce == "" { _ = writeLine("535 Authentication failed") @@ -1765,8 +1914,10 @@ func (s *testSCRAMSMTPServer) handleSCRAMAuth(conn net.Conn) { } s.nonce = clientNonce + "server_nonce" - serverFirstMessage := fmt.Sprintf("r=%s,s=%s,i=0", s.nonce, "salt") + serverFirstMessage := fmt.Sprintf("r=%s,s=%s,i=4096", s.nonce, + base64.StdEncoding.EncodeToString([]byte("salt"))) _ = writeLine(fmt.Sprintf("334 %s", base64.StdEncoding.EncodeToString([]byte(serverFirstMessage)))) + authMsg = authMsg + "," + serverFirstMessage data, err = reader.ReadString('\n') if err != nil { @@ -1780,10 +1931,32 @@ func (s *testSCRAMSMTPServer) handleSCRAMAuth(conn net.Conn) { return } splits = strings.Split(string(decodedFinalMessage), ",") - if splits[0] != "c=biws" { + + if !s.tlsServer && splits[0] != "c=biws" { _ = writeLine("535 Authentication failed - expected c=biws to be in the first part") return } + if s.tlsServer { + if !strings.HasPrefix(splits[0], "c=") { + _ = writeLine("535 Authentication failed - expected c= to be in the first part") + return + } + channelBind, err := base64.StdEncoding.DecodeString(splits[0][2:]) + if err != nil { + _ = writeLine("535 Authentication failed - base64 channel bind is not valid - " + err.Error()) + return + } + if !strings.HasPrefix(string(channelBind), "p=") { + _ = writeLine("535 Authentication failed - expected channel binding to start with p=-") + return + } + cbType := string(channelBind[2:]) + if !strings.HasPrefix(cbType, "tls-unique") && !strings.HasPrefix(cbType, "tls-exporter") { + _ = writeLine("535 Authentication failed - expected channel binding type tls-unique or tls-exporter") + return + } + } + if !strings.HasPrefix(splits[1], "r=") { _ = writeLine("535 Authentication failed - expected r to be in the second part") return @@ -1797,10 +1970,27 @@ func (s *testSCRAMSMTPServer) handleSCRAMAuth(conn net.Conn) { return } - if s.shouldFail { + authMsg = authMsg + "," + splits[0] + "," + splits[1] + saltedPwd := pbkdf2.Key([]byte("password"), []byte("salt"), 4096, s.h().Size(), s.h) + mac := hmac.New(s.h, saltedPwd) + mac.Write([]byte("Server Key")) + skey := mac.Sum(nil) + mac.Reset() + + mac = hmac.New(s.h, skey) + mac.Write([]byte(authMsg)) + ssig := mac.Sum(nil) + mac.Reset() + + serverFinalMessage := fmt.Sprintf("v=%s", base64.StdEncoding.EncodeToString(ssig)) + _ = writeLine(fmt.Sprintf("334 %s", base64.StdEncoding.EncodeToString([]byte(serverFinalMessage)))) + + _, err = reader.ReadString('\n') + if err != nil { _ = writeLine("535 Authentication failed") return } + _ = writeLine("235 Authentication successful") } @@ -1814,11 +2004,12 @@ func (s *testSCRAMSMTPServer) extractNonce(message string) string { return "" } -func startSMTPServer(shouldFail bool, hostname, port string) { +func startSMTPServer(tlsServer bool, hostname, port string, h func() hash.Hash) { server := &testSCRAMSMTPServer{ - hostname: hostname, - port: port, - shouldFail: shouldFail, + hostname: hostname, + port: port, + tlsServer: tlsServer, + h: h, } listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", hostname, port)) if err != nil { @@ -1827,12 +2018,23 @@ func startSMTPServer(shouldFail bool, hostname, port string) { defer func() { _ = listener.Close() }() + + cert, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + fmt.Printf("error creating TLS cert: %s", err) + return + } + tlsConfig := tls.Config{Certificates: []tls.Certificate{cert}} + for { conn, err := listener.Accept() if err != nil { fmt.Printf("Failed to accept connection: %v", err) continue } + if server.tlsServer { + conn = tls.Server(conn, &tlsConfig) + } go server.handleConnection(conn) } } From e8739b88b0c511fe3a82f31fde3b141ad36e44b4 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Oct 2024 19:44:41 +0200 Subject: [PATCH 116/181] Enhance SMTP AUTH comments and error descriptions Extended documentation for each SMTP AUTH type including security considerations, relevant specifications, and the context for usage. Updated error descriptions for consistency and clarity. This enhances readability and provides better guidance for developers. --- auth.go | 81 +++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 64 insertions(+), 17 deletions(-) diff --git a/auth.go b/auth.go index f1dad86..aae1060 100644 --- a/auth.go +++ b/auth.go @@ -9,66 +9,113 @@ import "errors" // SMTPAuthType represents a string to any SMTP AUTH type type SMTPAuthType string -// Supported SMTP AUTH types const ( - // SMTPAuthCramMD5 is the "CRAM-MD5" SASL authentication mechanism as described in RFC 4954 + // SMTPAuthCramMD5 is the "CRAM-MD5" SASL authentication mechanism as described in RFC 4954. + // https://datatracker.ietf.org/doc/rfc4954/ + // + // CRAM-MD5 is not secure by modern standards. The vulnerabilities of MD5 and the lack of + // advanced security features make it inappropriate for protecting sensitive communications + // today. + // + // It was recommended to deprecate the standard in 20 November 2008. As an alternative it + // recommends e.g. SCRAM or SASL Plain protected by TLS instead. + // https://datatracker.ietf.org/doc/html/draft-ietf-sasl-crammd5-to-historic-00.html SMTPAuthCramMD5 SMTPAuthType = "CRAM-MD5" - // SMTPAuthLogin is the "LOGIN" SASL authentication mechanism + // SMTPAuthLogin 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. + // https://msopenspecs.azureedge.net/files/MS-XLOGIN/%5bMS-XLOGIN%5d.pdf + // https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00 + // + // Since the "LOGIN" SASL authentication mechansim transmits the username and password in + // plaintext over the internet connection, we only allow this mechanism over a TLS secured + // connection. SMTPAuthLogin SMTPAuthType = "LOGIN" // 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 used with the WithSMTPAuth option + // authentication, the Client should not be passed the WithSMTPAuth option at all. SMTPAuthNoAuth SMTPAuthType = "" - // SMTPAuthPlain is the "PLAIN" authentication mechanism as described in RFC 4616 + // SMTPAuthPlain is the "PLAIN" authentication mechanism as described in RFC 4616. + // https://datatracker.ietf.org/doc/rfc4616/ + // + // Since the "PLAIN" SASL authentication mechansim transmits the username and password in + // plaintext over the internet connection, we only allow this mechanism over a TLS secured + // connection. SMTPAuthPlain SMTPAuthType = "PLAIN" // SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism. // https://developers.google.com/gmail/imap/xoauth2-protocol SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2" - // SMTPAuthSCRAMSHA1 represents the SCRAM-SHA-1 SMTP authentication mechanism + // SMTPAuthSCRAMSHA1 is the "SCRAM-SHA-1" SASL authentication mechanism as described in RFC 5802. // https://datatracker.ietf.org/doc/html/rfc5802 + // + // SCRAM-SHA-1 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 + // recommended to prefer stronger alternatives like SCRAM-SHA-256(-PLUS), as SHA-1 has known + // vulnerabilities in other contexts, although it remains effective in HMAC constructions. SMTPAuthSCRAMSHA1 SMTPAuthType = "SCRAM-SHA-1" - // SMTPAuthSCRAMSHA1PLUS represents the "SCRAM-SHA-1-PLUS" authentication mechanism for SMTP. + // SMTPAuthSCRAMSHA1PLUS is the "SCRAM-SHA-1-PLUS" SASL authentication mechanism as described in RFC 5802. // https://datatracker.ietf.org/doc/html/rfc5802 + // + // 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. + // + // 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 + // recommended to prefer stronger alternatives like SCRAM-SHA-256(-PLUS), as SHA-1 has known + // vulnerabilities in other contexts, although it remains effective in HMAC constructions. SMTPAuthSCRAMSHA1PLUS SMTPAuthType = "SCRAM-SHA-1-PLUS" - // SMTPAuthSCRAMSHA256 represents the SCRAM-SHA-256 authentication mechanism for SMTP. + // SMTPAuthSCRAMSHA256 is the "SCRAM-SHA-256" SASL authentication mechanism as described in RFC 7677. // https://datatracker.ietf.org/doc/html/rfc7677 SMTPAuthSCRAMSHA256 SMTPAuthType = "SCRAM-SHA-256" - // SMTPAuthSCRAMSHA256PLUS represents the "SCRAM-SHA-256-PLUS" SMTP AUTH type. + // SMTPAuthSCRAMSHA256PLUS is the "SCRAM-SHA-256-PLUS" SASL authentication mechanism as described in RFC 7677. // https://datatracker.ietf.org/doc/html/rfc7677 + // + // 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. SMTPAuthSCRAMSHA256PLUS SMTPAuthType = "SCRAM-SHA-256-PLUS" ) // SMTP Auth related static errors var ( - // ErrPlainAuthNotSupported should be used if the target server does not support the "PLAIN" schema + // ErrPlainAuthNotSupported is returned when the server does not support the "PLAIN" SMTP + // authentication type. ErrPlainAuthNotSupported = errors.New("server does not support SMTP AUTH type: PLAIN") - // ErrLoginAuthNotSupported should be used if the target server does not support the "LOGIN" schema + // ErrLoginAuthNotSupported is returned when the server does not support the "LOGIN" SMTP + // authentication type. ErrLoginAuthNotSupported = errors.New("server does not support SMTP AUTH type: LOGIN") - // ErrCramMD5AuthNotSupported should be used if the target server does not support the "CRAM-MD5" schema + // ErrCramMD5AuthNotSupported is returned when the server does not support the "CRAM-MD5" SMTP + // authentication type. ErrCramMD5AuthNotSupported = errors.New("server does not support SMTP AUTH type: CRAM-MD5") - // ErrXOauth2AuthNotSupported should be used if the target server does not support the "XOAUTH2" schema + // ErrXOauth2AuthNotSupported is returned when the server does not support the "XOAUTH2" schema. ErrXOauth2AuthNotSupported = errors.New("server does not support SMTP AUTH type: XOAUTH2") - // ErrSCRAMSHA1AuthNotSupported should be used if the target server does not support the "SCRAM-SHA-1" schema + // ErrSCRAMSHA1AuthNotSupported is returned when the server does not support the "SCRAM-SHA-1" SMTP + // authentication type. ErrSCRAMSHA1AuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-1") - // ErrSCRAMSHA1PLUSAuthNotSupported should be used if the target server does not support the "SCRAM-SHA-1-PLUS" schema + // ErrSCRAMSHA1PLUSAuthNotSupported is returned when the server does not support the "SCRAM-SHA-1-PLUS" SMTP + // authentication type. ErrSCRAMSHA1PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-1-PLUS") - // ErrSCRAMSHA256AuthNotSupported should be used if the target server does not support the "SCRAM-SHA-256" schema + // ErrSCRAMSHA256AuthNotSupported is returned when the server does not support the "SCRAM-SHA-256" SMTP + // authentication type. ErrSCRAMSHA256AuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256") - // ErrSCRAMSHA256PLUSAuthNotSupported should be used if the target server does not support the "SCRAM-SHA-256-PLUS" schema + // ErrSCRAMSHA256PLUSAuthNotSupported is returned when the server does not support the "SCRAM-SHA-256-PLUS" SMTP + // authentication type. ErrSCRAMSHA256PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256-PLUS") ) From ea90352ef47a6c4bc9172efdb5d2fb1db9712b6d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Oct 2024 19:45:37 +0200 Subject: [PATCH 117/181] Refactor SMTPAuthType comment for clarity Updated the comment for SMTPAuthType to more clearly explain its purpose as a type wrapper for SMTP authentication mechanisms. This improves code readability and helps future developers understand the type's function. --- auth.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/auth.go b/auth.go index aae1060..4eadd11 100644 --- a/auth.go +++ b/auth.go @@ -6,7 +6,8 @@ package mail import "errors" -// SMTPAuthType represents a string to any SMTP AUTH type +// SMTPAuthType is a type wrapper for a string type. It represents the type of SMTP authentication +// mechanism to be used. type SMTPAuthType string const ( From 6a9c8bb56bdcba34a911063e6a39f73e7432976a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Oct 2024 19:50:10 +0200 Subject: [PATCH 118/181] Refactor documentation and comments for clarity Streamlined comments and documentation in `b64linebreaker.go` for better readability and consistency. Improved descriptions of the Base64LineBreaker and its methods to ensure clarity on functionality. --- b64linebreaker.go | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/b64linebreaker.go b/b64linebreaker.go index 088b38e..8f82cda 100644 --- a/b64linebreaker.go +++ b/b64linebreaker.go @@ -9,21 +9,25 @@ import ( "io" ) -// ErrNoOutWriter is an error message that should be used if a Base64LineBreaker has no out io.Writer set +// newlineBytes is a byte slice representation of the SingleNewLine constant used for line breaking +// in encoding processes. +var newlineBytes = []byte(SingleNewLine) + +// ErrNoOutWriter is the error message returned when no io.Writer is set for Base64LineBreaker. const ErrNoOutWriter = "no io.Writer set for Base64LineBreaker" -// Base64LineBreaker is a io.WriteCloser that writes Base64 encoded data streams -// with line breaks at a given line length +// Base64LineBreaker is used to handle base64 encoding with the insertion of new lines after a certain +// number of characters. +// +// It satisfies the io.WriteCloser interface. type Base64LineBreaker struct { line [MaxBodyLength]byte used int out io.Writer } -var newlineBytes = []byte(SingleNewLine) - -// Write writes the data stream and inserts a SingleNewLine when the maximum -// line length is reached +// Write writes data to the Base64LineBreaker, ensuring lines do not exceed MaxBodyLength. +// It handles continuation if data length exceeds the limit and writes new lines accordingly. func (l *Base64LineBreaker) Write(data []byte) (numBytes int, err error) { if l.out == nil { err = errors.New(ErrNoOutWriter) @@ -55,8 +59,7 @@ func (l *Base64LineBreaker) Write(data []byte) (numBytes int, err error) { return l.Write(data[excess:]) } -// Close closes the Base64LineBreaker and writes any access data that is still -// unwritten in memory +// Close finalizes the Base64LineBreaker, writing any remaining buffered data and appending a newline. func (l *Base64LineBreaker) Close() (err error) { if l.used > 0 { _, err = l.out.Write(l.line[0:l.used]) From 59e91eb9361feb09f9a44936bc57dd3b54402a84 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Oct 2024 20:00:49 +0200 Subject: [PATCH 119/181] Update RFC URLs to use html versions Changed the URLs for RFC 4954 and RFC 4616 from plain text to HTML formats for improved readability and consistency. This adjustment does not affect the functionality but enhances the documentation quality. --- auth.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/auth.go b/auth.go index 4eadd11..110e45e 100644 --- a/auth.go +++ b/auth.go @@ -12,7 +12,7 @@ type SMTPAuthType string const ( // SMTPAuthCramMD5 is the "CRAM-MD5" SASL authentication mechanism as described in RFC 4954. - // https://datatracker.ietf.org/doc/rfc4954/ + // https://datatracker.ietf.org/doc/html/rfc4954/ // // CRAM-MD5 is not secure by modern standards. The vulnerabilities of MD5 and the lack of // advanced security features make it inappropriate for protecting sensitive communications @@ -41,7 +41,7 @@ const ( SMTPAuthNoAuth SMTPAuthType = "" // SMTPAuthPlain is the "PLAIN" authentication mechanism as described in RFC 4616. - // https://datatracker.ietf.org/doc/rfc4616/ + // https://datatracker.ietf.org/doc/html/rfc4616/ // // Since the "PLAIN" SASL authentication mechansim transmits the username and password in // plaintext over the internet connection, we only allow this mechanism over a TLS secured From 92c411454b184c9c76210045aaa15f07a2b40635 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Oct 2024 20:13:13 +0200 Subject: [PATCH 120/181] Enhance comments with detailed explanations and links Improved comments for better clarity by detailing the purpose of each constant and type, and included relevant RFC links for deeper context. These changes aim to help developers quickly understand the code without needing to cross-reference external documents. --- client.go | 75 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/client.go b/client.go index 4d1b0b1..247ca71 100644 --- a/client.go +++ b/client.go @@ -19,67 +19,74 @@ import ( "github.com/wneessen/go-mail/smtp" ) -// Defaults const ( - // DefaultPort is the default connection port to the SMTP server + // DefaultPort is the default connection port to the SMTP server. DefaultPort = 25 - // DefaultPortSSL is the default connection port for SSL/TLS to the SMTP server + // DefaultPortSSL is the default connection port for SSL/TLS to the SMTP server. DefaultPortSSL = 465 - // DefaultPortTLS is the default connection port for STARTTLS to the SMTP server + // DefaultPortTLS is the default connection port for STARTTLS to the SMTP server. DefaultPortTLS = 587 - // DefaultTimeout is the default connection timeout + // DefaultTimeout is the default connection timeout. DefaultTimeout = time.Second * 15 - // DefaultTLSPolicy is the default STARTTLS policy + // DefaultTLSPolicy specifies the default TLS policy for connections. DefaultTLSPolicy = TLSMandatory - // DefaultTLSMinVersion is the minimum TLS version required for the connection - // Nowadays TLS1.2 should be the sane default + // DefaultTLSMinVersion defines the minimum TLS version to be used for secure connections. + // Nowadays TLS 1.2 is assumed be a sane default. DefaultTLSMinVersion = tls.VersionTLS12 ) -// DSNMailReturnOption is a type to define which MAIL RET option is used when a DSN -// is requested -type DSNMailReturnOption string +type ( -// DSNRcptNotifyOption is a type to define which RCPT NOTIFY option is used when a DSN -// is requested -type DSNRcptNotifyOption string + // DSNMailReturnOption is a type wrapper for a string and specifies the type of return content requested + // in a Delivery Status Notification (DSN). + // https://datatracker.ietf.org/doc/html/rfc1891/ + DSNMailReturnOption string + + // DSNRcptNotifyOption is a type wrapper for a string and specifies the notification options for a + // recipient in DSNs. + // https://datatracker.ietf.org/doc/html/rfc1891/ + DSNRcptNotifyOption string +) const ( - // DSNMailReturnHeadersOnly requests that only the headers of the message be returned. - // See: https://www.rfc-editor.org/rfc/rfc1891#section-5.3 + + // DSNMailReturnHeadersOnly requests that only the message headers of the mail message are returned in + // a DSN (Delivery Status Notification). + // https://datatracker.ietf.org/doc/html/rfc1891#section-5.3 DSNMailReturnHeadersOnly DSNMailReturnOption = "HDRS" - // DSNMailReturnFull requests that the entire message be returned in any "failed" - // delivery status notification issued for this recipient - // See: https://www.rfc-editor.org/rfc/rfc1891#section-5.3 + // DSNMailReturnFull requests that the entire mail message is returned in any failed DSN + // (Delivery Status Notification) issued for this recipient. + // https://datatracker.ietf.org/doc/html/rfc1891/#section-5.3 DSNMailReturnFull DSNMailReturnOption = "FULL" - // DSNRcptNotifyNever requests that a DSN not be returned to the sender under - // any conditions. - // See: https://www.rfc-editor.org/rfc/rfc1891#section-5.1 + // DSNRcptNotifyNever indicates that no DSN (Delivery Status Notifications) should be sent for the + // recipient under any condition. + // https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1 DSNRcptNotifyNever DSNRcptNotifyOption = "NEVER" - // DSNRcptNotifySuccess requests that a DSN be issued on successful delivery - // See: https://www.rfc-editor.org/rfc/rfc1891#section-5.1 + // DSNRcptNotifySuccess indicates that the sender requests a DSN (Delivery Status Notification) if the + // message is successfully delivered. + // https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1 DSNRcptNotifySuccess DSNRcptNotifyOption = "SUCCESS" - // DSNRcptNotifyFailure requests that a DSN be issued on delivery failure - // See: https://www.rfc-editor.org/rfc/rfc1891#section-5.1 + // DSNRcptNotifyFailure requests that a DSN (Delivery Status Notification) is issued if delivery of + // a message fails. + // https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1 DSNRcptNotifyFailure DSNRcptNotifyOption = "FAILURE" - // DSNRcptNotifyDelay indicates the sender's willingness to receive - // "delayed" DSNs. Delayed DSNs may be issued if delivery of a message has - // been delayed for an unusual amount of time (as determined by the MTA at - // which the message is delayed), but the final delivery status (whether - // successful or failure) cannot be determined. The absence of the DELAY - // keyword in a NOTIFY parameter requests that a "delayed" DSN NOT be - // issued under any conditions. - // See: https://www.rfc-editor.org/rfc/rfc1891#section-5.1 + // DSNRcptNotifyDelay indicates the sender's willingness to receive "delayed" DSNs. + // + // Delayed DSNs may be issued if delivery of a message has been delayed for an unusual amount of time + // (as determined by the MTA at which the message is delayed), but the final delivery status (whether + // successful or failure) cannot be determined. The absence of the DELAY keyword in a NOTIFY parameter + // requests that a "delayed" DSN NOT be issued under any conditions. + // https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1 DSNRcptNotifyDelay DSNRcptNotifyOption = "DELAY" ) From ef3da39840564fc9dc940920a1dc7d334bf2740e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Oct 2024 20:29:14 +0200 Subject: [PATCH 121/181] Refactor DSN field in Client structure Renamed `dsn` field to `requestDSN` in Client structure for clarity and consistency. Adjusted associated methods and tests to reflect this change, improving code readability and maintainability. --- client.go | 143 +++++++++++++++++++++++++------------------------ client_test.go | 4 +- 2 files changed, 75 insertions(+), 72 deletions(-) diff --git a/client.go b/client.go index 247ca71..6885d90 100644 --- a/client.go +++ b/client.go @@ -40,19 +40,6 @@ const ( DefaultTLSMinVersion = tls.VersionTLS12 ) -type ( - - // DSNMailReturnOption is a type wrapper for a string and specifies the type of return content requested - // in a Delivery Status Notification (DSN). - // https://datatracker.ietf.org/doc/html/rfc1891/ - DSNMailReturnOption string - - // DSNRcptNotifyOption is a type wrapper for a string and specifies the notification options for a - // recipient in DSNs. - // https://datatracker.ietf.org/doc/html/rfc1891/ - DSNRcptNotifyOption string -) - const ( // DSNMailReturnHeadersOnly requests that only the message headers of the mail message are returned in @@ -90,82 +77,98 @@ const ( DSNRcptNotifyDelay DSNRcptNotifyOption = "DELAY" ) -// DialContextFunc is a type to define custom DialContext function. -type DialContextFunc func(ctx context.Context, network, address string) (net.Conn, error) +type ( -// Client is the SMTP client struct -type Client struct { - // Timeout for the SMTP server connection - connTimeout time.Duration + // DialContextFunc defines a function type for establishing a network connection using context, network + // type, and address. It is used to specify custom DialContext function. + // + // By default we use net.Dial or tls.Dial respectively. + DialContextFunc func(ctx context.Context, network, address string) (net.Conn, error) - // dialContextFunc is a custom DialContext function to dial target SMTP server - dialContextFunc DialContextFunc + // DSNMailReturnOption is a type wrapper for a string and specifies the type of return content requested + // in a Delivery Status Notification (DSN). + // https://datatracker.ietf.org/doc/html/rfc1891/ + DSNMailReturnOption string - // dsn indicates that we want to use DSN for the Client - dsn bool + // DSNRcptNotifyOption is a type wrapper for a string and specifies the notification options for a + // recipient in DSNs. + // https://datatracker.ietf.org/doc/html/rfc1891/ + DSNRcptNotifyOption string - // dsnmrtype defines the DSNMailReturnOption in case DSN is enabled - dsnmrtype DSNMailReturnOption + // Option is a function type that modifies the configuration or behavior of a Client instance. + Option func(*Client) error - // dsnrntype defines the DSNRcptNotifyOption in case DSN is enabled - dsnrntype []string + // Client is the go-mail client that is responsible for connecting and interacting with an SMTP server. + Client struct { + // connTimeout specifies timeout for the connection to the SMTP server. + connTimeout time.Duration - // fallbackPort is used as an alternative port number in case the primary port is unavailable or - // fails to bind. - fallbackPort int + // dialContextFunc is the DialContextFunc that is used by the Client to connect to the SMTP server. + dialContextFunc DialContextFunc - // HELO/EHLO string for the greeting the target SMTP server - helo string + // dsnmrtype defines the DSNMailReturnOption in case DSN is enabled + dsnmrtype DSNMailReturnOption - // Hostname of the target SMTP server to connect to - host string + // dsnrntype defines the DSNRcptNotifyOption in case DSN is enabled + dsnrntype []string - // isEncrypted indicates if a Client connection is encrypted or not - isEncrypted bool + // fallbackPort is used as an alternative port number in case the primary port is unavailable or + // fails to bind. + fallbackPort int - // logger is a logger that implements the log.Logger interface - logger log.Logger + // HELO/EHLO string for the greeting the target SMTP server + helo string - // mutex is used to synchronize access to shared resources, ensuring that only one goroutine can - // modify them at a time. - mutex sync.RWMutex + // Hostname of the target SMTP server to connect to + host string - // noNoop indicates the Noop is to be skipped - noNoop bool + // isEncrypted indicates if a Client connection is encrypted or not + isEncrypted bool - // pass is the corresponding SMTP AUTH password - pass string + // logger is a logger that implements the log.Logger interface + logger log.Logger - // port specifies the network port number on which the server listens for incoming connections. - port int + // mutex is used to synchronize access to shared resources, ensuring that only one goroutine can + // modify them at a time. + mutex sync.RWMutex - // smtpAuth is a pointer to smtp.Auth - smtpAuth smtp.Auth + // noNoop indicates the Noop is to be skipped + noNoop bool - // smtpAuthType represents the authentication type for SMTP AUTH - smtpAuthType SMTPAuthType + // pass is the corresponding SMTP AUTH password + pass string - // smtpClient is the smtp.Client that is set up when using the Dial*() methods - smtpClient *smtp.Client + // port specifies the network port number on which the server listens for incoming connections. + port int - // tlspolicy sets the client to use the provided TLSPolicy for the STARTTLS protocol - tlspolicy TLSPolicy + // requestDSN indicates that we want to use DSN for the Client + requestDSN bool - // tlsconfig represents the tls.Config setting for the STARTTLS connection - tlsconfig *tls.Config + // smtpAuth is a pointer to smtp.Auth + smtpAuth smtp.Auth - // useDebugLog enables the debug logging on the SMTP client - useDebugLog bool + // smtpAuthType represents the authentication type for SMTP AUTH + smtpAuthType SMTPAuthType - // user is the SMTP AUTH username - user string + // smtpClient is the smtp.Client that is set up when using the Dial*() methods + smtpClient *smtp.Client - // Use SSL for the connection - useSSL bool -} + // tlspolicy sets the client to use the provided TLSPolicy for the STARTTLS protocol + tlspolicy TLSPolicy -// Option returns a function that can be used for grouping Client options -type Option func(*Client) error + // tlsconfig represents the tls.Config setting for the STARTTLS connection + tlsconfig *tls.Config + + // useDebugLog enables the debug logging on the SMTP client + useDebugLog bool + + // user is the SMTP AUTH username + user string + + // Use SSL for the connection + useSSL bool + } +) var ( // ErrInvalidPort should be used if a port is specified that is not valid @@ -394,7 +397,7 @@ func WithPassword(password string) Option { // and DSNRcptNotifyFailure func WithDSN() Option { return func(c *Client) error { - c.dsn = true + c.requestDSN = true c.dsnmrtype = DSNMailReturnFull c.dsnrntype = []string{string(DSNRcptNotifyFailure), string(DSNRcptNotifySuccess)} return nil @@ -414,7 +417,7 @@ func WithDSNMailReturnType(option DSNMailReturnOption) Option { return ErrInvalidDSNMailReturnOption } - c.dsn = true + c.requestDSN = true c.dsnmrtype = option return nil } @@ -448,7 +451,7 @@ func WithDSNRcptNotifyType(opts ...DSNRcptNotifyOption) Option { return ErrInvalidDSNRcptNotifyCombination } - c.dsn = true + c.requestDSN = true c.dsnrntype = rcptOpts return nil } @@ -870,7 +873,7 @@ func (c *Client) sendSingleMsg(message *Msg) error { } } - if c.dsn { + if c.requestDSN { if c.dsnmrtype != "" { c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype)) } diff --git a/client_test.go b/client_test.go index b8d77ed..7c6f00e 100644 --- a/client_test.go +++ b/client_test.go @@ -483,8 +483,8 @@ func TestWithDSN(t *testing.T) { t.Errorf("failed to create new client: %s", err) return } - if !c.dsn { - t.Errorf("WithDSN failed. c.dsn expected to be: %t, got: %t", true, c.dsn) + if !c.requestDSN { + t.Errorf("WithDSN failed. c.requestDSN expected to be: %t, got: %t", true, c.requestDSN) } if c.dsnmrtype != DSNMailReturnFull { t.Errorf("WithDSN failed. c.dsnmrtype expected to be: %s, got: %s", DSNMailReturnFull, From f7c12d412be96c881cce32d5f746e2c426a87bd6 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Oct 2024 20:30:43 +0200 Subject: [PATCH 122/181] Rename dsnmrtype to dsnReturnType in client.go Refactor variable names for consistency. The `dsnmrtype` variable has been renamed to `dsnReturnType` across the client and test files to improve code readability and maintain uniformity. --- client.go | 12 ++++++------ client_test.go | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/client.go b/client.go index 6885d90..48d36dd 100644 --- a/client.go +++ b/client.go @@ -106,8 +106,8 @@ type ( // dialContextFunc is the DialContextFunc that is used by the Client to connect to the SMTP server. dialContextFunc DialContextFunc - // dsnmrtype defines the DSNMailReturnOption in case DSN is enabled - dsnmrtype DSNMailReturnOption + // dsnReturnType defines the DSNMailReturnOption in case DSN is enabled + dsnReturnType DSNMailReturnOption // dsnrntype defines the DSNRcptNotifyOption in case DSN is enabled dsnrntype []string @@ -398,7 +398,7 @@ func WithPassword(password string) Option { func WithDSN() Option { return func(c *Client) error { c.requestDSN = true - c.dsnmrtype = DSNMailReturnFull + c.dsnReturnType = DSNMailReturnFull c.dsnrntype = []string{string(DSNRcptNotifyFailure), string(DSNRcptNotifySuccess)} return nil } @@ -418,7 +418,7 @@ func WithDSNMailReturnType(option DSNMailReturnOption) Option { } c.requestDSN = true - c.dsnmrtype = option + c.dsnReturnType = option return nil } } @@ -874,8 +874,8 @@ func (c *Client) sendSingleMsg(message *Msg) error { } if c.requestDSN { - if c.dsnmrtype != "" { - c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype)) + if c.dsnReturnType != "" { + c.smtpClient.SetDSNMailReturnOption(string(c.dsnReturnType)) } } if err = c.smtpClient.Mail(from); err != nil { diff --git a/client_test.go b/client_test.go index 7c6f00e..47ffab8 100644 --- a/client_test.go +++ b/client_test.go @@ -486,9 +486,9 @@ func TestWithDSN(t *testing.T) { if !c.requestDSN { t.Errorf("WithDSN failed. c.requestDSN expected to be: %t, got: %t", true, c.requestDSN) } - if c.dsnmrtype != DSNMailReturnFull { - t.Errorf("WithDSN failed. c.dsnmrtype expected to be: %s, got: %s", DSNMailReturnFull, - c.dsnmrtype) + if c.dsnReturnType != DSNMailReturnFull { + t.Errorf("WithDSN failed. c.dsnReturnType expected to be: %s, got: %s", DSNMailReturnFull, + c.dsnReturnType) } if c.dsnrntype[0] != string(DSNRcptNotifyFailure) { t.Errorf("WithDSN failed. c.dsnrntype[0] expected to be: %s, got: %s", DSNRcptNotifyFailure, @@ -519,8 +519,8 @@ func TestWithDSNMailReturnType(t *testing.T) { t.Errorf("failed to create new client: %s", err) return } - if string(c.dsnmrtype) != tt.want { - t.Errorf("WithDSNMailReturnType failed. Expected %s, got: %s", tt.want, string(c.dsnmrtype)) + if string(c.dsnReturnType) != tt.want { + t.Errorf("WithDSNMailReturnType failed. Expected %s, got: %s", tt.want, string(c.dsnReturnType)) } }) } From aab04672f8c78fe782a7d9e52f8eeac78bbbc90b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Oct 2024 20:39:14 +0200 Subject: [PATCH 123/181] Rename DSN type variable for clarity Renamed the variable from `dsnrntype` to `dsnRcptNotifyType` to improve code readability and ensure clarity regarding its purpose. Also updated corresponding comments and test cases to reflect this change. --- client.go | 22 +++++++++++++--------- client_test.go | 18 +++++++++--------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/client.go b/client.go index 48d36dd..45ccf0c 100644 --- a/client.go +++ b/client.go @@ -106,20 +106,24 @@ type ( // dialContextFunc is the DialContextFunc that is used by the Client to connect to the SMTP server. dialContextFunc DialContextFunc - // dsnReturnType defines the DSNMailReturnOption in case DSN is enabled - dsnReturnType DSNMailReturnOption + // dsnRcptNotifyType represents the different types of notifications for DSN (Delivery Status Notifications) + // receipts. + dsnRcptNotifyType []string - // dsnrntype defines the DSNRcptNotifyOption in case DSN is enabled - dsnrntype []string + // dsnReturnType specifies the type of Delivery Status Notification (DSN) that should be requested for an + // email. + dsnReturnType DSNMailReturnOption // fallbackPort is used as an alternative port number in case the primary port is unavailable or // fails to bind. + // + // The fallbackPort is only used in combination with SetTLSPortPolicy and SetSSLPort correspondingly. fallbackPort int - // HELO/EHLO string for the greeting the target SMTP server + // helo is the hostname used in the HELO/EHLO greeting, that is sent to the target SMTP server. helo string - // Hostname of the target SMTP server to connect to + // host is the hostname of the SMTP server we are connecting to. host string // isEncrypted indicates if a Client connection is encrypted or not @@ -399,7 +403,7 @@ func WithDSN() Option { return func(c *Client) error { c.requestDSN = true c.dsnReturnType = DSNMailReturnFull - c.dsnrntype = []string{string(DSNRcptNotifyFailure), string(DSNRcptNotifySuccess)} + c.dsnRcptNotifyType = []string{string(DSNRcptNotifyFailure), string(DSNRcptNotifySuccess)} return nil } } @@ -452,7 +456,7 @@ func WithDSNRcptNotifyType(opts ...DSNRcptNotifyOption) Option { } c.requestDSN = true - c.dsnrntype = rcptOpts + c.dsnRcptNotifyType = rcptOpts return nil } } @@ -892,7 +896,7 @@ func (c *Client) sendSingleMsg(message *Msg) error { rcptSendErr := &SendError{affectedMsg: message} rcptSendErr.errlist = make([]error, 0) rcptSendErr.rcpt = make([]string, 0) - rcptNotifyOpt := strings.Join(c.dsnrntype, ",") + rcptNotifyOpt := strings.Join(c.dsnRcptNotifyType, ",") c.smtpClient.SetDSNRcptNotifyOption(rcptNotifyOpt) for _, rcpt := range rcpts { if err = c.smtpClient.Rcpt(rcpt); err != nil { diff --git a/client_test.go b/client_test.go index 47ffab8..1539426 100644 --- a/client_test.go +++ b/client_test.go @@ -490,13 +490,13 @@ func TestWithDSN(t *testing.T) { t.Errorf("WithDSN failed. c.dsnReturnType expected to be: %s, got: %s", DSNMailReturnFull, c.dsnReturnType) } - if c.dsnrntype[0] != string(DSNRcptNotifyFailure) { - t.Errorf("WithDSN failed. c.dsnrntype[0] expected to be: %s, got: %s", DSNRcptNotifyFailure, - c.dsnrntype[0]) + if c.dsnRcptNotifyType[0] != string(DSNRcptNotifyFailure) { + t.Errorf("WithDSN failed. c.dsnRcptNotifyType[0] expected to be: %s, got: %s", DSNRcptNotifyFailure, + c.dsnRcptNotifyType[0]) } - if c.dsnrntype[1] != string(DSNRcptNotifySuccess) { - t.Errorf("WithDSN failed. c.dsnrntype[1] expected to be: %s, got: %s", DSNRcptNotifySuccess, - c.dsnrntype[1]) + if c.dsnRcptNotifyType[1] != string(DSNRcptNotifySuccess) { + t.Errorf("WithDSN failed. c.dsnRcptNotifyType[1] expected to be: %s, got: %s", DSNRcptNotifySuccess, + c.dsnRcptNotifyType[1]) } } @@ -547,11 +547,11 @@ func TestWithDSNRcptNotifyType(t *testing.T) { t.Errorf("failed to create new client: %s", err) return } - if len(c.dsnrntype) <= 0 && !tt.sf { + if len(c.dsnRcptNotifyType) <= 0 && !tt.sf { t.Errorf("WithDSNRcptNotifyType failed. Expected at least one DSNRNType but got none") } - if !tt.sf && c.dsnrntype[0] != tt.want { - t.Errorf("WithDSNRcptNotifyType failed. Expected %s, got: %s", tt.want, c.dsnrntype[0]) + if !tt.sf && c.dsnRcptNotifyType[0] != tt.want { + t.Errorf("WithDSNRcptNotifyType failed. Expected %s, got: %s", tt.want, c.dsnRcptNotifyType[0]) } }) } From 6cd3cfd2f7c95d277f42dd2cf63ed6e0b6fa925d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Oct 2024 20:57:51 +0200 Subject: [PATCH 124/181] Refactor comments for clarity and consistency Rephrase comments to enhance clarity and maintain consistent style. Improved explanations for fields such as `smtpAuth`, `helo`, and `noNoop`, and standardize grammar and format across all comments. This helps in better understanding the code and its functionality. --- client.go | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/client.go b/client.go index 45ccf0c..3be7d2e 100644 --- a/client.go +++ b/client.go @@ -121,55 +121,66 @@ type ( fallbackPort int // helo is the hostname used in the HELO/EHLO greeting, that is sent to the target SMTP server. + // + // helo might be different as host. This can be useful in a shared-hosting scenario. helo string // host is the hostname of the SMTP server we are connecting to. host string - // isEncrypted indicates if a Client connection is encrypted or not + // isEncrypted indicates wether the Client connection is encrypted or not. isEncrypted bool - // logger is a logger that implements the log.Logger interface + // logger is a logger that satisfies the log.Logger interface. logger log.Logger // mutex is used to synchronize access to shared resources, ensuring that only one goroutine can // modify them at a time. mutex sync.RWMutex - // noNoop indicates the Noop is to be skipped + // noNoop indicates that the Client should skip the "NOOP" command during the dial. + // + // This is useful for servers which delay potentially unwanted clients when they perform commands + // other than AUTH. noNoop bool - // pass is the corresponding SMTP AUTH password + // pass represents a password or a secret token used for the SMTP authentication. pass string - // port specifies the network port number on which the server listens for incoming connections. + // port specifies the network port that is used to establish the connection with the SMTP server. port int - // requestDSN indicates that we want to use DSN for the Client + // requestDSN indicates wether we want to request DSN (Delivery Status Notifications). requestDSN bool - // smtpAuth is a pointer to smtp.Auth + // smtpAuth is the authentication type that is used to authenticate the user with SMTP server. It + // satisfies the smtp.Auth interface. + // + // Unless you plan to write you own custom authentication method, it is advised to not set this manually. + // You should use one of go-mail's SMTPAuthType, instead. smtpAuth smtp.Auth - // smtpAuthType represents the authentication type for SMTP AUTH + // smtpAuthType specifies the authentication type to be used for SMTP authentication. smtpAuthType SMTPAuthType - // smtpClient is the smtp.Client that is set up when using the Dial*() methods + // smtpClient is an instance of smtp.Client used for handling the communication with the SMTP server. smtpClient *smtp.Client - // tlspolicy sets the client to use the provided TLSPolicy for the STARTTLS protocol + // tlspolicy defines the TLSPolicy configuration the Client uses for the STARTTLS protocol. + // https://datatracker.ietf.org/doc/html/rfc3207#section-2 tlspolicy TLSPolicy - // tlsconfig represents the tls.Config setting for the STARTTLS connection + // tlsconfig is a pointer to tls.Config that specifies the TLS configuration for the STARTTLS communication. tlsconfig *tls.Config - // useDebugLog enables the debug logging on the SMTP client + // useDebugLog indicates whether debug level logging is enabled for the Client. useDebugLog bool - // user is the SMTP AUTH username + // user represents a username used for the SMTP authentication. user string - // Use SSL for the connection + // useSSL indicates whether to use SSL/TLS encryption for network communication. + // https://datatracker.ietf.org/doc/html/rfc8314 useSSL bool } ) From 779a3f3942cc1e93ec3924b98a171de2491b9535 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Oct 2024 21:12:05 +0200 Subject: [PATCH 125/181] Refactor error comments for clarity Updated error comments to provide clearer and more descriptive explanations. Enhanced readability by elaborating on the conditions that result in each error, giving developers better context. No functional changes to the code were made. --- client.go | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/client.go b/client.go index 3be7d2e..48f4040 100644 --- a/client.go +++ b/client.go @@ -186,43 +186,41 @@ type ( ) var ( - // ErrInvalidPort should be used if a port is specified that is not valid + // ErrInvalidPort is returned when the specified port for the SMTP connection is not valid ErrInvalidPort = errors.New("invalid port number") - // ErrInvalidTimeout should be used if a timeout is set that is zero or negative + // ErrInvalidTimeout is returned when the specified timeout is zero or negative. ErrInvalidTimeout = errors.New("timeout cannot be zero or negative") - // ErrInvalidHELO should be used if an empty HELO sting is provided + // ErrInvalidHELO is returned when the HELO/EHLO value is invalid due to being empty. ErrInvalidHELO = errors.New("invalid HELO/EHLO value - must not be empty") - // ErrInvalidTLSConfig should be used if an empty tls.Config is provided + // ErrInvalidTLSConfig is returned when the provided TLS configuration is invalid or nil. ErrInvalidTLSConfig = errors.New("invalid TLS config") - // ErrNoHostname should be used if a Client has no hostname set + // ErrNoHostname is returned when the hostname for the client is not provided or empty. ErrNoHostname = errors.New("hostname for client cannot be empty") - // ErrDeadlineExtendFailed should be used if the extension of the connection deadline fails + // ErrDeadlineExtendFailed is returned when an attempt to extend the connection deadline fails. ErrDeadlineExtendFailed = errors.New("connection deadline extension failed") - // ErrNoActiveConnection should be used when a method is used that requies a server connection - // but is not yet connected + // ErrNoActiveConnection indicates that there is no active connection to the SMTP server. ErrNoActiveConnection = errors.New("not connected to SMTP server") - // ErrServerNoUnencoded should be used when 8BIT encoding is selected for a message, but - // the server does not offer 8BITMIME mode + // ErrServerNoUnencoded indicates that the server does not support 8BITMIME for unencoded 8-bit messages. ErrServerNoUnencoded = errors.New("message is 8bit unencoded, but server does not support 8BITMIME") - // ErrInvalidDSNMailReturnOption should be used when an invalid option is provided for the - // DSNMailReturnOption in WithDSN + // ErrInvalidDSNMailReturnOption is returned when an invalid DSNMailReturnOption is provided as argument + // to the WithDSN Option. ErrInvalidDSNMailReturnOption = errors.New("DSN mail return option can only be HDRS or FULL") - // ErrInvalidDSNRcptNotifyOption should be used when an invalid option is provided for the - // DSNRcptNotifyOption in WithDSN + // ErrInvalidDSNRcptNotifyOption is returned when an invalid DSNRcptNotifyOption is provided as argument + // to the WithDSN Option. ErrInvalidDSNRcptNotifyOption = errors.New("DSN rcpt notify option can only be: NEVER, " + "SUCCESS, FAILURE or DELAY") - // ErrInvalidDSNRcptNotifyCombination should be used when an invalid option is provided for the - // DSNRcptNotifyOption in WithDSN + // ErrInvalidDSNRcptNotifyCombination is returned when an invalid combination of DSNRcptNotifyOption is + // provided as argument to the WithDSN Option. ErrInvalidDSNRcptNotifyCombination = errors.New("DSN rcpt notify option NEVER cannot be " + "combined with any of SUCCESS, FAILURE or DELAY") ) From a34f400a05794b4cdecf63ee8d5285217cc6aa5f Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Oct 2024 21:26:29 +0200 Subject: [PATCH 126/181] Improve client option documentation for clarity and validation Enhanced the documentation for NewClient and related option functions to provide clearer descriptions. Added validation details for WithPort and WithTimeout, and improved explanations for SSL/TLS settings. --- client.go | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/client.go b/client.go index 48f4040..5662e22 100644 --- a/client.go +++ b/client.go @@ -225,7 +225,11 @@ var ( "combined with any of SUCCESS, FAILURE or DELAY") ) -// NewClient returns a new Session client object +// NewClient creates a new Client instance with the provided host and optional configuration Option functions. +// It initializes default values for connection timeout, port, TLS settings, and HELO/EHLO hostname. +// Option functions, if provided, override default values. +// +// Returns an error if critical defaults are unset. func NewClient(host string, opts ...Option) (*Client, error) { c := &Client{ connTimeout: DefaultTimeout, @@ -258,7 +262,8 @@ func NewClient(host string, opts ...Option) (*Client, error) { return c, nil } -// WithPort overrides the default connection port +// WithPort sets the port number for the Client and overrides the default port. It validates the port number to +// ensure it is between 1 and 65535. An error is returned if the provided port number is invalid. func WithPort(port int) Option { return func(c *Client) error { if port < 1 || port > 65535 { @@ -269,7 +274,8 @@ func WithPort(port int) Option { } } -// WithTimeout overrides the default connection timeout +// WithTimeout sets the connection timeout for the Client to the provided duration and overrides the default +// timeout. An error is returned if the provided timeout is invalid. func WithTimeout(timeout time.Duration) Option { return func(c *Client) error { if timeout <= 0 { @@ -280,7 +286,7 @@ func WithTimeout(timeout time.Duration) Option { } } -// WithSSL tells the client to use a SSL/TLS connection +// WithSSL enables implicit SSL/TLS for the Client. func WithSSL() Option { return func(c *Client) error { c.useSSL = true @@ -288,16 +294,16 @@ func WithSSL() Option { } } -// WithSSLPort tells the Client wether or not to use SSL and fallback. +// WithSSLPort enables implicit SSL/TLS with an optional fallback for the Client. // The correct port is automatically set. // -// Port 465 is used when SSL set (true). -// Port 25 is used when SSL is unset (false). -// When the SSL connection fails and fb is set to true, -// the client will attempt to connect on port 25 using plaintext. +// If this option is used with NewClient, the default port 25 will be overriden +// with port 465. If fallback is set to true and the SSL/TLS connection fails, +// the Client will attempt to connect on port 25 using plaintext. // -// Note: If a different port has already been set otherwise, the port-choosing -// and fallback automatism will be skipped. +// Note: If a different port has already been set otherwise using WithPort, the +// selected port has higher precedence and is used to establish the SSL/TLS +// connection. In this case the authmatic fallback mechanism is skipped at all. func WithSSLPort(fallback bool) Option { return func(c *Client) error { c.SetSSLPort(true, fallback) From 3e5c93a4189a9a5ee25c978bf9bd4075e10b5e33 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Oct 2024 21:38:39 +0200 Subject: [PATCH 127/181] Refine and clarify SSL and debug logging comments Revised the comments for `WithSSLPort`, `WithDebugLog`, `WithLogger`, and `WithHELO` options to improve readability and provide clearer explanations. Added caution about potential data protection issues when using debug logging and specified defaults where applicable. --- client.go | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/client.go b/client.go index 5662e22..731a73d 100644 --- a/client.go +++ b/client.go @@ -294,16 +294,16 @@ func WithSSL() Option { } } -// WithSSLPort enables implicit SSL/TLS with an optional fallback for the Client. -// The correct port is automatically set. +// WithSSLPort enables implicit SSL/TLS with an optional fallback for the Client. The correct port is +// automatically set. // -// If this option is used with NewClient, the default port 25 will be overriden -// with port 465. If fallback is set to true and the SSL/TLS connection fails, -// the Client will attempt to connect on port 25 using plaintext. +// If this option is used with NewClient, the default port 25 will be overriden with port 465. If fallback +// is set to true and the SSL/TLS connection fails, the Client will attempt to connect on port 25 using +// using an unencrypted connection. // -// Note: If a different port has already been set otherwise using WithPort, the -// selected port has higher precedence and is used to establish the SSL/TLS -// connection. In this case the authmatic fallback mechanism is skipped at all. +// Note: If a different port has already been set otherwise using WithPort, the selected port has higher +// precedence and is used to establish the SSL/TLS connection. In this case the authmatic fallback +// mechanism is skipped at all. func WithSSLPort(fallback bool) Option { return func(c *Client) error { c.SetSSLPort(true, fallback) @@ -311,8 +311,12 @@ func WithSSLPort(fallback bool) Option { } } -// WithDebugLog tells the client to log incoming and outgoing messages of the SMTP client -// to StdErr +// WithDebugLog neables debug logging for the Client. The debug logger will log incoming and outgoing +// communication between the Client and the server to os.StdErr. +// +// Note: The SMTP communication might include unencrypted authentication data, depending if you are +// using SMTP authentication and the type of authentication mechanism. This could pose a data +// protection problem. Use debug logging with care. func WithDebugLog() Option { return func(c *Client) error { c.useDebugLog = true @@ -320,7 +324,10 @@ func WithDebugLog() Option { } } -// WithLogger overrides the default log.Logger that is used for debug logging +// WithLogger defines a custom logger for the Client. The logger has to satisfy the log.Logger +// interface and is only used when debug logging is enabled on the Client. +// +// By default we use log.Stdlog. func WithLogger(logger log.Logger) Option { return func(c *Client) error { c.logger = logger @@ -328,7 +335,9 @@ func WithLogger(logger log.Logger) Option { } } -// WithHELO tells the client to use the provided string as HELO/EHLO greeting host +// WithHELO sets the HELO/EHLO string used for the the Client. +// +// By default we use os.Hostname to identify the HELO/EHLO string. func WithHELO(helo string) Option { return func(c *Client) error { if helo == "" { From bae0ac6cdeb2f01d6ee3e795ec737158e21e8795 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Oct 2024 21:45:12 +0200 Subject: [PATCH 128/181] Refactor TLS policy comments for clarity Updated comments for WithTLSPolicy and WithTLSPortPolicy to provide clearer explanations. Improved readability and emphasized recommended best practices for SMTP TLS connections. --- client.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/client.go b/client.go index 731a73d..200f0c5 100644 --- a/client.go +++ b/client.go @@ -348,10 +348,10 @@ func WithHELO(helo string) Option { } } -// WithTLSPolicy tells the client to use the provided TLSPolicy +// WithTLSPolicy sets the TLSPolicy of the Client and overrides the DefaultTLSPolicy // -// Note: To follow best-practices for SMTP TLS connections, it is recommended -// to use WithTLSPortPolicy instead. +// Note: To follow best-practices for SMTP TLS connections, it is recommended to use +// WithTLSPortPolicy instead. func WithTLSPolicy(policy TLSPolicy) Option { return func(c *Client) error { c.tlspolicy = policy @@ -359,16 +359,16 @@ func WithTLSPolicy(policy TLSPolicy) Option { } } -// WithTLSPortPolicy tells the client to use the provided TLSPolicy, -// The correct port is automatically set. +// WithTLSPortPolicy enables explicit TLS via STARTTLS for the Client using the provided TLSPolicy. The +// correct port is automatically set. // -// Port 587 is used for TLSMandatory and TLSOpportunistic. -// If the connection fails with TLSOpportunistic, -// a plaintext connection is attempted on port 25 as a fallback. -// NoTLS will allways use port 25. +// If TLSMandatory or TLSOpportunistic are provided as TLSPolicy, port 587 will be used for the connection. +// If the connection fails with TLSOpportunistic, the Client will attempt to connect on port 25 using +// using an unencrypted connection as a fallback. If NoTLS is provided, the Client will always use port 25. // -// Note: If a different port has already been set otherwise, the port-choosing -// and fallback automatism will be skipped. +// Note: If a different port has already been set otherwise using WithPort, the selected port has higher +// precedence and is used to establish the SSL/TLS connection. In this case the authmatic fallback +// mechanism is skipped at all. func WithTLSPortPolicy(policy TLSPolicy) Option { return func(c *Client) error { c.SetTLSPortPolicy(policy) From eeaee3f60a609a402e8e05e79767fadc00ef4eb2 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Oct 2024 21:57:38 +0200 Subject: [PATCH 129/181] Update and expand documentation for client configuration options Revised comments provide clearer guidance on the usage of various client configuration functions. Additional details and external references are included for better understanding and error handling. --- client.go | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/client.go b/client.go index 200f0c5..aafed72 100644 --- a/client.go +++ b/client.go @@ -376,7 +376,8 @@ func WithTLSPortPolicy(policy TLSPolicy) Option { } } -// WithTLSConfig tells the client to use the provided *tls.Config +// WithTLSConfig sets the tls.Config for the Client and overrides the default. An error is returned +// if the provided tls.Config is invalid. func WithTLSConfig(tlsconfig *tls.Config) Option { return func(c *Client) error { if tlsconfig == nil { @@ -387,7 +388,7 @@ func WithTLSConfig(tlsconfig *tls.Config) Option { } } -// WithSMTPAuth tells the client to use the provided SMTPAuthType for authentication +// WithSMTPAuth configures the Client to use the specified SMTPAuthType for the SMTP authentication. func WithSMTPAuth(authtype SMTPAuthType) Option { return func(c *Client) error { c.smtpAuthType = authtype @@ -395,7 +396,8 @@ func WithSMTPAuth(authtype SMTPAuthType) Option { } } -// WithSMTPAuthCustom tells the client to use the provided smtp.Auth for SMTP authentication +// WithSMTPAuthCustom sets a custom SMTP authentication mechanism for the client instance. The provided +// authentication mechanism has to satisfy the smtp.Auth interface. func WithSMTPAuthCustom(smtpAuth smtp.Auth) Option { return func(c *Client) error { c.smtpAuth = smtpAuth @@ -403,7 +405,7 @@ func WithSMTPAuthCustom(smtpAuth smtp.Auth) Option { } } -// WithUsername tells the client to use the provided string as username for authentication +// WithUsername sets the username, the Client will use for the SMTP authentication. func WithUsername(username string) Option { return func(c *Client) error { c.user = username @@ -411,7 +413,7 @@ func WithUsername(username string) Option { } } -// WithPassword tells the client to use the provided string as password/secret for authentication +// WithPassword sets the password, the Client will use for the SMTP authentication. func WithPassword(password string) Option { return func(c *Client) error { c.pass = password @@ -419,10 +421,12 @@ func WithPassword(password string) Option { } } -// WithDSN enables the Client to request DSNs (if the server supports it) -// as described in the RFC 1891 and set defaults for DSNMailReturnOption -// to DSNMailReturnFull and DSNRcptNotifyOption to DSNRcptNotifySuccess -// and DSNRcptNotifyFailure +// WithDSN enables DSN (Delivery Status Notifications) for the Client as described in the RFC 1891. DSN +// only work if the server supports them. +// https://datatracker.ietf.org/doc/html/rfc1891 +// +// By default we set DSNMailReturnOption to DSNMailReturnFull and DSNRcptNotifyOption to DSNRcptNotifySuccess +// and DSNRcptNotifyFailure. func WithDSN() Option { return func(c *Client) error { c.requestDSN = true @@ -432,10 +436,11 @@ func WithDSN() Option { } } -// WithDSNMailReturnType enables the Client to request DSNs (if the server supports it) -// as described in the RFC 1891 and set the MAIL FROM Return option type to the -// given DSNMailReturnOption -// See: https://www.rfc-editor.org/rfc/rfc1891 +// WithDSNMailReturnType enables DSN (Delivery Status Notifications) for the Client as described in the +// RFC 1891. DSN only work if the server supports them. +// https://datatracker.ietf.org/doc/html/rfc1891 +// +// It will set the DSNMailReturnOption to the provided value. func WithDSNMailReturnType(option DSNMailReturnOption) Option { return func(c *Client) error { switch option { @@ -451,9 +456,11 @@ func WithDSNMailReturnType(option DSNMailReturnOption) Option { } } -// WithDSNRcptNotifyType enables the Client to request DSNs as described in the RFC 1891 -// and sets the RCPT TO notify options to the given list of DSNRcptNotifyOption -// See: https://www.rfc-editor.org/rfc/rfc1891 +// WithDSNRcptNotifyType enables DSN (Delivery Status Notifications) for the Client as described in the +// RFC 1891. DSN only work if the server supports them. +// https://datatracker.ietf.org/doc/html/rfc1891 +// +// It will set the DSNRcptNotifyOption to the provided values. func WithDSNRcptNotifyType(opts ...DSNRcptNotifyOption) Option { return func(c *Client) error { var rcptOpts []string From d900f5403e6b83d8af5d25cb197bab33629793b5 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Oct 2024 22:23:18 +0200 Subject: [PATCH 130/181] Refine method documentation in client.go Correct typos and enhance clarity in method descriptions. Provide additional context for default values and behavior, and specify consequences for security settings in client configurations. --- client.go | 71 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/client.go b/client.go index aafed72..b63b58d 100644 --- a/client.go +++ b/client.go @@ -311,7 +311,7 @@ func WithSSLPort(fallback bool) Option { } } -// WithDebugLog neables debug logging for the Client. The debug logger will log incoming and outgoing +// WithDebugLog enables debug logging for the Client. The debug logger will log incoming and outgoing // communication between the Client and the server to os.StdErr. // // Note: The SMTP communication might include unencrypted authentication data, depending if you are @@ -492,8 +492,10 @@ func WithDSNRcptNotifyType(opts ...DSNRcptNotifyOption) Option { } } -// WithoutNoop disables the Client Noop check during connections. This is primarily for servers which delay responses -// to SMTP commands that are not the AUTH command. For example Microsoft Exchange's Tarpit. +// WithoutNoop indicates that the Client should skip the "NOOP" command during the dial. +// +// This is useful for servers which delay potentially unwanted clients when they perform commands +// other than AUTH. For example Microsoft Exchange's Tarpit. func WithoutNoop() Option { return func(c *Client) error { c.noNoop = true @@ -501,7 +503,8 @@ func WithoutNoop() Option { } } -// WithDialContextFunc overrides the default DialContext for connecting SMTP server +// WithDialContextFunc sets the provided DialContextFunc as DialContext and overrides the default DialContext for +// connecting to the SMTP server func WithDialContextFunc(dialCtxFunc DialContextFunc) Option { return func(c *Client) error { c.dialContextFunc = dialCtxFunc @@ -509,34 +512,35 @@ func WithDialContextFunc(dialCtxFunc DialContextFunc) Option { } } -// TLSPolicy returns the currently set TLSPolicy as string +// TLSPolicy returns the TLSPolicy that is currently set on the Client as string func (c *Client) TLSPolicy() string { return c.tlspolicy.String() } -// ServerAddr returns the currently set combination of hostname and port +// ServerAddr returns the server address that is currently set on the Client in the format "host:port". func (c *Client) ServerAddr() string { return fmt.Sprintf("%s:%d", c.host, c.port) } -// SetTLSPolicy overrides the current TLSPolicy with the given TLSPolicy value +// SetTLSPolicy sets or overrides the TLSPolicy that is currently set on the Client with the given +// TLSPolicy. // -// Note: To follow best-practices for SMTP TLS connections, it is recommended -// to use SetTLSPortPolicy instead. +// Note: To follow best-practices for SMTP TLS connections, it is recommended to use SetTLSPortPolicy +// instead. func (c *Client) SetTLSPolicy(policy TLSPolicy) { c.tlspolicy = policy } -// SetTLSPortPolicy overrides the current TLSPolicy with the given TLSPolicy -// value. The correct port is automatically set. +// SetTLSPortPolicy sets or overrides the TLSPolicy that is currently set on the Client with the given +// TLSPolicy. The correct port is automatically set. // -// Port 587 is used for TLSMandatory and TLSOpportunistic. -// If the connection fails with TLSOpportunistic, a plaintext connection is -// attempted on port 25 as a fallback. -// NoTLS will allways use port 25. +// If TLSMandatory or TLSOpportunistic are provided as TLSPolicy, port 587 will be used for the connection. +// If the connection fails with TLSOpportunistic, the Client will attempt to connect on port 25 using +// using an unencrypted connection as a fallback. If NoTLS is provided, the Client will always use port 25. // -// Note: If a different port has already been set otherwise, the port-choosing -// and fallback automatism will be skipped. +// Note: If a different port has already been set otherwise using WithPort, the selected port has higher +// precedence and is used to establish the SSL/TLS connection. In this case the authmatic fallback +// mechanism is skipped at all. func (c *Client) SetTLSPortPolicy(policy TLSPolicy) { if c.port == DefaultPort { c.port = DefaultPortTLS @@ -552,21 +556,21 @@ func (c *Client) SetTLSPortPolicy(policy TLSPolicy) { c.tlspolicy = policy } -// SetSSL tells the Client wether to use SSL or not +// SetSSL sets or overrides wether the Client should use implicit SSL/TLS. func (c *Client) SetSSL(ssl bool) { c.useSSL = ssl } -// SetSSLPort tells the Client wether or not to use SSL and fallback. -// The correct port is automatically set. +// SetSSLPort sets or overrides wether the Client should use implicit SSL/TLS with optional fallback. The +// correct port is automatically set. // -// Port 465 is used when SSL set (true). -// Port 25 is used when SSL is unset (false). -// When the SSL connection fails and fb is set to true, -// the client will attempt to connect on port 25 using plaintext. +// If ssl is set to true, the default port 25 will be overriden with port 465. If fallback is set to true +// and the SSL/TLS connection fails, the Client will attempt to connect on port 25 using using an +// unencrypted connection. // -// Note: If a different port has already been set otherwise, the port-choosing -// and fallback automatism will be skipped. +// Note: If a different port has already been set otherwise using WithPort, the selected port has higher +// precedence and is used to establish the SSL/TLS connection. In this case the authmatic fallback +// mechanism is skipped at all. func (c *Client) SetSSLPort(ssl bool, fallback bool) { if c.port == DefaultPort { if ssl { @@ -582,7 +586,12 @@ func (c *Client) SetSSLPort(ssl bool, fallback bool) { c.useSSL = ssl } -// SetDebugLog tells the Client whether debug logging is enabled or not +// SetDebugLog sets or overrides wether the Client is using debug logging. The debug logger will log +// incoming and outgoing communication between the Client and the server to os.StdErr. +// +// Note: The SMTP communication might include unencrypted authentication data, depending if you are +// using SMTP authentication and the type of authentication mechanism. This could pose a data +// protection problem. Use debug logging with care. func (c *Client) SetDebugLog(val bool) { c.useDebugLog = val if c.smtpClient != nil { @@ -590,7 +599,10 @@ func (c *Client) SetDebugLog(val bool) { } } -// SetLogger tells the Client which log.Logger to use +// SetLogger sets of overrides the custom logger currently set for the Client. The logger has to satisfy +// the log.Logger interface and is only used when debug logging is enabled on the Client. +// +// By default we use log.Stdlog. func (c *Client) SetLogger(logger log.Logger) { c.logger = logger if c.smtpClient != nil { @@ -598,7 +610,8 @@ func (c *Client) SetLogger(logger log.Logger) { } } -// SetTLSConfig overrides the current *tls.Config with the given *tls.Config value +// SetTLSConfig sets or overrides the tls.Config that is currently set for the Client with the given value. +// An error is returned if the provided tls.Config is invalid. func (c *Client) SetTLSConfig(tlsconfig *tls.Config) error { c.mutex.Lock() defer c.mutex.Unlock() From 972a3c51c76091fe80015ccda4b235545f00f5bd Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Oct 2024 22:33:42 +0200 Subject: [PATCH 131/181] Add custom SMTP authentication type support Introduce the SMTPAuthCustom type to represent user-defined SMTP authentication mechanisms. Updated relevant functions in client.go to handle the new type appropriately and made sure the client distinguishes between built-in and custom authentication methods. --- auth.go | 6 ++++++ client.go | 16 ++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/auth.go b/auth.go index 110e45e..fef7881 100644 --- a/auth.go +++ b/auth.go @@ -23,6 +23,12 @@ const ( // https://datatracker.ietf.org/doc/html/draft-ietf-sasl-crammd5-to-historic-00.html SMTPAuthCramMD5 SMTPAuthType = "CRAM-MD5" + // SMTPAuthCustom is a custom SMTP AUTH mechanism provided by the user. If a user provides + // a custom smtp.Auth function to the Client, the Client will its smtpAuthType to this type. + // + // Do not use this SMTPAuthType without setting a custom smtp.Auth function on the Client. + SMTPAuthCustom SMTPAuthType = "CUSTOM" + // SMTPAuthLogin 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 diff --git a/client.go b/client.go index b63b58d..5332cda 100644 --- a/client.go +++ b/client.go @@ -396,11 +396,12 @@ func WithSMTPAuth(authtype SMTPAuthType) Option { } } -// WithSMTPAuthCustom sets a custom SMTP authentication mechanism for the client instance. The provided +// WithSMTPAuthCustom sets a custom SMTP authentication mechanism for the Client. The provided // authentication mechanism has to satisfy the smtp.Auth interface. func WithSMTPAuthCustom(smtpAuth smtp.Auth) Option { return func(c *Client) error { c.smtpAuth = smtpAuth + c.smtpAuthType = SMTPAuthCustom return nil } } @@ -623,25 +624,28 @@ func (c *Client) SetTLSConfig(tlsconfig *tls.Config) error { return nil } -// SetUsername overrides the current username string with the given value +// SetUsername sets or overrides the username, the Client will use for the SMTP authentication. func (c *Client) SetUsername(username string) { c.user = username } -// SetPassword overrides the current password string with the given value +// SetPassword sets or overrides the password, the Client will use for the SMTP authentication. func (c *Client) SetPassword(password string) { c.pass = password } -// SetSMTPAuth overrides the current SMTP AUTH type setting with the given value +// SetSMTPAuth sets or overrides the SMTPAuthType that is currently set on the Client for the SMTP +// authentication. func (c *Client) SetSMTPAuth(authtype SMTPAuthType) { c.smtpAuthType = authtype c.smtpAuth = nil } -// SetSMTPAuthCustom overrides the current SMTP AUTH setting with the given custom smtp.Auth +// SetSMTPAuthCustom sets or overrides the custom SMTP authentication mechanism currently set for +// the Client. The provided authentication mechanism has to satisfy the smtp.Auth interface. func (c *Client) SetSMTPAuthCustom(smtpAuth smtp.Auth) { c.smtpAuth = smtpAuth + c.smtpAuthType = SMTPAuthCustom } // setDefaultHelo retrieves the current hostname and sets it as HELO/EHLO hostname @@ -827,7 +831,7 @@ 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 != "" { + if c.smtpAuth == nil && c.smtpAuthType != SMTPAuthCustom { hasSMTPAuth, smtpAuthType := c.smtpClient.Extension("AUTH") if !hasSMTPAuth { return fmt.Errorf("server does not support SMTP AUTH") From 8942b08424e441fa6079583c5495fda4711268a9 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Oct 2024 22:36:28 +0200 Subject: [PATCH 132/181] Add validation for custom SMTP auth type in client tests Previously, only the presence of the SMTP auth method was checked, but not its type. This additional validation ensures that the SMTP auth type is correctly set to custom, thereby improving test accuracy. --- client_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client_test.go b/client_test.go index 1539426..d8dc87f 100644 --- a/client_test.go +++ b/client_test.go @@ -602,6 +602,10 @@ func TestSetSMTPAuthCustom(t *testing.T) { if c.smtpAuth == nil { t.Errorf("failed to set custom SMTP auth method. SMTP Auth method is empty") } + if c.smtpAuthType != SMTPAuthCustom { + t.Errorf("failed to set custom SMTP auth method. SMTP Auth type is not custom: %s", + c.smtpAuthType) + } p, _, err := c.smtpAuth.Start(&si) if err != nil { t.Errorf("SMTP Auth Start() method returned error: %s", err) From dfdadc5da235b0270152c20351c2cacf9491222e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Oct 2024 22:38:18 +0200 Subject: [PATCH 133/181] Refactor client.go: Move functions to new locations Reordered several functions within client.go for better code organization and readability. This change involves moving `setDefaultHelo`, `checkConn`, `serverFallbackAddr`, and `tls` functions to new locations without altering their implementations. --- client.go | 150 +++++++++++++++++++++++++++--------------------------- 1 file changed, 75 insertions(+), 75 deletions(-) diff --git a/client.go b/client.go index 5332cda..48d6731 100644 --- a/client.go +++ b/client.go @@ -648,16 +648,6 @@ func (c *Client) SetSMTPAuthCustom(smtpAuth smtp.Auth) { c.smtpAuthType = SMTPAuthCustom } -// setDefaultHelo retrieves the current hostname and sets it as HELO/EHLO hostname -func (c *Client) setDefaultHelo() error { - hostname, err := os.Hostname() - if err != nil { - return fmt.Errorf("failed to read local hostname: %w", err) - } - c.helo = hostname - return nil -} - // DialWithContext establishes a connection to the SMTP server with a given context.Context func (c *Client) DialWithContext(dialCtx context.Context) error { c.mutex.Lock() @@ -761,71 +751,6 @@ func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) e return nil } -// checkConn makes sure that a required server connection is available and extends the -// connection deadline -func (c *Client) checkConn() error { - if !c.smtpClient.HasConnection() { - return ErrNoActiveConnection - } - - if !c.noNoop { - if err := c.smtpClient.Noop(); err != nil { - return ErrNoActiveConnection - } - } - - if err := c.smtpClient.UpdateDeadline(c.connTimeout); err != nil { - return ErrDeadlineExtendFailed - } - return nil -} - -// serverFallbackAddr returns the currently set combination of hostname -// and fallback port. -func (c *Client) serverFallbackAddr() string { - return fmt.Sprintf("%s:%d", c.host, c.fallbackPort) -} - -// tls tries to make sure that the STARTTLS requirements are satisfied -func (c *Client) tls() error { - if !c.smtpClient.HasConnection() { - return ErrNoActiveConnection - } - if !c.useSSL && c.tlspolicy != NoTLS { - hasStartTLS := false - extension, _ := c.smtpClient.Extension("STARTTLS") - if c.tlspolicy == TLSMandatory { - hasStartTLS = true - if !extension { - return fmt.Errorf("STARTTLS mode set to: %q, but target host does not support STARTTLS", - c.tlspolicy) - } - } - if c.tlspolicy == TLSOpportunistic { - if extension { - hasStartTLS = true - } - } - if hasStartTLS { - if err := c.smtpClient.StartTLS(c.tlsconfig); err != nil { - return err - } - } - tlsConnState, err := c.smtpClient.GetTLSConnectionState() - if err != nil { - switch { - case errors.Is(err, smtp.ErrNonTLSConnection): - c.isEncrypted = false - return nil - default: - return fmt.Errorf("failed to get TLS connection state: %w", err) - } - } - c.isEncrypted = tlsConnState.HandshakeComplete - } - return nil -} - // auth will try to perform SMTP AUTH if requested func (c *Client) auth() error { if err := c.checkConn(); err != nil { @@ -998,3 +923,78 @@ func (c *Client) sendSingleMsg(message *Msg) error { } return nil } + +// checkConn makes sure that a required server connection is available and extends the +// connection deadline +func (c *Client) checkConn() error { + if !c.smtpClient.HasConnection() { + return ErrNoActiveConnection + } + + if !c.noNoop { + if err := c.smtpClient.Noop(); err != nil { + return ErrNoActiveConnection + } + } + + if err := c.smtpClient.UpdateDeadline(c.connTimeout); err != nil { + return ErrDeadlineExtendFailed + } + return nil +} + +// serverFallbackAddr returns the currently set combination of hostname +// and fallback port. +func (c *Client) serverFallbackAddr() string { + return fmt.Sprintf("%s:%d", c.host, c.fallbackPort) +} + +// setDefaultHelo retrieves the current hostname and sets it as HELO/EHLO hostname +func (c *Client) setDefaultHelo() error { + hostname, err := os.Hostname() + if err != nil { + return fmt.Errorf("failed to read local hostname: %w", err) + } + c.helo = hostname + return nil +} + +// tls tries to make sure that the STARTTLS requirements are satisfied +func (c *Client) tls() error { + if !c.smtpClient.HasConnection() { + return ErrNoActiveConnection + } + if !c.useSSL && c.tlspolicy != NoTLS { + hasStartTLS := false + extension, _ := c.smtpClient.Extension("STARTTLS") + if c.tlspolicy == TLSMandatory { + hasStartTLS = true + if !extension { + return fmt.Errorf("STARTTLS mode set to: %q, but target host does not support STARTTLS", + c.tlspolicy) + } + } + if c.tlspolicy == TLSOpportunistic { + if extension { + hasStartTLS = true + } + } + if hasStartTLS { + if err := c.smtpClient.StartTLS(c.tlsconfig); err != nil { + return err + } + } + tlsConnState, err := c.smtpClient.GetTLSConnectionState() + if err != nil { + switch { + case errors.Is(err, smtp.ErrNonTLSConnection): + c.isEncrypted = false + return nil + default: + return fmt.Errorf("failed to get TLS connection state: %w", err) + } + } + c.isEncrypted = tlsConnState.HandshakeComplete + } + return nil +} From adcb8ac41dede831a53691e71167a0b867288a01 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Oct 2024 23:15:01 +0200 Subject: [PATCH 134/181] Fix connection handling and improve thread-safety in SMTP client Reset connections to nil after Close, add RLock in HasConnection, and refine Close logic to handle already closed connections gracefully. Enhanced DialWithContext documentation and added tests for double-close scenarios to ensure robustness. --- client.go | 17 ++++++++++++++--- client_test.go | 43 ++++++++++++++++++++++++++++++++----------- smtp/smtp.go | 7 ++++++- 3 files changed, 52 insertions(+), 15 deletions(-) diff --git a/client.go b/client.go index 48d6731..787fafa 100644 --- a/client.go +++ b/client.go @@ -648,7 +648,17 @@ func (c *Client) SetSMTPAuthCustom(smtpAuth smtp.Auth) { c.smtpAuthType = SMTPAuthCustom } -// DialWithContext establishes a connection to the SMTP server with a given context.Context +// DialWithContext establishes a connection to the server using the provided context.Context. +// +// Before connecting to the server, the function will add a deadline of the Client's timeout +// to the provided context.Context. +// +// After dialing the DialContextFunc defined in the Client and successfully establishing the +// connection to the SMTP server, it will send the HELO/EHLO SMTP command followed by the +// optional STARTTLS and SMTP AUTH commands. It will also attach the log.Logger in case +// debug logging is enabled on the Client. +// +// From this point in time the Client has an active (cancelable) connection to the SMTP server. func (c *Client) DialWithContext(dialCtx context.Context) error { c.mutex.Lock() defer c.mutex.Unlock() @@ -707,8 +717,9 @@ func (c *Client) DialWithContext(dialCtx context.Context) error { // Close closes the Client connection func (c *Client) Close() error { - if err := c.checkConn(); err != nil { - return err + // If the connection is already closed, we considered this a no-op and disregard any error. + if !c.smtpClient.HasConnection() { + return nil } if err := c.smtpClient.Quit(); err != nil { return fmt.Errorf("failed to close SMTP client: %w", err) diff --git a/client_test.go b/client_test.go index d8dc87f..c767e75 100644 --- a/client_test.go +++ b/client_test.go @@ -617,6 +617,32 @@ func TestSetSMTPAuthCustom(t *testing.T) { } } +// TestClient_Close_double tests if a close on an already closed connection causes an error. +func TestClient_Close_double(t *testing.T) { + c, err := getTestConnection(true) + if err != nil { + t.Skipf("failed to create test client: %s. Skipping tests", err) + } + ctx := context.Background() + if err = c.DialWithContext(ctx); err != nil { + t.Errorf("failed to dial with context: %s", err) + return + } + if c.smtpClient == nil { + t.Errorf("DialWithContext didn't fail but no SMTP client found.") + return + } + if !c.smtpClient.HasConnection() { + t.Errorf("DialWithContext didn't fail but no connection found.") + } + if err = c.Close(); err != nil { + t.Errorf("failed to close connection: %s", err) + } + if err = c.Close(); err != nil { + t.Errorf("failed 2nd close connection: %s", err) + } +} + // TestClient_DialWithContext tests the DialWithContext method for the Client object func TestClient_DialWithContext(t *testing.T) { c, err := getTestConnection(true) @@ -2391,7 +2417,6 @@ func TestXOAuth2OK_faker(t *testing.T) { "250 8BITMIME", "250 OK", "235 2.7.0 Accepted", - "250 OK", "221 OK", } var wrote strings.Builder @@ -2412,10 +2437,10 @@ func TestXOAuth2OK_faker(t *testing.T) { if err != nil { t.Fatalf("unable to create new client: %v", err) } - if err := c.DialWithContext(context.Background()); err != nil { + if err = c.DialWithContext(context.Background()); err != nil { t.Fatalf("unexpected dial error: %v", err) } - if err := c.Close(); err != nil { + if err = c.Close(); err != nil { t.Fatalf("disconnect from test server failed: %v", err) } if !strings.Contains(wrote.String(), "AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n") { @@ -2430,7 +2455,6 @@ func TestXOAuth2Unsupported_faker(t *testing.T) { "250-AUTH LOGIN PLAIN", "250 8BITMIME", "250 OK", - "250 OK", "221 OK", } var wrote strings.Builder @@ -2449,18 +2473,18 @@ func TestXOAuth2Unsupported_faker(t *testing.T) { if err != nil { t.Fatalf("unable to create new client: %v", err) } - if err := c.DialWithContext(context.Background()); err == nil { + if err = c.DialWithContext(context.Background()); err == nil { t.Fatal("expected dial error got nil") } else { if !errors.Is(err, ErrXOauth2AuthNotSupported) { t.Fatalf("expected %v; got %v", ErrXOauth2AuthNotSupported, err) } } - if err := c.Close(); err != nil { + if err = c.Close(); err != nil { t.Fatalf("disconnect from test server failed: %v", err) } client := strings.Split(wrote.String(), "\r\n") - if len(client) != 5 { + if len(client) != 4 { t.Fatalf("unexpected number of client requests got %d; want 5", len(client)) } if !strings.HasPrefix(client[0], "EHLO") { @@ -2469,10 +2493,7 @@ func TestXOAuth2Unsupported_faker(t *testing.T) { if client[1] != "NOOP" { t.Fatalf("expected NOOP, got %q", client[1]) } - if client[2] != "NOOP" { - t.Fatalf("expected NOOP, got %q", client[2]) - } - if client[3] != "QUIT" { + if client[2] != "QUIT" { t.Fatalf("expected QUIT, got %q", client[3]) } } diff --git a/smtp/smtp.go b/smtp/smtp.go index f9961c9..ce163a2 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -516,6 +516,8 @@ func (c *Client) Quit() error { } c.mutex.Lock() err = c.Text.Close() + c.Text = nil + c.conn = nil c.mutex.Unlock() return err @@ -555,7 +557,10 @@ func (c *Client) SetDSNRcptNotifyOption(d string) { // HasConnection checks if the client has an active connection. // Returns true if the `conn` field is not nil, indicating an active connection. func (c *Client) HasConnection() bool { - return c.conn != nil + c.mutex.RLock() + conn := c.conn + c.mutex.RUnlock() + return conn != nil } func (c *Client) UpdateDeadline(timeout time.Duration) error { From 48b469faf726d0d44f00547b278277595c6b5444 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Oct 2024 23:23:16 +0200 Subject: [PATCH 135/181] Refactor SMTP client comment and function documentation Updated function comments across client.go to improve clarity and consistency. Added missing details on error handling and context usage for `DialAndSendWithContext` and ensured all functions contain relevant, detailed descriptions. --- client.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/client.go b/client.go index 787fafa..6ea1cbb 100644 --- a/client.go +++ b/client.go @@ -715,9 +715,9 @@ func (c *Client) DialWithContext(dialCtx context.Context) error { return nil } -// Close closes the Client connection +// Close terminates the connection to the SMTP server, returning an error if the disconnection fails. +// If the connection is already closed, we considered this a no-op and disregard any error. func (c *Client) Close() error { - // If the connection is already closed, we considered this a no-op and disregard any error. if !c.smtpClient.HasConnection() { return nil } @@ -728,7 +728,7 @@ func (c *Client) Close() error { return nil } -// Reset sends the RSET command to the SMTP client +// Reset sends an SMTP RSET command to reset the state of the current SMTP session. func (c *Client) Reset() error { if err := c.checkConn(); err != nil { return err @@ -740,19 +740,24 @@ func (c *Client) Reset() error { return nil } -// DialAndSend establishes a connection to the SMTP server with a -// default context.Background and sends the mail +// DialAndSend establishes a connection to the server and sends out the provided Msg. It will call +// DialAndSendWithContext with an empty Context.Background func (c *Client) DialAndSend(messages ...*Msg) error { ctx := context.Background() return c.DialAndSendWithContext(ctx, messages...) } -// DialAndSendWithContext establishes a connection to the SMTP server with a -// custom context and sends the mail +// DialAndSendWithContext establishes a connection to the SMTP server using DialWithContext using the +// provided context.Context, then sends out the given Msg. After successful delivery the Client +// will close the connection to the server. func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) error { if err := c.DialWithContext(ctx); err != nil { return fmt.Errorf("dial failed: %w", err) } + defer func() { + _ = c.Close() + }() + if err := c.Send(messages...); err != nil { return fmt.Errorf("send failed: %w", err) } From fbbf17acd06144be2244643f5b71b7140876dace Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Oct 2024 23:25:43 +0200 Subject: [PATCH 136/181] Refactor comments for clarity in client.go Simplify comments in `client.go` to improve code documentation. Ensure comments are more descriptive and provide context for the functions they describe, enhancing code readability and maintainability. --- client.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/client.go b/client.go index 6ea1cbb..d196a24 100644 --- a/client.go +++ b/client.go @@ -767,7 +767,9 @@ func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) e return nil } -// auth will try to perform SMTP AUTH if requested +// auth attempts to authenticate the client using SMTP AUTH mechanisms. It checks the connection, determines +// the supported authentication methods, and applies the appropriate authentication type. Returns an error if +// authentication fails. func (c *Client) auth() error { if err := c.checkConn(); err != nil { return fmt.Errorf("failed to authenticate: %w", err) @@ -940,8 +942,7 @@ func (c *Client) sendSingleMsg(message *Msg) error { return nil } -// checkConn makes sure that a required server connection is available and extends the -// connection deadline +// checkConn makes sure that a required server connection is available and extends the connection deadline func (c *Client) checkConn() error { if !c.smtpClient.HasConnection() { return ErrNoActiveConnection @@ -959,13 +960,12 @@ func (c *Client) checkConn() error { return nil } -// serverFallbackAddr returns the currently set combination of hostname -// and fallback port. +// serverFallbackAddr returns the currently set combination of hostname and fallback port. func (c *Client) serverFallbackAddr() string { return fmt.Sprintf("%s:%d", c.host, c.fallbackPort) } -// setDefaultHelo retrieves the current hostname and sets it as HELO/EHLO hostname +// setDefaultHelo sets the HELO/EHLO hostname to the local machine's hostname. func (c *Client) setDefaultHelo() error { hostname, err := os.Hostname() if err != nil { @@ -975,7 +975,8 @@ func (c *Client) setDefaultHelo() error { return nil } -// tls tries to make sure that the STARTTLS requirements are satisfied +// tls establishes a TLS connection based on the client's TLS policy and configuration. +// Returns an error if no active connection exists or if a TLS error occurs. func (c *Client) tls() error { if !c.smtpClient.HasConnection() { return ErrNoActiveConnection From 91639436843a757e0a35a04c2523438e956ce3da Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Oct 2024 10:15:43 +0200 Subject: [PATCH 137/181] Add isConnected flag to track active connection state Introduced the isConnected boolean flag in the Client struct to clearly indicate whether there is an active connection. Updated relevant methods to set this flag accordingly, ensuring consistent state management across the Client's lifecycle. --- smtp/smtp.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/smtp/smtp.go b/smtp/smtp.go index ce163a2..444b203 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -67,6 +67,9 @@ type Client struct { // helloError is the error from the hello helloError error + // isConnected indicates if the Client has an active connection + isConnected bool + // localName is the name to use in HELO/EHLO localName string // the name to use in HELO/EHLO @@ -113,6 +116,7 @@ func NewClient(conn net.Conn, host string) (*Client, error) { } c := &Client{Text: text, conn: conn, serverName: host, localName: "localhost"} _, c.tls = conn.(*tls.Conn) + c.isConnected = true return c, nil } @@ -121,6 +125,7 @@ func NewClient(conn net.Conn, host string) (*Client, error) { func (c *Client) Close() error { c.mutex.Lock() err := c.Text.Close() + c.isConnected = false c.mutex.Unlock() return err } @@ -516,8 +521,7 @@ func (c *Client) Quit() error { } c.mutex.Lock() err = c.Text.Close() - c.Text = nil - c.conn = nil + c.isConnected = false c.mutex.Unlock() return err @@ -558,11 +562,12 @@ func (c *Client) SetDSNRcptNotifyOption(d string) { // Returns true if the `conn` field is not nil, indicating an active connection. func (c *Client) HasConnection() bool { c.mutex.RLock() - conn := c.conn + isConn := c.isConnected c.mutex.RUnlock() - return conn != nil + return isConn } +// UpdateDeadline sets a new deadline on the SMTP connection with the specified timeout duration. func (c *Client) UpdateDeadline(timeout time.Duration) error { c.mutex.Lock() if err := c.conn.SetDeadline(time.Now().Add(timeout)); err != nil { From 159c1bf850e02d787cfd04021fe2e917d7dec9cb Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Oct 2024 10:55:25 +0200 Subject: [PATCH 138/181] Add tests for new tls and connection handling methods This commit introduces tests for various TLS-related methods such as GetTLSConnectionState, HasConnection, SetDSNMailReturnOption, SetDSNRcptNotifyOption, and UpdateDeadline. It also modifies the error handling logic in smtp.go to include new error types and improves the mutex handling in UpdateDeadline. --- smtp/smtp.go | 21 ++- smtp/smtp_test.go | 352 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 367 insertions(+), 6 deletions(-) diff --git a/smtp/smtp.go b/smtp/smtp.go index 444b203..7c39996 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -36,7 +36,15 @@ import ( "github.com/wneessen/go-mail/log" ) -var ErrNonTLSConnection = errors.New("connection is not using TLS") +var ( + + // ErrNonTLSConnection is returned when an attempt is made to retrieve TLS state on a non-TLS connection. + ErrNonTLSConnection = errors.New("connection is not using TLS") + + // ErrNoConnection is returned when attempting to perform an operation that requires an established + // connection but none exists. + ErrNoConnection = errors.New("connection is not established") +) // A Client represents a client connection to an SMTP server. type Client struct { @@ -570,10 +578,10 @@ func (c *Client) HasConnection() bool { // UpdateDeadline sets a new deadline on the SMTP connection with the specified timeout duration. func (c *Client) UpdateDeadline(timeout time.Duration) error { c.mutex.Lock() + defer c.mutex.Unlock() if err := c.conn.SetDeadline(time.Now().Add(timeout)); err != nil { return fmt.Errorf("smtp: failed to update deadline: %w", err) } - c.mutex.Unlock() return nil } @@ -583,17 +591,18 @@ func (c *Client) GetTLSConnectionState() (*tls.ConnectionState, error) { c.mutex.RLock() defer c.mutex.RUnlock() + if !c.isConnected { + return nil, ErrNoConnection + + } if !c.tls { return nil, ErrNonTLSConnection } - if c.conn == nil { - return nil, errors.New("smtp: connection is not established") - } if conn, ok := c.conn.(*tls.Conn); ok { cstate := conn.ConnectionState() return &cstate, nil } - return nil, errors.New("smtp: connection is not a TLS connection") + return nil, errors.New("unable to retrieve TLS connection state") } // debugLog checks if the debug flag is set and if so logs the provided message to diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 451a349..7b53963 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -1640,6 +1640,356 @@ func TestTLSConnState(t *testing.T) { <-serverDone } +func TestClient_GetTLSConnectionState(t *testing.T) { + ln := newLocalListener(t) + defer func() { + _ = ln.Close() + }() + clientDone := make(chan bool) + serverDone := make(chan bool) + go func() { + defer close(serverDone) + c, err := ln.Accept() + if err != nil { + t.Errorf("Server accept: %v", err) + return + } + defer func() { + _ = c.Close() + }() + if err := serverHandle(c, t); err != nil { + t.Errorf("server error: %v", err) + } + }() + go func() { + defer close(clientDone) + c, err := Dial(ln.Addr().String()) + if err != nil { + t.Errorf("Client dial: %v", err) + return + } + defer func() { + _ = c.Quit() + }() + cfg := &tls.Config{ServerName: "example.com"} + testHookStartTLS(cfg) // set the RootCAs + if err := c.StartTLS(cfg); err != nil { + t.Errorf("StartTLS: %v", err) + return + } + cs, err := c.GetTLSConnectionState() + if err != nil { + t.Errorf("failed to get TLSConnectionState: %s", err) + return + } + if cs.Version == 0 || !cs.HandshakeComplete { + t.Errorf("ConnectionState = %#v; expect non-zero Version and HandshakeComplete", cs) + } + }() + <-clientDone + <-serverDone +} + +func TestClient_GetTLSConnectionState_noTLS(t *testing.T) { + ln := newLocalListener(t) + defer func() { + _ = ln.Close() + }() + clientDone := make(chan bool) + serverDone := make(chan bool) + go func() { + defer close(serverDone) + c, err := ln.Accept() + if err != nil { + t.Errorf("Server accept: %v", err) + return + } + defer func() { + _ = c.Close() + }() + if err := serverHandle(c, t); err != nil { + t.Errorf("server error: %v", err) + } + }() + go func() { + defer close(clientDone) + c, err := Dial(ln.Addr().String()) + if err != nil { + t.Errorf("Client dial: %v", err) + return + } + defer func() { + _ = c.Quit() + }() + _, err = c.GetTLSConnectionState() + if err == nil { + t.Error("GetTLSConnectionState: expected error; got nil") + return + } + }() + <-clientDone + <-serverDone +} + +func TestClient_GetTLSConnectionState_noConn(t *testing.T) { + ln := newLocalListener(t) + defer func() { + _ = ln.Close() + }() + clientDone := make(chan bool) + serverDone := make(chan bool) + go func() { + defer close(serverDone) + c, err := ln.Accept() + if err != nil { + t.Errorf("Server accept: %v", err) + return + } + defer func() { + _ = c.Close() + }() + if err := serverHandle(c, t); err != nil { + t.Errorf("server error: %v", err) + } + }() + go func() { + defer close(clientDone) + c, err := Dial(ln.Addr().String()) + if err != nil { + t.Errorf("Client dial: %v", err) + return + } + _ = c.Close() + _, err = c.GetTLSConnectionState() + if err == nil { + t.Error("GetTLSConnectionState: expected error; got nil") + return + } + }() + <-clientDone + <-serverDone +} + +func TestClient_GetTLSConnectionState_unableErr(t *testing.T) { + ln := newLocalListener(t) + defer func() { + _ = ln.Close() + }() + clientDone := make(chan bool) + serverDone := make(chan bool) + go func() { + defer close(serverDone) + c, err := ln.Accept() + if err != nil { + t.Errorf("Server accept: %v", err) + return + } + defer func() { + _ = c.Close() + }() + if err := serverHandle(c, t); err != nil { + t.Errorf("server error: %v", err) + } + }() + go func() { + defer close(clientDone) + c, err := Dial(ln.Addr().String()) + if err != nil { + t.Errorf("Client dial: %v", err) + return + } + defer func() { + _ = c.Quit() + }() + c.tls = true + _, err = c.GetTLSConnectionState() + if err == nil { + t.Error("GetTLSConnectionState: expected error; got nil") + return + } + }() + <-clientDone + <-serverDone +} +func TestClient_HasConnection(t *testing.T) { + ln := newLocalListener(t) + defer func() { + _ = ln.Close() + }() + clientDone := make(chan bool) + serverDone := make(chan bool) + go func() { + defer close(serverDone) + c, err := ln.Accept() + if err != nil { + t.Errorf("Server accept: %v", err) + return + } + defer func() { + _ = c.Close() + }() + if err := serverHandle(c, t); err != nil { + t.Errorf("server error: %v", err) + } + }() + go func() { + defer close(clientDone) + c, err := Dial(ln.Addr().String()) + if err != nil { + t.Errorf("Client dial: %v", err) + return + } + cfg := &tls.Config{ServerName: "example.com"} + testHookStartTLS(cfg) // set the RootCAs + if err := c.StartTLS(cfg); err != nil { + t.Errorf("StartTLS: %v", err) + return + } + if !c.HasConnection() { + t.Error("HasConnection: expected true; got false") + return + } + if err = c.Quit(); err != nil { + t.Errorf("closing connection failed: %s", err) + return + } + if c.HasConnection() { + t.Error("HasConnection: expected false; got true") + } + }() + <-clientDone + <-serverDone +} + +func TestClient_SetDSNMailReturnOption(t *testing.T) { + ln := newLocalListener(t) + defer func() { + _ = ln.Close() + }() + clientDone := make(chan bool) + serverDone := make(chan bool) + go func() { + defer close(serverDone) + c, err := ln.Accept() + if err != nil { + t.Errorf("Server accept: %v", err) + return + } + defer func() { + _ = c.Close() + }() + if err := serverHandle(c, t); err != nil { + t.Errorf("server error: %v", err) + } + }() + go func() { + defer close(clientDone) + c, err := Dial(ln.Addr().String()) + if err != nil { + t.Errorf("Client dial: %v", err) + return + } + defer func() { + _ = c.Quit() + }() + c.SetDSNMailReturnOption("foo") + if c.dsnmrtype != "foo" { + t.Errorf("SetDSNMailReturnOption: expected %s; got %s", "foo", c.dsnrntype) + } + }() + <-clientDone + <-serverDone +} + +func TestClient_SetDSNRcptNotifyOption(t *testing.T) { + ln := newLocalListener(t) + defer func() { + _ = ln.Close() + }() + clientDone := make(chan bool) + serverDone := make(chan bool) + go func() { + defer close(serverDone) + c, err := ln.Accept() + if err != nil { + t.Errorf("Server accept: %v", err) + return + } + defer func() { + _ = c.Close() + }() + if err := serverHandle(c, t); err != nil { + t.Errorf("server error: %v", err) + } + }() + go func() { + defer close(clientDone) + c, err := Dial(ln.Addr().String()) + if err != nil { + t.Errorf("Client dial: %v", err) + return + } + defer func() { + _ = c.Quit() + }() + c.SetDSNRcptNotifyOption("foo") + if c.dsnrntype != "foo" { + t.Errorf("SetDSNMailReturnOption: expected %s; got %s", "foo", c.dsnrntype) + } + }() + <-clientDone + <-serverDone +} + +func TestClient_UpdateDeadline(t *testing.T) { + ln := newLocalListener(t) + defer func() { + _ = ln.Close() + }() + clientDone := make(chan bool) + serverDone := make(chan bool) + go func() { + defer close(serverDone) + c, err := ln.Accept() + if err != nil { + t.Errorf("Server accept: %v", err) + return + } + defer func() { + _ = c.Close() + }() + if err = serverHandle(c, t); err != nil { + t.Errorf("server error: %v", err) + } + }() + go func() { + defer close(clientDone) + c, err := Dial(ln.Addr().String()) + if err != nil { + t.Errorf("Client dial: %v", err) + return + } + defer func() { + _ = c.Close() + }() + if !c.HasConnection() { + t.Error("HasConnection: expected true; got false") + return + } + if err = c.UpdateDeadline(time.Millisecond * 20); err != nil { + t.Errorf("failed to update deadline: %s", err) + return + } + time.Sleep(time.Millisecond * 50) + if !c.HasConnection() { + t.Error("HasConnection: expected true; got false") + return + } + }() + <-clientDone + <-serverDone +} + func newLocalListener(t *testing.T) net.Listener { ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { @@ -1685,6 +2035,8 @@ func serverHandle(c net.Conn, t *testing.T) error { } config := &tls.Config{Certificates: []tls.Certificate{keypair}} return tf(config) + case "QUIT": + return nil default: t.Fatalf("unrecognized command: %q", s.Text()) } From fa3c6f956edcfd974f64b157d50f2e9426ce4175 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Oct 2024 11:02:26 +0200 Subject: [PATCH 139/181] Update Send function documentation for better clarity Enhanced the documentation of the `Send` function to explicitly describe the behavior when the Client has no active connection and the handling of multiple message transmissions. This ensures developers understand the error handling mechanism and the association of `SendError` with each message. --- client_119.go | 5 ++++- client_120.go | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/client_119.go b/client_119.go index 7de5d59..24a0118 100644 --- a/client_119.go +++ b/client_119.go @@ -9,7 +9,10 @@ package mail import "errors" -// Send sends out the mail message +// Send attempts to send one or more Msg using the Client connection to the SMTP server. +// If the Client has no active connection to the server, Send will fail with an error. For each of the +// provided Msg it will associate a SendError to the Msg in case there of a transmission or delivery +// error. func (c *Client) Send(messages ...*Msg) error { if err := c.checkConn(); err != nil { return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} diff --git a/client_120.go b/client_120.go index 4f82aa7..c6049eb 100644 --- a/client_120.go +++ b/client_120.go @@ -11,7 +11,10 @@ import ( "errors" ) -// Send sends out the mail message +// Send attempts to send one or more Msg using the Client connection to the SMTP server. +// If the Client has no active connection to the server, Send will fail with an error. For each of the +// provided Msg it will associate a SendError to the Msg in case there of a transmission or delivery +// error. func (c *Client) Send(messages ...*Msg) (returnErr error) { if err := c.checkConn(); err != nil { returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} From 869e8db6c566ccf55dde2f1932ddebcf6b50feb7 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Oct 2024 11:06:46 +0200 Subject: [PATCH 140/181] Improve package documentation and description Expand the package mail description to highlight ease of use, reliance on the Go Standard Library, and added functionalities. Clarify the role of VERSION in the user agent string. --- doc.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/doc.go b/doc.go index 831a57c..de16f5d 100644 --- a/doc.go +++ b/doc.go @@ -2,8 +2,13 @@ // // SPDX-License-Identifier: MIT -// Package mail provides a simple and easy way to send mails with Go +// Package mail provides an easy to use interface for formating and sending mails. go-mail follows idiomatic Go style +// and best practice. It has a small dependency footprint by mainly relying on the Go Standard Library and the Go +// extended packages. It combines a lot of functionality from the standard library to give easy and convenient access +// to mail and SMTP related tasks. It works like a programatic email client and provides lots of methods and +// functionalities you would consider standard in a MUA. package mail -// VERSION is used in the default user agent string +// VERSION indicates the current version of the package. It is also attached to the default user +// agent string. const VERSION = "0.4.4" From 5653df373ba399d244765ac54456b430e4c56c33 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Oct 2024 11:11:17 +0200 Subject: [PATCH 141/181] Add periods to end of go doc comments Ensure all go doc comments in eml.go have consistent punctuation by adding periods to the end of each comment. This improves code readability and maintains uniformity in the documentation style. --- eml.go | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/eml.go b/eml.go index 7e705f6..b57ad3c 100644 --- a/eml.go +++ b/eml.go @@ -18,14 +18,13 @@ import ( "strings" ) -// EMLToMsgFromString will parse a given EML string and returns a pre-filled Msg pointer +// EMLToMsgFromString will parse a given EML string and returns a pre-filled Msg pointer. func EMLToMsgFromString(emlString string) (*Msg, error) { eb := bytes.NewBufferString(emlString) return EMLToMsgFromReader(eb) } -// EMLToMsgFromReader will parse a reader that holds EML content and returns a pre-filled -// Msg pointer +// EMLToMsgFromReader will parse a reader that holds EML content and returns a pre-filled Msg pointer. func EMLToMsgFromReader(reader io.Reader) (*Msg, error) { msg := &Msg{ addrHeader: make(map[AddrHeader][]*netmail.Address), @@ -46,8 +45,7 @@ func EMLToMsgFromReader(reader io.Reader) (*Msg, error) { return msg, nil } -// EMLToMsgFromFile will open and parse a .eml file at a provided file path and returns a -// pre-filled Msg pointer +// EMLToMsgFromFile will open and parse a .eml file at a provided file path and returns a pre-filled Msg pointer. func EMLToMsgFromFile(filePath string) (*Msg, error) { msg := &Msg{ addrHeader: make(map[AddrHeader][]*netmail.Address), @@ -68,7 +66,7 @@ func EMLToMsgFromFile(filePath string) (*Msg, error) { return msg, nil } -// parseEML parses the EML's headers and body and inserts the parsed values into the Msg +// parseEML parses the EML's headers and body and inserts the parsed values into the Msg. func parseEML(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error { if err := parseEMLHeaders(&parsedMsg.Header, msg); err != nil { return fmt.Errorf("failed to parse EML headers: %w", err) @@ -79,7 +77,7 @@ func parseEML(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error return nil } -// readEML opens an EML file and uses net/mail to parse the header and body +// readEML opens an EML file and uses net/mail to parse the header and body. func readEML(filePath string) (*netmail.Message, *bytes.Buffer, error) { fileHandle, err := os.Open(filePath) if err != nil { @@ -91,7 +89,7 @@ func readEML(filePath string) (*netmail.Message, *bytes.Buffer, error) { return readEMLFromReader(fileHandle) } -// readEMLFromReader uses net/mail to parse the header and body from a given io.Reader +// readEMLFromReader uses net/mail to parse the header and body from a given io.Reader. func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error) { parsedMsg, err := netmail.ReadMessage(reader) if err != nil { @@ -106,8 +104,8 @@ func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error return parsedMsg, &buf, nil } -// parseEMLHeaders will check the EML headers for the most common headers and set the -// according settings in the Msg +// parseEMLHeaders will check the EML headers for the most common headers and set the according settings +// in the Msg. func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error { commonHeaders := []Header{ HeaderContentType, HeaderImportance, HeaderInReplyTo, HeaderListUnsubscribe, @@ -175,7 +173,7 @@ func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error { return nil } -// parseEMLBodyParts parses the body of a EML based on the different content types and encodings +// parseEMLBodyParts parses the body of a EML based on the different content types and encodings. func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error { // Extract the transfer encoding of the body mediatype, params, err := mime.ParseMediaType(parsedMsg.Header.Get(HeaderContentType.String())) @@ -212,10 +210,11 @@ func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *M return nil } -// parseEMLBodyPlain parses the mail body of plain type mails +// parseEMLBodyPlain parses the mail body of plain type mails. func parseEMLBodyPlain(mediatype string, parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error { contentTransferEnc := parsedMsg.Header.Get(HeaderContentTransferEnc.String()) - // According to RFC2045, if no Content-Transfer-Encoding is set, we can imply 7bit US-ASCII encoding + // If no Content-Transfer-Encoding is set, we can imply 7bit US-ASCII encoding + // https://datatracker.ietf.org/doc/html/rfc2045#section-6.1 if contentTransferEnc == "" || strings.EqualFold(contentTransferEnc, EncodingUSASCII.String()) { msg.SetEncoding(EncodingUSASCII) msg.SetBodyString(ContentType(mediatype), bodybuf.String()) @@ -349,7 +348,7 @@ ReadNextPart: return nil } -// parseEMLEncoding parses and determines the encoding of the message +// parseEMLEncoding parses and determines the encoding of the message. func parseEMLEncoding(mailHeader *netmail.Header, msg *Msg) { if value := mailHeader.Get(HeaderContentTransferEnc.String()); value != "" { switch { @@ -363,7 +362,7 @@ func parseEMLEncoding(mailHeader *netmail.Header, msg *Msg) { } } -// parseEMLContentTypeCharset parses and determines the charset and content type of the message +// parseEMLContentTypeCharset parses and determines the charset and content type of the message. func parseEMLContentTypeCharset(mailHeader *netmail.Header, msg *Msg) { if value := mailHeader.Get(HeaderContentType.String()); value != "" { contentType, optional := parseMultiPartHeader(value) @@ -377,7 +376,7 @@ func parseEMLContentTypeCharset(mailHeader *netmail.Header, msg *Msg) { } } -// handleEMLMultiPartBase64Encoding sets the content body of a base64 encoded Part +// handleEMLMultiPartBase64Encoding sets the content body of a base64 encoded Part. func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error { part.SetEncoding(EncodingB64) content, err := base64.StdEncoding.DecodeString(string(multiPartData)) @@ -388,8 +387,7 @@ func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error { return nil } -// parseMultiPartHeader parses a multipart header and returns the value and optional parts as -// separate map +// parseMultiPartHeader parses a multipart header and returns the value and optional parts as separate map. func parseMultiPartHeader(multiPartHeader string) (header string, optional map[string]string) { optional = make(map[string]string) headerSplit := strings.SplitN(multiPartHeader, ";", 2) @@ -404,7 +402,7 @@ func parseMultiPartHeader(multiPartHeader string) (header string, optional map[s return } -// parseEMLAttachmentEmbed parses a multipart that is an attachment or embed +// parseEMLAttachmentEmbed parses a multipart that is an attachment or embed. func parseEMLAttachmentEmbed(contentDisposition []string, multiPart *multipart.Part, msg *Msg) error { cdType, optional := parseMultiPartHeader(contentDisposition[0]) filename := "generic.attachment" From ecd0bff5ad17cbd1f2ed138fcbcc63da47c87cf1 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Oct 2024 11:23:44 +0200 Subject: [PATCH 142/181] Update comments for better clarity and add RFC references Revised the comments to provide more detailed descriptions and context for each type and constant. Additionally, included relevant RFC document references where applicable to improve understanding of encoding and MIME types. --- encoding.go | 76 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 27 deletions(-) diff --git a/encoding.go b/encoding.go index 47213da..1adce33 100644 --- a/encoding.go +++ b/encoding.go @@ -4,37 +4,41 @@ package mail -// Charset represents a character set for the encoding +// Charset is a type wrapper for a string representing different character encodings. type Charset string -// ContentType represents a content type for the Msg +// ContentType is a type wrapper for a string and represents the MIME type of the content being handled. type ContentType string -// Encoding represents a MIME encoding scheme like quoted-printable or Base64. +// Encoding is a type wrapper for a string and represents the type of encoding used for email messages +// and/or parts. type Encoding string -// MIMEVersion represents the MIME version for the mail +// MIMEVersion is a type wrapper for a string nad represents the MIME version used in email messages. type MIMEVersion string -// MIMEType represents the MIME type for the mail +// MIMEType is a type wrapper for a string and represents the MIME type for the Msg content or parts. type MIMEType string -// List of supported encodings const ( // EncodingB64 represents the Base64 encoding as specified in RFC 2045. + // https://datatracker.ietf.org/doc/html/rfc2045#section-6.8 EncodingB64 Encoding = "base64" // EncodingQP represents the "quoted-printable" encoding as specified in RFC 2045. + // https://datatracker.ietf.org/doc/html/rfc2045#section-6.7 EncodingQP Encoding = "quoted-printable" // EncodingUSASCII represents encoding with only US-ASCII characters (aka 7Bit) + // https://datatracker.ietf.org/doc/html/rfc2045#section-2.7 EncodingUSASCII Encoding = "7bit" - // NoEncoding avoids any character encoding (except of the mail headers) + // NoEncoding represents 8-bit encoding for email messages as specified in RFC 6152. + // https://datatracker.ietf.org/doc/html/rfc2045#section-2.8 + // https://datatracker.ietf.org/doc/html/rfc6152 NoEncoding Encoding = "8bit" ) -// List of common charsets const ( // CharsetUTF7 represents the "UTF-7" charset CharsetUTF7 Charset = "UTF-7" @@ -133,42 +137,60 @@ const ( CharsetGBK Charset = "GBK" ) -// List of MIME versions -const ( - // MIME10 is the MIME Version 1.0 - MIME10 MIMEVersion = "1.0" -) +// MIME10 represents the MIME version "1.0" used in email messages. +const MIME10 MIMEVersion = "1.0" -// List of common content types const ( - TypeAppOctetStream ContentType = "application/octet-stream" + // TypeAppOctetStream represents the MIME type for arbitrary binary data. + TypeAppOctetStream ContentType = "application/octet-stream" + + // TypeMultipartAlternative represents the MIME type for a message body that can contain multiple alternative + // formats. TypeMultipartAlternative ContentType = "multipart/alternative" - TypeMultipartMixed ContentType = "multipart/mixed" - TypeMultipartRelated ContentType = "multipart/related" - TypePGPSignature ContentType = "application/pgp-signature" - TypePGPEncrypted ContentType = "application/pgp-encrypted" - TypeTextHTML ContentType = "text/html" - TypeTextPlain ContentType = "text/plain" + + // TypeMultipartMixed represents the MIME type for a multipart message containing different parts. + TypeMultipartMixed ContentType = "multipart/mixed" + + // TypeMultipartRelated represents the MIME type for a multipart message where each part is a related file + // or resource. + TypeMultipartRelated ContentType = "multipart/related" + + // TypePGPSignature represents the MIME type for PGP signed messages. + TypePGPSignature ContentType = "application/pgp-signature" + + // TypePGPEncrypted represents the MIME type for PGP encrypted messages. + TypePGPEncrypted ContentType = "application/pgp-encrypted" + + // TypeTextHTML represents the MIME type for HTML text content. + TypeTextHTML ContentType = "text/html" + + // TypeTextPlain represents the MIME type for plain text content. + TypeTextPlain ContentType = "text/plain" ) -// List of MIMETypes const ( + // MIMEAlternative MIMEType represents a MIME multipart/alternative type, used for emails with multiple versions. MIMEAlternative MIMEType = "alternative" - MIMEMixed MIMEType = "mixed" - MIMERelated MIMEType = "related" + + // MIMEMixed MIMEType represents a MIME multipart/mixed type used for emails containing different types of content. + MIMEMixed MIMEType = "mixed" + + // MIMERelated MIMEType represents a MIME multipart/related type, used for emails with related content entities. + MIMERelated MIMEType = "related" ) -// String is a standard method to convert an Charset into a printable format +// String satisfies the fmt.Stringer interface for the Charset type. It converts a Charset into a printable format. func (c Charset) String() string { return string(c) } -// String is a standard method to convert an ContentType into a printable format +// String satisfies the fmt.Stringer interface for the ContentType type. It converts a ContentType into a printable +// format. func (c ContentType) String() string { return string(c) } -// String is a standard method to convert an Encoding into a printable format +// String satisfies the fmt.Stringer interface for the Encoding type. It converts an Encoding into a printable format. func (e Encoding) String() string { return string(e) } From a0a7f74121c040a4453d57061d1e3e130fee77a2 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Oct 2024 11:42:21 +0200 Subject: [PATCH 143/181] Refactor file.go comments for clarity and detail Improved comments for better readability and understanding. Enhanced descriptions for File, FileOption, and various methods, providing more context and precision. Added notes on default behaviors and specific use cases for methods where applicable. --- file.go | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/file.go b/file.go index 45e142a..77e516c 100644 --- a/file.go +++ b/file.go @@ -9,10 +9,11 @@ import ( "net/textproto" ) -// FileOption returns a function that can be used for grouping File options +// FileOption is a function type used to modify properties of a File type FileOption func(*File) -// File is an attachment or embedded file of the Msg +// File represents a file with properties like content type, description, encoding, headers, name, and +// writer function. This can either be an attachment or an embedded file for a Msg. type File struct { ContentType ContentType Desc string @@ -22,32 +23,35 @@ type File struct { Writer func(w io.Writer) (int64, error) } -// WithFileContentID sets the Content-ID header for the File +// WithFileContentID sets the "Content-ID" header in the File's MIME headers to the specified id. func WithFileContentID(id string) FileOption { return func(f *File) { f.Header.Set(HeaderContentID.String(), id) } } -// WithFileName sets the filename of the File +// WithFileName sets the name of a File to the provided value. func WithFileName(name string) FileOption { return func(f *File) { f.Name = name } } -// WithFileDescription sets an optional file description of the File that will be -// added as Content-Description part +// WithFileDescription sets an optional file description for the File. The description is used in the +// Content-Description header of the MIME output. func WithFileDescription(description string) FileOption { return func(f *File) { f.Desc = description } } -// WithFileEncoding sets the encoding of the File. By default we should always use -// Base64 encoding but there might be exceptions, where this might come handy. -// Please note that quoted-printable should never be used for attachments/embeds. If this -// is provided as argument, the function will automatically override back to Base64 +// WithFileEncoding sets the encoding type for a file. +// +// By default one should always use Base64 encoding for attachments and embeds, but there might be exceptions in +// which this might come handy. +// +// Note: that quoted-printable must never be used for attachments or embeds. If EncodingQP is provided as encoding +// to this method, it will be automatically overwritten with EncodingB64. func WithFileEncoding(encoding Encoding) FileOption { return func(f *File) { if encoding == EncodingQP { @@ -58,23 +62,23 @@ func WithFileEncoding(encoding Encoding) FileOption { } // WithFileContentType sets the content type of the File. -// By default go-mail will try to guess the file type and its corresponding -// content type and fall back to application/octet-stream if the file type -// could not be guessed. In some cases, however, it might be needed to force -// this to a specific type. For such situations this override method can -// be used +// +// By default we will try to guess the file type and its corresponding content type and fall back to +// application/octet-stream if the file type, if no matching type could be guessed. This FileOption can +// be used to override this type, in case a specific type is required. func WithFileContentType(contentType ContentType) FileOption { return func(f *File) { f.ContentType = contentType } } -// setHeader sets header fields to a File +// setHeader sets the value of a given MIME header field for the File. func (f *File) setHeader(header Header, value string) { f.Header.Set(string(header), value) } -// getHeader return header fields of a File +// getHeader retrieves the value of the specified MIME header field. It returns the header value and a boolean +// indicating whether the header was present or not. func (f *File) getHeader(header Header) (string, bool) { v := f.Header.Get(string(header)) return v, v != "" From 476130d6e3f20c4bafefca5ff9b495d73c382fe1 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Oct 2024 11:43:50 +0200 Subject: [PATCH 144/181] Fumpt files to make golangci-lint happy --- smtp/smtp.go | 1 - smtp/smtp_test.go | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/smtp/smtp.go b/smtp/smtp.go index 7c39996..d713f8c 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -593,7 +593,6 @@ func (c *Client) GetTLSConnectionState() (*tls.ConnectionState, error) { if !c.isConnected { return nil, ErrNoConnection - } if !c.tls { return nil, ErrNonTLSConnection diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 7b53963..4fd32eb 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -1811,6 +1811,7 @@ func TestClient_GetTLSConnectionState_unableErr(t *testing.T) { <-clientDone <-serverDone } + func TestClient_HasConnection(t *testing.T) { ln := newLocalListener(t) defer func() { From 94f47d43696733f1fb455b4d692d63f13d4a6202 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Oct 2024 11:47:15 +0200 Subject: [PATCH 145/181] Update crypto and text libraries Upgraded golang.org/x/crypto to v0.28.0 and golang.org/x/text to v0.19.0. These updates improve security and compatibility with recent changes in Go modules. Ensure to run `go mod tidy` to clean up any unused dependencies. --- go.mod | 4 ++-- go.sum | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 1dcef3a..4fb64a8 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,6 @@ module github.com/wneessen/go-mail go 1.16 require ( - golang.org/x/crypto v0.27.0 - golang.org/x/text v0.18.0 + golang.org/x/crypto v0.28.0 + golang.org/x/text v0.19.0 ) diff --git a/go.sum b/go.sum index 78b6dba..8e6bffc 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -37,7 +37,7 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -46,7 +46,7 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -55,8 +55,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= From 493f8fc65762531f9cb6dcef1c8975968a0e1535 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Oct 2024 11:54:37 +0200 Subject: [PATCH 146/181] Add periods to charset comments Updated the comment lines for various charset constants to include ending periods for consistency and better readability. This change enhances code documentation quality without altering any functional behavior. --- encoding.go | 64 ++++++++++++++++++++++++++--------------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/encoding.go b/encoding.go index 1adce33..669c694 100644 --- a/encoding.go +++ b/encoding.go @@ -40,100 +40,100 @@ const ( ) const ( - // CharsetUTF7 represents the "UTF-7" charset + // CharsetUTF7 represents the "UTF-7" charset. CharsetUTF7 Charset = "UTF-7" - // CharsetUTF8 represents the "UTF-8" charset + // CharsetUTF8 represents the "UTF-8" charset. CharsetUTF8 Charset = "UTF-8" - // CharsetASCII represents the "US-ASCII" charset + // CharsetASCII represents the "US-ASCII" charset. CharsetASCII Charset = "US-ASCII" - // CharsetISO88591 represents the "ISO-8859-1" charset + // CharsetISO88591 represents the "ISO-8859-1" charset. CharsetISO88591 Charset = "ISO-8859-1" - // CharsetISO88592 represents the "ISO-8859-2" charset + // CharsetISO88592 represents the "ISO-8859-2" charset. CharsetISO88592 Charset = "ISO-8859-2" - // CharsetISO88593 represents the "ISO-8859-3" charset + // CharsetISO88593 represents the "ISO-8859-3" charset. CharsetISO88593 Charset = "ISO-8859-3" - // CharsetISO88594 represents the "ISO-8859-4" charset + // CharsetISO88594 represents the "ISO-8859-4" charset. CharsetISO88594 Charset = "ISO-8859-4" - // CharsetISO88595 represents the "ISO-8859-5" charset + // CharsetISO88595 represents the "ISO-8859-5" charset. CharsetISO88595 Charset = "ISO-8859-5" - // CharsetISO88596 represents the "ISO-8859-6" charset + // CharsetISO88596 represents the "ISO-8859-6" charset. CharsetISO88596 Charset = "ISO-8859-6" - // CharsetISO88597 represents the "ISO-8859-7" charset + // CharsetISO88597 represents the "ISO-8859-7" charset. CharsetISO88597 Charset = "ISO-8859-7" - // CharsetISO88599 represents the "ISO-8859-9" charset + // CharsetISO88599 represents the "ISO-8859-9" charset. CharsetISO88599 Charset = "ISO-8859-9" - // CharsetISO885913 represents the "ISO-8859-13" charset + // CharsetISO885913 represents the "ISO-8859-13" charset. CharsetISO885913 Charset = "ISO-8859-13" - // CharsetISO885914 represents the "ISO-8859-14" charset + // CharsetISO885914 represents the "ISO-8859-14" charset. CharsetISO885914 Charset = "ISO-8859-14" - // CharsetISO885915 represents the "ISO-8859-15" charset + // CharsetISO885915 represents the "ISO-8859-15" charset. CharsetISO885915 Charset = "ISO-8859-15" - // CharsetISO885916 represents the "ISO-8859-16" charset + // CharsetISO885916 represents the "ISO-8859-16" charset. CharsetISO885916 Charset = "ISO-8859-16" - // CharsetISO2022JP represents the "ISO-2022-JP" charset + // CharsetISO2022JP represents the "ISO-2022-JP" charset. CharsetISO2022JP Charset = "ISO-2022-JP" - // CharsetISO2022KR represents the "ISO-2022-KR" charset + // CharsetISO2022KR represents the "ISO-2022-KR" charset. CharsetISO2022KR Charset = "ISO-2022-KR" - // CharsetWindows1250 represents the "windows-1250" charset + // CharsetWindows1250 represents the "windows-1250" charset. CharsetWindows1250 Charset = "windows-1250" - // CharsetWindows1251 represents the "windows-1251" charset + // CharsetWindows1251 represents the "windows-1251" charset. CharsetWindows1251 Charset = "windows-1251" - // CharsetWindows1252 represents the "windows-1252" charset + // CharsetWindows1252 represents the "windows-1252" charset. CharsetWindows1252 Charset = "windows-1252" - // CharsetWindows1255 represents the "windows-1255" charset + // CharsetWindows1255 represents the "windows-1255" charset. CharsetWindows1255 Charset = "windows-1255" - // CharsetWindows1256 represents the "windows-1256" charset + // CharsetWindows1256 represents the "windows-1256" charset. CharsetWindows1256 Charset = "windows-1256" - // CharsetKOI8R represents the "KOI8-R" charset + // CharsetKOI8R represents the "KOI8-R" charset. CharsetKOI8R Charset = "KOI8-R" - // CharsetKOI8U represents the "KOI8-U" charset + // CharsetKOI8U represents the "KOI8-U" charset. CharsetKOI8U Charset = "KOI8-U" - // CharsetBig5 represents the "Big5" charset + // CharsetBig5 represents the "Big5" charset. CharsetBig5 Charset = "Big5" - // CharsetGB18030 represents the "GB18030" charset + // CharsetGB18030 represents the "GB18030" charset. CharsetGB18030 Charset = "GB18030" - // CharsetGB2312 represents the "GB2312" charset + // CharsetGB2312 represents the "GB2312" charset. CharsetGB2312 Charset = "GB2312" - // CharsetTIS620 represents the "TIS-620" charset + // CharsetTIS620 represents the "TIS-620" charset. CharsetTIS620 Charset = "TIS-620" - // CharsetEUCKR represents the "EUC-KR" charset + // CharsetEUCKR represents the "EUC-KR" charset. CharsetEUCKR Charset = "EUC-KR" - // CharsetShiftJIS represents the "Shift_JIS" charset + // CharsetShiftJIS represents the "Shift_JIS" charset. CharsetShiftJIS Charset = "Shift_JIS" - // CharsetUnknown represents the "Unknown" charset + // CharsetUnknown represents the "Unknown" charset. CharsetUnknown Charset = "Unknown" - // CharsetGBK represents the "GBK" charset + // CharsetGBK represents the "GBK" charset. CharsetGBK Charset = "GBK" ) From 96466facdd9fd5953793f8f10787a4dc88b8c109 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Oct 2024 12:00:10 +0200 Subject: [PATCH 147/181] Refactor and document header types and importance levels. Updated type declarations for headers and importance to clarify their roles in the Msg package. Added detailed inline comments and RFC links for better documentation and understanding. Enhanced string representation methods to explicitly handle importance levels and header types. --- header.go | 105 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 58 insertions(+), 47 deletions(-) diff --git a/header.go b/header.go index 9191b7e..d96271a 100644 --- a/header.go +++ b/header.go @@ -4,129 +4,137 @@ package mail -// Header represents a generic mail header field name +// Header is a type wrapper for a string and represents email header fields in a Msg. type Header string -// AddrHeader represents a address related mail Header field name +// AddrHeader is a type wrapper for a string and represents email address headers fields in a Msg. type AddrHeader string -// Importance represents a Importance/Priority value string +// Importance is a type wrapper for an int and represents the level of importance or priority for a Msg. type Importance int -// List of common generic header field names const ( - // HeaderContentDescription is the "Content-Description" header + // HeaderContentDescription is the "Content-Description" header. HeaderContentDescription Header = "Content-Description" - // HeaderContentDisposition is the "Content-Disposition" header + // HeaderContentDisposition is the "Content-Disposition" header. HeaderContentDisposition Header = "Content-Disposition" - // HeaderContentID is the "Content-ID" header + // HeaderContentID is the "Content-ID" header. HeaderContentID Header = "Content-ID" - // HeaderContentLang is the "Content-Language" header + // HeaderContentLang is the "Content-Language" header. HeaderContentLang Header = "Content-Language" - // HeaderContentLocation is the "Content-Location" header (RFC 2110) + // HeaderContentLocation is the "Content-Location" header (RFC 2110). + // https://datatracker.ietf.org/doc/html/rfc2110#section-4.3 HeaderContentLocation Header = "Content-Location" - // HeaderContentTransferEnc is the "Content-Transfer-Encoding" header + // HeaderContentTransferEnc is the "Content-Transfer-Encoding" header. HeaderContentTransferEnc Header = "Content-Transfer-Encoding" - // HeaderContentType is the "Content-Type" header + // HeaderContentType is the "Content-Type" header. HeaderContentType Header = "Content-Type" - // HeaderDate represents the "Date" field - // See: https://www.rfc-editor.org/rfc/rfc822#section-5.1 + // HeaderDate represents the "Date" field. + // https://datatracker.ietf.org/doc/html/rfc822#section-5.1 HeaderDate Header = "Date" - // HeaderDispositionNotificationTo is the MDN header as described in RFC8098 - // See: https://www.rfc-editor.org/rfc/rfc8098.html#section-2.1 + // HeaderDispositionNotificationTo is the MDN header as described in RFC 8098. + // https://datatracker.ietf.org/doc/html/rfc8098#section-2.1 HeaderDispositionNotificationTo Header = "Disposition-Notification-To" - // HeaderImportance represents the "Importance" field + // HeaderImportance represents the "Importance" field. HeaderImportance Header = "Importance" - // HeaderInReplyTo represents the "In-Reply-To" field + // HeaderInReplyTo represents the "In-Reply-To" field. HeaderInReplyTo Header = "In-Reply-To" - // HeaderListUnsubscribe is the "List-Unsubscribe" header field + // HeaderListUnsubscribe is the "List-Unsubscribe" header field. HeaderListUnsubscribe Header = "List-Unsubscribe" - // HeaderListUnsubscribePost is the "List-Unsubscribe-Post" header field + // HeaderListUnsubscribePost is the "List-Unsubscribe-Post" header field. HeaderListUnsubscribePost Header = "List-Unsubscribe-Post" - // HeaderMessageID represents the "Message-ID" field for message identification - // See: https://www.rfc-editor.org/rfc/rfc1036#section-2.1.5 + // HeaderMessageID represents the "Message-ID" field for message identification. + // https://datatracker.ietf.org/doc/html/rfc1036#section-2.1.5 HeaderMessageID Header = "Message-ID" - // HeaderMIMEVersion represents the "MIME-Version" field as per RFC 2045 - // See: https://datatracker.ietf.org/doc/html/rfc2045#section-4 + // HeaderMIMEVersion represents the "MIME-Version" field as per RFC 2045. + // https://datatracker.ietf.org/doc/html/rfc2045#section-4 HeaderMIMEVersion Header = "MIME-Version" - // HeaderOrganization is the "Organization" header field + // HeaderOrganization is the "Organization" header field. HeaderOrganization Header = "Organization" - // HeaderPrecedence is the "Precedence" header field + // HeaderPrecedence is the "Precedence" header field. HeaderPrecedence Header = "Precedence" - // HeaderPriority represents the "Priority" field + // HeaderPriority represents the "Priority" field. HeaderPriority Header = "Priority" - // HeaderReferences is the "References" header field + // HeaderReferences is the "References" header field. HeaderReferences Header = "References" - // HeaderReplyTo is the "Reply-To" header field + // HeaderReplyTo is the "Reply-To" header field. HeaderReplyTo Header = "Reply-To" - // HeaderSubject is the "Subject" header field + // HeaderSubject is the "Subject" header field. HeaderSubject Header = "Subject" - // HeaderUserAgent is the "User-Agent" header field + // HeaderUserAgent is the "User-Agent" header field. HeaderUserAgent Header = "User-Agent" - // HeaderXAutoResponseSuppress is the "X-Auto-Response-Suppress" header field + // HeaderXAutoResponseSuppress is the "X-Auto-Response-Suppress" header field. HeaderXAutoResponseSuppress Header = "X-Auto-Response-Suppress" - // HeaderXMailer is the "X-Mailer" header field + // HeaderXMailer is the "X-Mailer" header field. HeaderXMailer Header = "X-Mailer" - // HeaderXMSMailPriority is the "X-MSMail-Priority" header field + // HeaderXMSMailPriority is the "X-MSMail-Priority" header field. HeaderXMSMailPriority Header = "X-MSMail-Priority" - // HeaderXPriority is the "X-Priority" header field + // HeaderXPriority is the "X-Priority" header field. HeaderXPriority Header = "X-Priority" ) -// List of common address header field names const ( - // HeaderBcc is the "Blind Carbon Copy" header field + // HeaderBcc is the "Blind Carbon Copy" header field. HeaderBcc AddrHeader = "Bcc" - // HeaderCc is the "Carbon Copy" header field + // HeaderCc is the "Carbon Copy" header field. HeaderCc AddrHeader = "Cc" - // HeaderEnvelopeFrom is the envelope FROM header field - // It's not included in the mail body but only used by the Client for the envelope + // HeaderEnvelopeFrom is the envelope FROM header field. It is not included in the mail body but only used by + // the Client for the envelope. HeaderEnvelopeFrom AddrHeader = "EnvelopeFrom" - // HeaderFrom is the "From" header field + // HeaderFrom is the "From" header field. HeaderFrom AddrHeader = "From" - // HeaderTo is the "Receipient" header field + // HeaderTo is the "Receipient" header field. HeaderTo AddrHeader = "To" ) -// List of Importance values const ( + // ImportanceLow indicates a low level of importance or priority in a Msg. ImportanceLow Importance = iota + + // ImportanceNormal indicates a standard level of importance or priority for a Msg. ImportanceNormal + + // ImportanceHigh indicates a high level of importance or priority in a Msg. ImportanceHigh + + // ImportanceNonUrgent indicates a non-urgent level of importance or priority in a Msg. ImportanceNonUrgent + + // ImportanceUrgent indicates an urgent level of importance or priority in a Msg. ImportanceUrgent ) -// NumString returns the importance number string based on the Importance +// NumString returns a numerical string representation of the Importance, mapping ImportanceHigh and +// ImportanceUrgent to "1" and others to "0". func (i Importance) NumString() string { switch i { case ImportanceNonUrgent: @@ -142,7 +150,8 @@ func (i Importance) NumString() string { } } -// XPrioString returns the X-Priority number string based on the Importance +// XPrioString returns the X-Priority string representation of the Importance, mapping ImportanceHigh and +// ImportanceUrgent to "1" and others to "5". func (i Importance) XPrioString() string { switch i { case ImportanceNonUrgent: @@ -158,7 +167,8 @@ func (i Importance) XPrioString() string { } } -// String returns the importance string based on the Importance +// String satisfies the fmt.Stringer interface for the Importance type and returns the string representation of the +// Importance level. func (i Importance) String() string { switch i { case ImportanceNonUrgent: @@ -174,12 +184,13 @@ func (i Importance) String() string { } } -// String returns the header string based on the given Header +// String satisfies the fmt.Stringer interface for the Header type and returns the string representation of the Header. func (h Header) String() string { return string(h) } -// String returns the address header string based on the given AddrHeader +// String satisfies the fmt.Stringer interface for the AddrHeader type and returns the string representation of the +// AddrHeader. func (a AddrHeader) String() string { return string(a) } From a820ba3cee708bd1057e39dabcc2347e2fd1ec9f Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Oct 2024 12:14:00 +0200 Subject: [PATCH 148/181] Correct typo in README.md Fixed a typo in the README where `io.WriteTo` was incorrectly spelled instead of `io.WriterTo`. This ensures the documentation correctly reflects the interfaces implemented by the Message object. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c200c7..3a67d0d 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Here are some highlights of go-mail's featureset: * [X] Support sending mails via a local sendmail command * [X] Support for requestng MDNs (RFC 8098) and DSNs (RFC 1891) * [X] DKIM signature support via [go-mail-middlware](https://github.com/wneessen/go-mail-middleware) -* [X] Message object satisfies `io.WriteTo` and `io.Reader` interfaces +* [X] Message object satisfies `io.WriterTo` and `io.Reader` interfaces * [X] Support for Go's `html/template` and `text/template` (as message body, alternative part or attachment/emebed) * [X] Output to file support which allows storing mail messages as e. g. `.eml` files to disk to open them in a MUA * [X] Debug logging of SMTP traffic From cd4c0194dc24aafd8c97d0155a6ee67d355598e9 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Oct 2024 12:15:01 +0200 Subject: [PATCH 149/181] Refactor comments for clarity and detail Enhanced the descriptive comments for error variables, constants, and types to provide clearer and more detailed explanations. This improves code readability and ensures that the purpose and usage of different elements are better understood by developers. --- msg.go | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/msg.go b/msg.go index b66bffe..5b92cc1 100644 --- a/msg.go +++ b/msg.go @@ -24,51 +24,60 @@ import ( ) var ( - // ErrNoFromAddress should be used when a FROM address is requrested but not set + // ErrNoFromAddress indicates that the FROM address is not set, which is required. ErrNoFromAddress = errors.New("no FROM address set") - // ErrNoRcptAddresses should be used when the list of RCPTs is empty + // ErrNoRcptAddresses indicates that no recipient addresses have been set. ErrNoRcptAddresses = errors.New("no recipient addresses set") ) const ( - // errTplExecuteFailed is issued when the template execution was not successful + // errTplExecuteFailed indicates that the execution of a template has failed, including the underlying error. errTplExecuteFailed = "failed to execute template: %w" - // errTplPointerNil is issued when a template pointer is expected but it is nil + // errTplPointerNil indicates that a template pointer is nil, which prevents further template execution or + // processing. errTplPointerNil = "template pointer is nil" - // errParseMailAddr is used when a mail address could not be validated + // errParseMailAddr indicates that parsing of a mail address has failed, including the problematic address + // and error. errParseMailAddr = "failed to parse mail address %q: %w" ) const ( - // NoPGP indicates that a message should not be treated as PGP encrypted - // or signed and is the default value for a message + // NoPGP indicates that a message should not be treated as PGP encrypted or signed and is the default value + // for a message NoPGP PGPType = iota - // PGPEncrypt indicates that a message should be treated as PGP encrypted - // This works closely together with the corresponding go-mail-middleware + // PGPEncrypt indicates that a message should be treated as PGP encrypted. This works closely together with + // the corresponding go-mail-middleware. PGPEncrypt - // PGPSignature indicates that a message should be treated as PGP signed - // This works closely together with the corresponding go-mail-middleware + // PGPSignature indicates that a message should be treated as PGP signed. This works closely together with + // the corresponding go-mail-middleware. PGPSignature ) -// MiddlewareType is the type description of the Middleware and needs to be returned -// in the Middleware interface by the Type method +// MiddlewareType is a type wrapper for a string. It describes the type of the Middleware and needs to be +// returned by the Middleware.Type method to satisfy the Middleware interface. type MiddlewareType string -// Middleware is an interface to define a function to apply to Msg before sending +// Middleware represents the interface for modifying or handling email messages. A Middleware allows the user to +// alter a Msg before it is finally processed. Multiple Middleware can be applied to a Msg. +// +// Type returns a unique MiddlewareType. It describes the type of Middleware and makes sure that +// a Middleware is only applied once. +// Handle performs all the processing to the Msg. It always needs to return a Msg back. type Middleware interface { Handle(*Msg) *Msg Type() MiddlewareType } -// PGPType is a type alias for a int representing a type of PGP encryption -// or signature +// PGPType is a type wrapper for an int, representing a type of PGP encryption or signature. type PGPType int -// Msg is the mail message struct +// Msg represents an email message with various headers, attachments, and encoding settings. +// +// The Msg is the central part of go-mail. It provided a lot of methods that you would expect in a mail +// user agent (MUA). Msg satisfies the io.WriterTo and io.Reader interfaces. type Msg struct { // addrHeader is a slice of strings that the different mail AddrHeader fields addrHeader map[AddrHeader][]*mail.Address From 682f7a6ca5cb43cfb3d1a8e0bbdeb7c57534fd4e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Oct 2024 12:35:15 +0200 Subject: [PATCH 150/181] Clarify Msg struct field comments Updated comments for all fields in the Msg struct to provide clearer and more detailed explanations. This includes specifying the data type for each field and the role they play within the Msg struct, making the code easier to understand and maintain. --- msg.go | 49 +++++++++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/msg.go b/msg.go index 5b92cc1..118c7b3 100644 --- a/msg.go +++ b/msg.go @@ -79,55 +79,68 @@ type PGPType int // The Msg is the central part of go-mail. It provided a lot of methods that you would expect in a mail // user agent (MUA). Msg satisfies the io.WriterTo and io.Reader interfaces. type Msg struct { - // addrHeader is a slice of strings that the different mail AddrHeader fields + // addrHeader holds a mapping between AddrHeader keys and their corresponding slices of mail.Address pointers. addrHeader map[AddrHeader][]*mail.Address - // attachments represent the different attachment File of the Msg + // attachments holds a list of File pointers that represent files either as attachments or embeds files in + // a Msg. attachments []*File - // boundary is the MIME content boundary + // boundary represents the delimiter for separating parts in a multipart message. boundary string - // charset represents the charset of the mail (defaults to UTF-8) + // charset represents the Charset of the Msg. + // + // By default we set CharsetUTF8 for a Msg unless overridden by a corresponding MsgOption. charset Charset - // embeds represent the different embedded File of the Msg + // embeds contains a slice of File pointers representing the embedded files in a Msg. embeds []*File - // encoder represents a mime.WordEncoder from the std lib + // encoder is a mime.WordEncoder used to encode strings (such as email headers) using a specified + // Encoding. encoder mime.WordEncoder - // encoding represents the message encoding (the encoder will be a corresponding WordEncoder) + // encoding specifies the type of Encoding used for email messages and/or parts. encoding Encoding - // genHeader is a slice of strings that the different generic mail Header fields + // genHeader is a map where the keys are email headers (of type Header) and the values are slices of strings + // representing header values. genHeader map[Header][]string - // isDelivered signals if a message has been delivered or not + // isDelivered indicates wether the Msg has been delivered. isDelivered bool - // middlewares is the list of middlewares to apply to the Msg before sending in FIFO order + // middlewares is a slice of Middleware used for modifying or handling messages before they are processed. + // + // middlewares are processed in FIFO order. middlewares []Middleware - // mimever represents the MIME version + // mimever represents the MIME version used in a Msg. mimever MIMEVersion - // parts represent the different parts of the Msg + // parts is a slice that holds pointers to Part structures, which represent different parts of a Msg. parts []*Part - // preformHeader is a slice of strings that the different generic mail Header fields - // of which content is already preformated and will not be affected by the automatic line - // breaks + // preformHeader maps Header types to their already preformatted string values. + // + // Preformatted Header values will not be affected by automatic line breaks. preformHeader map[Header]string // pgptype indicates that a message has a PGPType assigned and therefore will generate - // different Content-Type settings in the msgWriter + // different Content-Type settings in the msgWriter. pgptype PGPType - // sendError holds the SendError in case a Msg could not be delivered during the Client.Send operation + // sendError represents an error encountered during the process of sending a Msg during the + // Client.Send operation. + // + // sendError will hold an error of type SendError. sendError error - // noDefaultUserAgent indicates whether the default User Agent will be excluded for the Msg when it's sent. + // noDefaultUserAgent indicates whether the default User-Agent will be omitted for the Msg when it is + // being sent. + // + // This can be useful in scenarios where headers are conditionally passed based on receipt - i. e. SMTP proxies. noDefaultUserAgent bool } From c186cba2c22cad99400e801a8dd09b092befdf43 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Oct 2024 12:48:09 +0200 Subject: [PATCH 151/181] Refactor MsgOption comments for clarity Revised the comments for MsgOption functions to provide clearer explanations of their purpose and usage. Added detailed descriptions for options such as WithMIMEVersion and WithBoundary to clarify their contexts and constraints. --- msg.go | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/msg.go b/msg.go index 118c7b3..4c547ca 100644 --- a/msg.go +++ b/msg.go @@ -144,13 +144,14 @@ type Msg struct { noDefaultUserAgent bool } -// SendmailPath is the default system path to the sendmail binary +// SendmailPath is the default system path to the sendmail binary - at least on standard Unix-like OS. const SendmailPath = "/usr/sbin/sendmail" -// MsgOption returns a function that can be used for grouping Msg options +// MsgOption is a function type that modifies a Msg instance during its creation or initialization. type MsgOption func(*Msg) -// NewMsg returns a new Msg pointer +// NewMsg creates a new email message with optional MsgOption functions that customize various aspects of the +// message. func NewMsg(opts ...MsgOption) *Msg { msg := &Msg{ addrHeader: make(map[AddrHeader][]*mail.Address), @@ -161,7 +162,7 @@ func NewMsg(opts ...MsgOption) *Msg { mimever: MIME10, } - // Override defaults with optionally provided MsgOption functions + // Override defaults with optionally provided MsgOption functions. for _, option := range opts { if option == nil { continue @@ -175,49 +176,63 @@ func NewMsg(opts ...MsgOption) *Msg { return msg } -// WithCharset overrides the default message charset +// WithCharset sets the Charset type for a Msg during its creation or initialization. func WithCharset(c Charset) MsgOption { return func(m *Msg) { m.charset = c } } -// WithEncoding overrides the default message encoding +// WithEncoding sets the Encoding type for a Msg during its creation or initialization. func WithEncoding(e Encoding) MsgOption { return func(m *Msg) { m.encoding = e } } -// WithMIMEVersion overrides the default MIME version +// WithMIMEVersion sets the MIMEVersion type for a Msg during its creation or initialization. +// +// Note that in the context of email, MIME Version 1.0 is the only officially standardized and supported +// version. While MIME has been updated and extended over time (via various RFCs), these updates and extensions +// do not introduce new MIME versions; they refine or add features within the framework of MIME 1.0. +// Therefore there should be no reason to ever use this MsgOption. +// https://datatracker.ietf.org/doc/html/rfc1521 +// https://datatracker.ietf.org/doc/html/rfc2045 +// https://datatracker.ietf.org/doc/html/rfc2049 func WithMIMEVersion(mv MIMEVersion) MsgOption { return func(m *Msg) { m.mimever = mv } } -// WithBoundary overrides the default MIME boundary +// WithBoundary sets the boundary of a Msg to the provided string value during its creation or initialization. +// +// Note that by default we create random MIME boundaries. This should only be used if a specific boundary is +// required. func WithBoundary(b string) MsgOption { return func(m *Msg) { m.boundary = b } } -// WithMiddleware add the given middleware in the end of the list of the client middlewares +// WithMiddleware adds the given Middleware to the end of the list of the Client middlewares slice. Middleware +// are processed in FIFO order. func WithMiddleware(mw Middleware) MsgOption { return func(m *Msg) { m.middlewares = append(m.middlewares, mw) } } -// WithPGPType overrides the default PGPType of the message +// WithPGPType sets the PGP type for the Msg during its creation or initialization, determining the encryption or +// signature method. func WithPGPType(pt PGPType) MsgOption { return func(m *Msg) { m.pgptype = pt } } -// WithNoDefaultUserAgent configures the Msg to not use the default User Agent +// WithNoDefaultUserAgent disables the inclusion of a default User-Agent header in the Msg during its creation or +// initialization. func WithNoDefaultUserAgent() MsgOption { return func(m *Msg) { m.noDefaultUserAgent = true From 78e285778298b4d5fbd7c62ea89ca38706f99610 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Oct 2024 13:39:21 +0200 Subject: [PATCH 152/181] Update method comments to include additional context and RFC references Enhanced the comments for various methods in `msg.go`, `client.go`, `auth.go`, and `encoding.go` to provide more detailed explanations, context, and relevant RFC references. This improves the clarity and maintainability of the code by providing developers with a deeper understanding of each method's purpose and usage. --- auth.go | 20 +++++++++++----- client.go | 19 ++++++++++++--- encoding.go | 5 ++++ msg.go | 66 +++++++++++++++++++++++++++++++++-------------------- 4 files changed, 76 insertions(+), 34 deletions(-) diff --git a/auth.go b/auth.go index fef7881..e175a12 100644 --- a/auth.go +++ b/auth.go @@ -20,6 +20,7 @@ const ( // // It was recommended to deprecate the standard in 20 November 2008. As an alternative it // recommends e.g. SCRAM or SASL Plain protected by TLS instead. + // // https://datatracker.ietf.org/doc/html/draft-ietf-sasl-crammd5-to-historic-00.html SMTPAuthCramMD5 SMTPAuthType = "CRAM-MD5" @@ -33,12 +34,14 @@ const ( // 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. - // https://msopenspecs.azureedge.net/files/MS-XLOGIN/%5bMS-XLOGIN%5d.pdf - // https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00 // // Since the "LOGIN" SASL authentication mechansim transmits the username and password in // plaintext over the internet connection, we only allow this mechanism over a TLS secured // connection. + // + // https://msopenspecs.azureedge.net/files/MS-XLOGIN/%5bMS-XLOGIN%5d.pdf + // + // https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00 SMTPAuthLogin SMTPAuthType = "LOGIN" // SMTPAuthNoAuth is equivalent to performing no authentication at all. It is a convenience @@ -47,11 +50,12 @@ const ( SMTPAuthNoAuth SMTPAuthType = "" // SMTPAuthPlain is the "PLAIN" authentication mechanism as described in RFC 4616. - // https://datatracker.ietf.org/doc/html/rfc4616/ // // Since the "PLAIN" SASL authentication mechansim 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" // SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism. @@ -59,16 +63,16 @@ const ( SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2" // SMTPAuthSCRAMSHA1 is the "SCRAM-SHA-1" SASL authentication mechanism as described in RFC 5802. - // https://datatracker.ietf.org/doc/html/rfc5802 // // SCRAM-SHA-1 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 // recommended to prefer stronger alternatives like SCRAM-SHA-256(-PLUS), as SHA-1 has known // vulnerabilities in other contexts, although it remains effective in HMAC constructions. + // + // https://datatracker.ietf.org/doc/html/rfc5802 SMTPAuthSCRAMSHA1 SMTPAuthType = "SCRAM-SHA-1" // SMTPAuthSCRAMSHA1PLUS is the "SCRAM-SHA-1-PLUS" SASL authentication mechanism as described in RFC 5802. - // https://datatracker.ietf.org/doc/html/rfc5802 // // 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 @@ -78,18 +82,22 @@ const ( // of a challenge-response authentication mechanism (as we use it). However, it is generally // recommended to prefer stronger alternatives like SCRAM-SHA-256(-PLUS), as SHA-1 has known // vulnerabilities in other contexts, although it remains effective in HMAC constructions. + // + // https://datatracker.ietf.org/doc/html/rfc5802 SMTPAuthSCRAMSHA1PLUS SMTPAuthType = "SCRAM-SHA-1-PLUS" // SMTPAuthSCRAMSHA256 is the "SCRAM-SHA-256" SASL authentication mechanism as described in RFC 7677. + // // https://datatracker.ietf.org/doc/html/rfc7677 SMTPAuthSCRAMSHA256 SMTPAuthType = "SCRAM-SHA-256" // SMTPAuthSCRAMSHA256PLUS is the "SCRAM-SHA-256-PLUS" SASL authentication mechanism as described in RFC 7677. - // https://datatracker.ietf.org/doc/html/rfc7677 // // 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. + // + // https://datatracker.ietf.org/doc/html/rfc7677 SMTPAuthSCRAMSHA256PLUS SMTPAuthType = "SCRAM-SHA-256-PLUS" ) diff --git a/client.go b/client.go index d196a24..26a8bb5 100644 --- a/client.go +++ b/client.go @@ -44,26 +44,31 @@ const ( // DSNMailReturnHeadersOnly requests that only the message headers of the mail message are returned in // a DSN (Delivery Status Notification). + // // https://datatracker.ietf.org/doc/html/rfc1891#section-5.3 DSNMailReturnHeadersOnly DSNMailReturnOption = "HDRS" // DSNMailReturnFull requests that the entire mail message is returned in any failed DSN // (Delivery Status Notification) issued for this recipient. + // // https://datatracker.ietf.org/doc/html/rfc1891/#section-5.3 DSNMailReturnFull DSNMailReturnOption = "FULL" // DSNRcptNotifyNever indicates that no DSN (Delivery Status Notifications) should be sent for the // recipient under any condition. + // // https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1 DSNRcptNotifyNever DSNRcptNotifyOption = "NEVER" // DSNRcptNotifySuccess indicates that the sender requests a DSN (Delivery Status Notification) if the // message is successfully delivered. + // // https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1 DSNRcptNotifySuccess DSNRcptNotifyOption = "SUCCESS" // DSNRcptNotifyFailure requests that a DSN (Delivery Status Notification) is issued if delivery of // a message fails. + // // https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1 DSNRcptNotifyFailure DSNRcptNotifyOption = "FAILURE" @@ -73,6 +78,7 @@ const ( // (as determined by the MTA at which the message is delayed), but the final delivery status (whether // successful or failure) cannot be determined. The absence of the DELAY keyword in a NOTIFY parameter // requests that a "delayed" DSN NOT be issued under any conditions. + // // https://datatracker.ietf.org/doc/html/rfc1891/#section-5.1 DSNRcptNotifyDelay DSNRcptNotifyOption = "DELAY" ) @@ -87,11 +93,13 @@ type ( // DSNMailReturnOption is a type wrapper for a string and specifies the type of return content requested // in a Delivery Status Notification (DSN). + // // https://datatracker.ietf.org/doc/html/rfc1891/ DSNMailReturnOption string // DSNRcptNotifyOption is a type wrapper for a string and specifies the notification options for a // recipient in DSNs. + // // https://datatracker.ietf.org/doc/html/rfc1891/ DSNRcptNotifyOption string @@ -167,6 +175,7 @@ type ( smtpClient *smtp.Client // tlspolicy defines the TLSPolicy configuration the Client uses for the STARTTLS protocol. + // // https://datatracker.ietf.org/doc/html/rfc3207#section-2 tlspolicy TLSPolicy @@ -180,6 +189,7 @@ type ( user string // useSSL indicates whether to use SSL/TLS encryption for network communication. + // // https://datatracker.ietf.org/doc/html/rfc8314 useSSL bool } @@ -424,10 +434,11 @@ func WithPassword(password string) Option { // WithDSN enables DSN (Delivery Status Notifications) for the Client as described in the RFC 1891. DSN // only work if the server supports them. -// https://datatracker.ietf.org/doc/html/rfc1891 // // By default we set DSNMailReturnOption to DSNMailReturnFull and DSNRcptNotifyOption to DSNRcptNotifySuccess // and DSNRcptNotifyFailure. +// +// https://datatracker.ietf.org/doc/html/rfc1891 func WithDSN() Option { return func(c *Client) error { c.requestDSN = true @@ -439,9 +450,10 @@ func WithDSN() Option { // WithDSNMailReturnType enables DSN (Delivery Status Notifications) for the Client as described in the // RFC 1891. DSN only work if the server supports them. -// https://datatracker.ietf.org/doc/html/rfc1891 // // It will set the DSNMailReturnOption to the provided value. +// +// https://datatracker.ietf.org/doc/html/rfc1891 func WithDSNMailReturnType(option DSNMailReturnOption) Option { return func(c *Client) error { switch option { @@ -459,9 +471,10 @@ func WithDSNMailReturnType(option DSNMailReturnOption) Option { // WithDSNRcptNotifyType enables DSN (Delivery Status Notifications) for the Client as described in the // RFC 1891. DSN only work if the server supports them. -// https://datatracker.ietf.org/doc/html/rfc1891 // // It will set the DSNRcptNotifyOption to the provided values. +// +// https://datatracker.ietf.org/doc/html/rfc1891 func WithDSNRcptNotifyType(opts ...DSNRcptNotifyOption) Option { return func(c *Client) error { var rcptOpts []string diff --git a/encoding.go b/encoding.go index 669c694..2b559c4 100644 --- a/encoding.go +++ b/encoding.go @@ -22,19 +22,24 @@ type MIMEType string const ( // EncodingB64 represents the Base64 encoding as specified in RFC 2045. + // // https://datatracker.ietf.org/doc/html/rfc2045#section-6.8 EncodingB64 Encoding = "base64" // EncodingQP represents the "quoted-printable" encoding as specified in RFC 2045. + // // https://datatracker.ietf.org/doc/html/rfc2045#section-6.7 EncodingQP Encoding = "quoted-printable" // EncodingUSASCII represents encoding with only US-ASCII characters (aka 7Bit) + // // https://datatracker.ietf.org/doc/html/rfc2045#section-2.7 EncodingUSASCII Encoding = "7bit" // NoEncoding represents 8-bit encoding for email messages as specified in RFC 6152. + // // https://datatracker.ietf.org/doc/html/rfc2045#section-2.8 + // // https://datatracker.ietf.org/doc/html/rfc6152 NoEncoding Encoding = "8bit" ) diff --git a/msg.go b/msg.go index 4c547ca..995e93f 100644 --- a/msg.go +++ b/msg.go @@ -197,7 +197,9 @@ func WithEncoding(e Encoding) MsgOption { // do not introduce new MIME versions; they refine or add features within the framework of MIME 1.0. // Therefore there should be no reason to ever use this MsgOption. // https://datatracker.ietf.org/doc/html/rfc1521 +// // https://datatracker.ietf.org/doc/html/rfc2045 +// // https://datatracker.ietf.org/doc/html/rfc2049 func WithMIMEVersion(mv MIMEVersion) MsgOption { return func(m *Msg) { @@ -239,52 +241,68 @@ func WithNoDefaultUserAgent() MsgOption { } } -// SetCharset sets the encoding charset of the Msg +// SetCharset sets or overrides the currently set encoding charset of the Msg. func (m *Msg) SetCharset(c Charset) { m.charset = c } -// SetEncoding sets the encoding of the Msg +// SetEncoding sets or overrides the currently set Encoding of the Msg. func (m *Msg) SetEncoding(e Encoding) { m.encoding = e m.setEncoder() } -// SetBoundary sets the boundary of the Msg +// SetBoundary sets or overrides the currently set boundary of the Msg. +// +// Note that by default we create random MIME boundaries. This should only be used if a specific boundary is +// required. func (m *Msg) SetBoundary(b string) { m.boundary = b } -// SetMIMEVersion sets the MIME version of the Msg +// SetMIMEVersion sets or overrides the currently set MIME version of the Msg. +// +// Note that in the context of email, MIME Version 1.0 is the only officially standardized and supported +// version. While MIME has been updated and extended over time (via various RFCs), these updates and extensions +// do not introduce new MIME versions; they refine or add features within the framework of MIME 1.0. +// Therefore there should be no reason to ever use this MsgOption. +// +// https://datatracker.ietf.org/doc/html/rfc1521 +// +// https://datatracker.ietf.org/doc/html/rfc2045 +// +// https://datatracker.ietf.org/doc/html/rfc2049 func (m *Msg) SetMIMEVersion(mv MIMEVersion) { m.mimever = mv } -// SetPGPType sets the PGPType of the Msg +// SetPGPType sets or overrides the currently set PGP type for the Msg, determining the encryption or +// signature method. func (m *Msg) SetPGPType(t PGPType) { m.pgptype = t } -// Encoding returns the currently set encoding of the Msg +// Encoding returns the currently set Encoding of the Msg as string. func (m *Msg) Encoding() string { return m.encoding.String() } -// Charset returns the currently set charset of the Msg +// Charset returns the currently set Charset of the Msg as string. func (m *Msg) Charset() string { return m.charset.String() } -// SetHeader sets a generic header field of the Msg -// For adding address headers like "To:" or "From", see SetAddrHeader +// SetHeader sets a generic header field of the Msg. // -// Deprecated: This method only exists for compatibility reason. Please use SetGenHeader instead +// Deprecated: This method only exists for compatibility reason. Please use SetGenHeader instead. +// For adding address headers like "To:" or "From", use SetAddrHeader instead. func (m *Msg) SetHeader(header Header, values ...string) { m.SetGenHeader(header, values...) } -// SetGenHeader sets a generic header field of the Msg -// For adding address headers like "To:" or "From", see SetAddrHeader +// SetGenHeader sets a generic header field of the Msg to the provided list of values. +// +// Note: for adding email address related headers (like "To:" or "From") use SetAddrHeader instead. func (m *Msg) SetGenHeader(header Header, values ...string) { if m.genHeader == nil { m.genHeader = make(map[Header][]string) @@ -295,26 +313,24 @@ func (m *Msg) SetGenHeader(header Header, values ...string) { m.genHeader[header] = values } -// SetHeaderPreformatted sets a generic header field of the Msg which content is -// already preformated. +// SetHeaderPreformatted sets a generic header field of the Msg, which content is already preformatted. // -// Deprecated: This method only exists for compatibility reason. Please use -// SetGenHeaderPreformatted instead +// Deprecated: This method only exists for compatibility reason. Please use SetGenHeaderPreformatted instead. func (m *Msg) SetHeaderPreformatted(header Header, value string) { m.SetGenHeaderPreformatted(header, value) } -// SetGenHeaderPreformatted sets a generic header field of the Msg which content is -// already preformated. +// SetGenHeaderPreformatted sets a generic header field of the Msg which content is already preformated. // -// This method does not take a slice of values but only a single value. This is -// due to the fact, that we do not perform any content alteration and expect the -// user has already done so +// This method does not take a slice of values but only a single value. The reason for this is that we do not +// perform any content alteration on these kind of headers and expect the user to have already taken care of +// any kind of formatting required for the header. // -// **Please note:** This method should be used only as a last resort. Since the -// user is respondible for the formating of the message header, go-mail cannot -// guarantee the fully compliance with the RFC 2822. It is recommended to use -// SetGenHeader instead. +// Note: This method should be used only as a last resort. Since the user is respondible for the formatting of +// the message header, we cannot guarantee any compliance with the RFC 2822. It is advised to use SetGenHeader +// instead. +// +// https://datatracker.ietf.org/doc/html/rfc2822 func (m *Msg) SetGenHeaderPreformatted(header Header, value string) { if m.preformHeader == nil { m.preformHeader = make(map[Header]string) From 1dcdad9da141e91ca8557f1480abb59e39d14006 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Oct 2024 13:56:47 +0200 Subject: [PATCH 153/181] Enhance address header methods with detailed documentation Updated comments in `header.go` and `msg.go` to provide more detailed explanations and references to RFC 5322. This improves clarity on how headers are set and utilized, and the conditions under which they operate. --- header.go | 7 +++++-- msg.go | 26 +++++++++++++++++++++----- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/header.go b/header.go index d96271a..2b25cd2 100644 --- a/header.go +++ b/header.go @@ -105,8 +105,11 @@ const ( // HeaderCc is the "Carbon Copy" header field. HeaderCc AddrHeader = "Cc" - // HeaderEnvelopeFrom is the envelope FROM header field. It is not included in the mail body but only used by - // the Client for the envelope. + // HeaderEnvelopeFrom is the envelope FROM header field. + // + // It is generally not included in the mail body but only used by the Client for the communication with the + // SMTP server. If the Msg has no "FROM" address set in the mail body, the msgWriter will try to use the + // envelope from address, if this has been set for the Msg. HeaderEnvelopeFrom AddrHeader = "EnvelopeFrom" // HeaderFrom is the "From" header field. diff --git a/msg.go b/msg.go index 995e93f..7e15192 100644 --- a/msg.go +++ b/msg.go @@ -338,7 +338,13 @@ func (m *Msg) SetGenHeaderPreformatted(header Header, value string) { m.preformHeader[header] = value } -// SetAddrHeader sets an address related header field of the Msg +// SetAddrHeader sets the specified AddrHeader for the Msg to the given values. +// +// Addresses are parsed according to RFC 5322. If parsing of ANY of the provided values fails, +// and error is returned. If you cannot guarantee that all provided values are valid, you can +// use SetAddrHeaderIgnoreInvalid instead, which will skip any parsing error silently. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.4 func (m *Msg) SetAddrHeader(header AddrHeader, values ...string) error { if m.addrHeader == nil { m.addrHeader = make(map[AddrHeader][]*mail.Address) @@ -362,8 +368,12 @@ func (m *Msg) SetAddrHeader(header AddrHeader, values ...string) error { return nil } -// SetAddrHeaderIgnoreInvalid sets an address related header field of the Msg and ignores invalid address -// in the validation process +// SetAddrHeaderIgnoreInvalid sets the specified AddrHeader for the Msg to the given values. +// +// Addresses are parsed according to RFC 5322. If parsing of ANY of the provided values fails, +// the error is ignored and the address omiitted from the address list. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.4 func (m *Msg) SetAddrHeaderIgnoreInvalid(header AddrHeader, values ...string) { var addresses []*mail.Address for _, addrVal := range values { @@ -376,8 +386,14 @@ func (m *Msg) SetAddrHeaderIgnoreInvalid(header AddrHeader, values ...string) { m.addrHeader[header] = addresses } -// EnvelopeFrom takes and validates a given mail address and sets it as envelope "FROM" -// addrHeader of the Msg +// EnvelopeFrom sets the envelope from address for the Msg. +// +// The HeaderEnvelopeFrom address is generally not included in the mail body but only used by the Client for the +// communication with the SMTP server. If the Msg has no "FROM" address set in the mail body, the msgWriter will +// try to use the envelope from address, if this has been set for the Msg. The provided address is validated +// according to RFC 5322 and will return an error if the validation fails. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.4 func (m *Msg) EnvelopeFrom(from string) error { return m.SetAddrHeader(HeaderEnvelopeFrom, from) } From 3d5435c138ec4a8e98c1e916f50c01ee33cd26e6 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Oct 2024 13:59:24 +0200 Subject: [PATCH 154/181] Update EnvelopeFromFormat documentation in msg.go Expanded the documentation for the EnvelopeFromFormat method to clarify its purpose, usage, and compliance with RFC 5322. Added details on how the envelope from address is used in SMTP communication and validation requirements. --- msg.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/msg.go b/msg.go index 7e15192..1eb90f2 100644 --- a/msg.go +++ b/msg.go @@ -398,8 +398,14 @@ func (m *Msg) EnvelopeFrom(from string) error { return m.SetAddrHeader(HeaderEnvelopeFrom, from) } -// EnvelopeFromFormat takes a name and address, formats them RFC5322 compliant and stores them as -// the envelope FROM address header field +// EnvelopeFromFormat sets the provided name and mail address as HeaderEnvelopeFrom for the Msg. +// +// The HeaderEnvelopeFrom address is generally not included in the mail body but only used by the Client for the +// communication with the SMTP server. If the Msg has no "FROM" address set in the mail body, the msgWriter will +// try to use the envelope from address, if this has been set for the Msg. The provided name and address adre +// validated according to RFC 5322 and will return an error if the validation fails. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.4 func (m *Msg) EnvelopeFromFormat(name, addr string) error { return m.SetAddrHeader(HeaderEnvelopeFrom, fmt.Sprintf(`"%s" <%s>`, name, addr)) } From c520925457fdd6be78d4ba848cd123277b0dbd84 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Oct 2024 19:11:16 +0200 Subject: [PATCH 155/181] Enhance documentation for email address methods Detailed doc comments have been added to various methods handling "From", "To", "CC", "BCC", and "Reply-To" email addresses within the Msg class. The new comments follow RFC 5322 standards and provide explicit descriptions of the functionality and validation rules for each method. This improves code readability and maintainability. Additionally, moved the `addAddr` function to a more appropriate position within the file. --- msg.go | 214 ++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 174 insertions(+), 40 deletions(-) diff --git a/msg.go b/msg.go index 1eb90f2..7e391ba 100644 --- a/msg.go +++ b/msg.go @@ -410,102 +410,229 @@ func (m *Msg) EnvelopeFromFormat(name, addr string) error { return m.SetAddrHeader(HeaderEnvelopeFrom, fmt.Sprintf(`"%s" <%s>`, name, addr)) } -// From takes and validates a given mail address and sets it as "From" genHeader of the Msg +// From sets the "FROM" address in the mail body for the Msg. +// +// The "FROM" address is included in the mail body and indicates the sender of the message to the recipient. +// This address is visible in the email client and is typically displayed to the recipient. If the "FROM" address +// is not set, the msgWriter may attempt to use the envelope from address (if available) for sending. The provided +// address is validated according to RFC 5322 and will return an error if the validation fails. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 func (m *Msg) From(from string) error { return m.SetAddrHeader(HeaderFrom, from) } -// FromFormat takes a name and address, formats them RFC5322 compliant and stores them as -// the From address header field +// FromFormat sets the provided name and mail address as the "FROM" address in the mail body for the Msg. +// +// The "FROM" address is included in the mail body and indicates the sender of the message to the recipient, +// and is visible in the email client. If the "FROM" address is not explicitly set, the msgWriter may use +// the envelope from address (if provided) when sending the message. The provided name and address are +// validated according to RFC 5322 and will return an error if the validation fails. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 func (m *Msg) FromFormat(name, addr string) error { return m.SetAddrHeader(HeaderFrom, fmt.Sprintf(`"%s" <%s>`, name, addr)) } -// To takes and validates a given mail address list sets the To: addresses of the Msg +// To sets one or more "TO" addresses in the mail body for the Msg. +// +// The "TO" address specifies the primary recipient(s) of the message and is included in the mail body. +// This address is visible to the recipient and any other recipients of the message. Multiple "TO" addresses +// can be set by passing them as variadic arguments to this method. Each provided address is validated +// according to RFC 5322, and an error will be returned if ANY validation fails. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) To(rcpts ...string) error { return m.SetAddrHeader(HeaderTo, rcpts...) } -// AddTo adds an additional address to the To address header field +// AddTo adds a single "TO" address to the existing list of recipients in the mail body for the Msg. +// +// This method allows you to add a single recipient to the "TO" field without replacing any previously set +// "TO" addresses. The "TO" address specifies the primary recipient(s) of the message and is visible in the mail +// client. The provided address is validated according to RFC 5322, and an error will be returned if the +// validation fails. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) AddTo(rcpt string) error { return m.addAddr(HeaderTo, rcpt) } -// AddToFormat takes a name and address, formats them RFC5322 compliant and stores them as -// as additional To address header field +// AddToFormat adds a single "TO" address with the provided name and email to the existing list of recipients +// in the mail body for the Msg. +// +// This method allows you to add a recipient's name and email address to the "TO" field without replacing any +// previously set "TO" addresses. The "TO" address specifies the primary recipient(s) of the message and is +// visible in the mail client. The provided name and address are validated according to RFC 5322, and an error +// will be returned if the validation fails. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) AddToFormat(name, addr string) error { return m.addAddr(HeaderTo, fmt.Sprintf(`"%s" <%s>`, name, addr)) } -// ToIgnoreInvalid takes and validates a given mail address list sets the To: addresses of the Msg -// Any provided address that is not RFC5322 compliant, will be ignored +// ToIgnoreInvalid sets one or more "TO" addresses in the mail body for the Msg, ignoring any invalid addresses. +// +// This method allows you to add multiple "TO" recipients to the message body. Unlike the standard `To` method, +// any invalid addresses are ignored, and no error is returned for those addresses. Valid addresses will still be +// included in the "TO" field, which is visible in the recipient's mail client. Use this method with caution if +// address validation is critical. Invalid addresses are determined according to RFC 5322. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) ToIgnoreInvalid(rcpts ...string) { m.SetAddrHeaderIgnoreInvalid(HeaderTo, rcpts...) } -// ToFromString takes and validates a given string of comma separted -// mail address and sets them as To: addresses of the Msg +// ToFromString takes a string of comma-separated email addresses, validates each, and sets them as the +// "TO" addresses for the Msg. +// +// This method allows you to pass a single string containing multiple email addresses separated by commas. +// Each address is validated according to RFC 5322 and set as a recipient in the "TO" field. If any validation +// fails, an error will be returned. The addresses are visible in the mail body and displayed to recipients in +// the mail client. Any "TO" address applied previously will be overwritten. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) ToFromString(rcpts string) error { return m.To(strings.Split(rcpts, ",")...) } -// Cc takes and validates a given mail address list sets the Cc: addresses of the Msg +// Cc sets one or more "CC" (carbon copy) addresses in the mail body for the Msg. +// +// The "CC" address specifies secondary recipient(s) of the message, and is included in the mail body. +// These addresses are visible to all recipients, including those listed in the "TO" and other "CC" fields. +// Multiple "CC" addresses can be set by passing them as variadic arguments to this method. Each provided +// address is validated according to RFC 5322, and an error will be returned if ANY validation fails. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) Cc(rcpts ...string) error { return m.SetAddrHeader(HeaderCc, rcpts...) } -// AddCc adds an additional address to the Cc address header field +// AddCc adds a single "CC" (carbon copy) address to the existing list of "CC" recipients in the mail body +// for the Msg. +// +// This method allows you to add a single recipient to the "CC" field without replacing any previously set "CC" +// addresses. The "CC" address specifies secondary recipient(s) and is visible to all recipients, including those +// in the "TO" field. The provided address is validated according to RFC 5322, and an error will be returned if +// the validation fails. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) AddCc(rcpt string) error { return m.addAddr(HeaderCc, rcpt) } -// AddCcFormat takes a name and address, formats them RFC5322 compliant and stores them as -// as additional Cc address header field +// AddCcFormat adds a single "CC" (carbon copy) address with the provided name and email to the existing list +// of "CC" recipients in the mail body for the Msg. +// +// This method allows you to add a recipient's name and email address to the "CC" field without replacing any +// previously set "CC" addresses. The "CC" address specifies secondary recipient(s) and is visible to all +// recipients, including those in the "TO" field. The provided name and address are validated according to +// RFC 5322, and an error will be returned if the validation fails. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) AddCcFormat(name, addr string) error { return m.addAddr(HeaderCc, fmt.Sprintf(`"%s" <%s>`, name, addr)) } -// CcIgnoreInvalid takes and validates a given mail address list sets the Cc: addresses of the Msg -// Any provided address that is not RFC5322 compliant, will be ignored +// CcIgnoreInvalid sets one or more "CC" (carbon copy) addresses in the mail body for the Msg, ignoring any +// invalid addresses. +// +// This method allows you to add multiple "CC" recipients to the message body. Unlike the standard `Cc` method, +// any invalid addresses are ignored, and no error is returned for those addresses. Valid addresses will still +// be included in the "CC" field, which is visible to all recipients in the mail client. Use this method with +// caution if address validation is critical, as invalid addresses are determined according to RFC 5322. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) CcIgnoreInvalid(rcpts ...string) { m.SetAddrHeaderIgnoreInvalid(HeaderCc, rcpts...) } -// CcFromString takes and validates a given string of comma separted -// mail address and sets them as Cc: addresses of the Msg +// CcFromString takes a string of comma-separated email addresses, validates each, and sets them as the "CC" +// addresses for the Msg. +// +// This method allows you to pass a single string containing multiple email addresses separated by commas. +// Each address is validated according to RFC 5322 and set as a recipient in the "CC" field. If any validation +// fails, an error will be returned. The addresses are visible in the mail body and displayed to recipients +// in the mail client. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) CcFromString(rcpts string) error { return m.Cc(strings.Split(rcpts, ",")...) } -// Bcc takes and validates a given mail address list sets the Bcc: addresses of the Msg +// Bcc sets one or more "BCC" (blind carbon copy) addresses in the mail body for the Msg. +// +// The "BCC" address specifies recipient(s) of the message who will receive a copy without other recipients +// being aware of it. These addresses are not visible in the mail body or to any other recipients, ensuring +// the privacy of BCC'd recipients. Multiple "BCC" addresses can be set by passing them as variadic arguments +// to this method. Each provided address is validated according to RFC 5322, and an error will be returned +// if ANY validation fails. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) Bcc(rcpts ...string) error { return m.SetAddrHeader(HeaderBcc, rcpts...) } -// AddBcc adds an additional address to the Bcc address header field +// AddBcc adds a single "BCC" (blind carbon copy) address to the existing list of "BCC" recipients in the mail +// body for the Msg. +// +// This method allows you to add a single recipient to the "BCC" field without replacing any previously set +// "BCC" addresses. The "BCC" address specifies recipient(s) of the message who will receive a copy without other +// recipients being aware of it. The provided address is validated according to RFC 5322, and an error will be +// returned if the validation fails. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) AddBcc(rcpt string) error { return m.addAddr(HeaderBcc, rcpt) } -// AddBccFormat takes a name and address, formats them RFC5322 compliant and stores them as -// as additional Bcc address header field +// AddBccFormat adds a single "BCC" (blind carbon copy) address with the provided name and email to the existing +// list of "BCC" recipients in the mail body for the Msg. +// +// This method allows you to add a recipient's name and email address to the "BCC" field without replacing +// any previously set "BCC" addresses. The "BCC" address specifies recipient(s) of the message who will receive +// a copy without other recipients being aware of it. The provided name and address are validated according to +// RFC 5322, and an error will be returned if the validation fails. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) AddBccFormat(name, addr string) error { return m.addAddr(HeaderBcc, fmt.Sprintf(`"%s" <%s>`, name, addr)) } -// BccIgnoreInvalid takes and validates a given mail address list sets the Bcc: addresses of the Msg -// Any provided address that is not RFC5322 compliant, will be ignored +// BccIgnoreInvalid sets one or more "BCC" (blind carbon copy) addresses in the mail body for the Msg, +// ignoring any invalid addresses. +// +// This method allows you to add multiple "BCC" recipients to the message body. Unlike the standard `Bcc` +// method, any invalid addresses are ignored, and no error is returned for those addresses. Valid addresses +// will still be included in the "BCC" field, which ensures the privacy of the BCC'd recipients. Use this method +// with caution if address validation is critical, as invalid addresses are determined according to RFC 5322. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) BccIgnoreInvalid(rcpts ...string) { m.SetAddrHeaderIgnoreInvalid(HeaderBcc, rcpts...) } -// BccFromString takes and validates a given string of comma separted -// mail address and sets them as Bcc: addresses of the Msg +// BccFromString takes a string of comma-separated email addresses, validates each, and sets them as the "BCC" +// addresses for the Msg. +// +// This method allows you to pass a single string containing multiple email addresses separated by commas. +// Each address is validated according to RFC 5322 and set as a recipient in the "BCC" field. If any validation +// fails, an error will be returned. The addresses are not visible in the mail body and ensure the privacy of +// BCC'd recipients. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) BccFromString(rcpts string) error { return m.Bcc(strings.Split(rcpts, ",")...) } -// ReplyTo takes and validates a given mail address and sets it as "Reply-To" addrHeader of the Msg +// ReplyTo sets the "Reply-To" address for the Msg, specifying where replies should be sent. +// +// This method takes a single email address as input and attempts to parse it. If the address is valid, it sets +// the "Reply-To" header in the message. The "Reply-To" address can be different from the "From" address, +// allowing the sender to specify an alternate address for responses. If the provided address cannot be parsed, +// an error will be returned, indicating the parsing failure. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 func (m *Msg) ReplyTo(addr string) error { replyTo, err := mail.ParseAddress(addr) if err != nil { @@ -515,22 +642,19 @@ func (m *Msg) ReplyTo(addr string) error { return nil } -// ReplyToFormat takes a name and address, formats them RFC5322 compliant and stores them as -// the Reply-To header field +// ReplyToFormat sets the "Reply-To" address for the Msg using the provided name and email address, specifying +// where replies should be sent. +// +// This method formats the name and email address into a single "Reply-To" header. If the formatted address is valid, +// it sets the "Reply-To" header in the message. This allows the sender to specify a display name along with the +// reply address, providing clarity for recipients. If the constructed address cannot be parsed, an error will +// be returned, indicating the parsing failure. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 func (m *Msg) ReplyToFormat(name, addr string) error { return m.ReplyTo(fmt.Sprintf(`"%s" <%s>`, name, addr)) } -// addAddr adds an additional address to the given addrHeader of the Msg -func (m *Msg) addAddr(header AddrHeader, addr string) error { - var addresses []string - for _, address := range m.addrHeader[header] { - addresses = append(addresses, address.String()) - } - addresses = append(addresses, addr) - return m.SetAddrHeader(header, addresses...) -} - // Subject sets the "Subject" header field of the Msg func (m *Msg) Subject(subj string) { m.SetGenHeader(HeaderSubject, subj) @@ -1230,6 +1354,16 @@ func (m *Msg) SendError() error { return m.sendError } +// addAddr adds an additional address to the given addrHeader of the Msg +func (m *Msg) addAddr(header AddrHeader, addr string) error { + var addresses []string + for _, address := range m.addrHeader[header] { + addresses = append(addresses, address.String()) + } + addresses = append(addresses, addr) + return m.SetAddrHeader(header, addresses...) +} + // encodeString encodes a string based on the configured message encoder and the corresponding // charset for the Msg func (m *Msg) encodeString(str string) string { From b37f8995dab89d1f5aa5ba7c6b0cac855f873cb4 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Oct 2024 19:34:37 +0200 Subject: [PATCH 156/181] Update Msg documentation for clarity and RFC compliance Enhanced the documentation for several Msg methods to provide clearer explanations and include relevant RFC references. This includes improved descriptions of functionality, parameter details, return values, and links to pertinent RFC sections. --- msg.go | 210 ++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 179 insertions(+), 31 deletions(-) diff --git a/msg.go b/msg.go index 7e391ba..5d29233 100644 --- a/msg.go +++ b/msg.go @@ -655,12 +655,26 @@ func (m *Msg) ReplyToFormat(name, addr string) error { return m.ReplyTo(fmt.Sprintf(`"%s" <%s>`, name, addr)) } -// Subject sets the "Subject" header field of the Msg +// Subject sets the "Subject" header for the Msg, specifying the topic of the message. +// +// This method takes a single string as input and sets it as the "Subject" of the email. The subject line provides +// a brief summary of the content of the message, allowing recipients to quickly understand its purpose. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.5 func (m *Msg) Subject(subj string) { m.SetGenHeader(HeaderSubject, subj) } -// SetMessageID generates a random message id for the mail +// SetMessageID generates and sets a unique "Message-ID" header for the Msg. +// +// This method creates a "Message-ID" string using the current process ID, random numbers, and the hostname +// of the machine. The generated ID helps uniquely identify the message in email systems, facilitating tracking +// and preventing duplication. If the hostname cannot be retrieved, it defaults to "localhost.localdomain". +// +// The generated Message-ID follows the format +// "". +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.4 func (m *Msg) SetMessageID() { hostname, err := os.Hostname() if err != nil { @@ -675,8 +689,14 @@ func (m *Msg) SetMessageID() { m.SetMessageIDWithValue(messageID) } -// GetMessageID returns the message ID of the Msg as string value. If no message ID -// is set, an empty string will be returned +// GetMessageID retrieves the "Message-ID" header from the Msg. +// +// This method checks if a "Message-ID" has been set in the message's generated headers. If a valid "Message-ID" +// exists in the Msg, it returns the first occurrence of the header. If the "Message-ID" has not been set or +// is empty, it returns an empty string. This allows other components to access the unique identifier for the +// message, which is useful for tracking and referencing in email systems. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.4 func (m *Msg) GetMessageID() string { if msgidheader, ok := m.genHeader[HeaderMessageID]; ok { if len(msgidheader) > 0 { @@ -686,32 +706,67 @@ func (m *Msg) GetMessageID() string { return "" } -// SetMessageIDWithValue sets the message id for the mail +// SetMessageIDWithValue sets the "Message-ID" header for the Msg using the provided messageID string. +// +// This method formats the input messageID by enclosing it in angle brackets ("<>") and sets it as the "Message-ID" +// header in the message. The "Message-ID" is a unique identifier for the email, helping email clients and servers +// to track and reference the message. There are no validations performed on the input messageID, so it should +// be in a suitable format for use as a Message-ID. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.4 func (m *Msg) SetMessageIDWithValue(messageID string) { m.SetGenHeader(HeaderMessageID, fmt.Sprintf("<%s>", messageID)) } -// SetBulk sets the "Precedence: bulk" and "X-Auto-Response-Suppress: All" genHeaders which are -// recommended for automated mails like OOO replies -// See: https://www.rfc-editor.org/rfc/rfc2076#section-3.9 -// See also: https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcmail/ced68690-498a-4567-9d14-5c01f974d8b1#Appendix_A_Target_51 +// SetBulk sets the "Precedence: bulk" and "X-Auto-Response-Suppress: All" headers for the Msg, +// which are recommended for automated emails such as out-of-office replies. +// +// The "Precedence: bulk" header indicates that the message is a bulk email, and the "X-Auto-Response-Suppress: All" +// header instructs mail servers and clients to suppress automatic responses to this message. +// This is particularly useful for reducing unnecessary replies to automated notifications or replies. +// For further details, refer to RFC 2076, Section 3.9, and Microsoft's documentation on +// handling automated emails. +// +// https://www.rfc-editor.org/rfc/rfc2076#section-3.9 +// +// https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcmail/ced68690-498a-4567-9d14-5c01f974d8b1#Appendix_A_Target_51 func (m *Msg) SetBulk() { m.SetGenHeader(HeaderPrecedence, "bulk") m.SetGenHeader(HeaderXAutoResponseSuppress, "All") } -// SetDate sets the Date genHeader field to the current time in a valid format +// SetDate sets the "Date" header for the Msg to the current time in a valid RFC 1123 format. +// +// This method retrieves the current time and formats it according to RFC 1123, ensuring that the "Date" +// header is compliant with email standards. The "Date" header indicates when the message was created, +// providing recipients with context for the timing of the email. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.3 func (m *Msg) SetDate() { now := time.Now().Format(time.RFC1123Z) m.SetGenHeader(HeaderDate, now) } -// SetDateWithValue sets the Date genHeader field to the provided time in a valid format +// SetDateWithValue sets the "Date" header for the Msg using the provided time value in a valid RFC 1123 format. +// +// This method takes a `time.Time` value as input and formats it according to RFC 1123, ensuring that the "Date" +// header is compliant with email standards. The "Date" header indicates when the message was created, +// providing recipients with context for the timing of the email. This allows for setting a custom date +// rather than using the current time. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.3 func (m *Msg) SetDateWithValue(timeVal time.Time) { m.SetGenHeader(HeaderDate, timeVal.Format(time.RFC1123Z)) } -// SetImportance sets the Msg Importance/Priority header to given Importance +// SetImportance sets the "Importance" and "Priority" headers for the Msg to the specified Importance level. +// +// This method adjusts the email's importance based on the provided Importance value. If the importance level +// is set to `ImportanceNormal`, no headers are modified. Otherwise, it sets the "Importance", "Priority", +// "X-Priority", and "X-MSMail-Priority" headers accordingly, providing email clients with information on +// how to prioritize the message. This allows the sender to indicate the significance of the email to recipients. +// +// https://datatracker.ietf.org/doc/html/rfc2156 func (m *Msg) SetImportance(importance Importance) { if importance == ImportanceNormal { return @@ -722,26 +777,48 @@ func (m *Msg) SetImportance(importance Importance) { m.SetGenHeader(HeaderXMSMailPriority, importance.NumString()) } -// SetOrganization sets the provided string as Organization header for the Msg +// SetOrganization sets the "Organization" header for the Msg to the specified organization string. +// +// This method allows you to specify the organization associated with the email sender. The "Organization" +// header provides recipients with information about the organization that is sending the message. +// This can help establish context and credibility for the email communication. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.4 func (m *Msg) SetOrganization(org string) { m.SetGenHeader(HeaderOrganization, org) } -// SetUserAgent sets the User-Agent/X-Mailer header for the Msg +// SetUserAgent sets the "User-Agent" and "X-Mailer" headers for the Msg to the specified user agent string. +// +// This method allows you to specify the user agent or mailer software used to send the email. +// The "User-Agent" and "X-Mailer" headers provide recipients with information about the email client +// or application that generated the message. This can be useful for identifying the source of the email, +// particularly for troubleshooting or filtering purposes. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.7 func (m *Msg) SetUserAgent(userAgent string) { m.SetGenHeader(HeaderUserAgent, userAgent) m.SetGenHeader(HeaderXMailer, userAgent) } -// IsDelivered will return true if the Msg has been successfully delivered +// IsDelivered indicates whether the Msg has been delivered. +// +// This method checks the internal state of the message to determine if it has been successfully +// delivered. It returns true if the message is marked as delivered and false otherwise. +// This can be useful for tracking the status of the email communication. func (m *Msg) IsDelivered() bool { return m.isDelivered } -// RequestMDNTo adds the Disposition-Notification-To header to request a MDN from the receiving end -// as described in RFC8098. It allows to provide a list recipient addresses. -// Address validation is performed -// See: https://www.rfc-editor.org/rfc/rfc8098.html +// RequestMDNTo adds the "Disposition-Notification-To" header to the Msg to request a Message Disposition +// Notification (MDN) from the receiving end, as specified in RFC 8098. +// +// This method allows you to provide a list of recipient addresses to receive the MDN. +// Each address is validated according to RFC 5322 standards. If ANY address is invalid, an error +// will be returned indicating the parsing failure. If the "Disposition-Notification-To" header +// is already set, it will be updated with the new list of addresses. +// +// https://datatracker.ietf.org/doc/html/rfc8098 func (m *Msg) RequestMDNTo(rcpts ...string) error { var addresses []string for _, addrVal := range rcpts { @@ -757,15 +834,26 @@ func (m *Msg) RequestMDNTo(rcpts ...string) error { return nil } -// RequestMDNToFormat adds the Disposition-Notification-To header to request a MDN from the receiving end -// as described in RFC8098. It allows to provide a recipient address with name and address and will format -// accordingly. Address validation is performed -// See: https://www.rfc-editor.org/rfc/rfc8098.html +// RequestMDNToFormat adds the "Disposition-Notification-To" header to the Msg to request a Message Disposition +// Notification (MDN) from the receiving end, as specified in RFC 8098. +// +// This method allows you to provide a recipient address along with a name, formatting it appropriately. +// Address validation is performed according to RFC 5322 standards. If the provided address is invalid, +// an error will be returned. This method internally calls RequestMDNTo to handle the actual setting of the header. +// +// https://datatracker.ietf.org/doc/html/rfc8098 func (m *Msg) RequestMDNToFormat(name, addr string) error { return m.RequestMDNTo(fmt.Sprintf(`%s <%s>`, name, addr)) } -// RequestMDNAddTo adds an additional recipient to the recipient list of the MDN +// RequestMDNAddTo adds an additional recipient to the "Disposition-Notification-To" header for the Msg. +// +// This method allows you to append a new recipient address to the existing list of recipients for the +// MDN. The provided address is validated according to RFC 5322 standards. If the address is invalid, +// an error will be returned indicating the parsing failure. If the "Disposition-Notification-To" +// header is already set, the new recipient will be added to the existing list. +// +// https://datatracker.ietf.org/doc/html/rfc8098 func (m *Msg) RequestMDNAddTo(rcpt string) error { address, err := mail.ParseAddress(rcpt) if err != nil { @@ -780,14 +868,35 @@ func (m *Msg) RequestMDNAddTo(rcpt string) error { return nil } -// RequestMDNAddToFormat adds an additional formated recipient to the recipient list of the MDN +// RequestMDNAddToFormat adds an additional formatted recipient to the "Disposition-Notification-To" +// header for the Msg. +// +// This method allows you to specify a recipient address along with a name, formatting it appropriately +// before adding it to the existing list of recipients for the MDN. The formatted address is validated +// according to RFC 5322 standards. If the provided address is invalid, an error will be returned. +// This method internally calls RequestMDNAddTo to handle the actual addition of the recipient. +// +// https://datatracker.ietf.org/doc/html/rfc8098 func (m *Msg) RequestMDNAddToFormat(name, addr string) error { return m.RequestMDNAddTo(fmt.Sprintf(`"%s" <%s>`, name, addr)) } -// GetSender returns the currently set envelope FROM address. If no envelope FROM is set it will use -// the first mail body FROM address. If useFullAddr is true, it will return the full address string -// including the address name, if set +// GetSender returns the currently set envelope "FROM" address for the Msg. If no envelope +// "FROM" address is set, it will use the first "FROM" address from the mail body. If the +// useFullAddr parameter is true, it will return the full address string, including the name +// if it is set. +// +// If neither the envelope "FROM" nor the body "FROM" addresses are available, it will return +// an error indicating that no "FROM" address is present. +// +// Parameters: +// - useFullAddr: A boolean indicating whether to return the full address string (including +// the name) or just the email address. +// +// Returns: +// - The sender's address as a string and an error if applicable. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 func (m *Msg) GetSender(useFullAddr bool) (string, error) { from, ok := m.addrHeader[HeaderEnvelopeFrom] if !ok || len(from) == 0 { @@ -802,7 +911,18 @@ func (m *Msg) GetSender(useFullAddr bool) (string, error) { return from[0].Address, nil } -// GetRecipients returns a list of the currently set TO/CC/BCC addresses. +// GetRecipients returns a list of the currently set "TO", "CC", and "BCC" addresses for the Msg. +// +// This method aggregates recipients from the "TO", "CC", and "BCC" headers and returns them as a +// slice of strings. If no recipients are found in these headers, it will return an error indicating +// that no recipient addresses are present. +// +// Returns: +// - A slice of strings containing the recipients' addresses and an error if applicable. +// - If there are no recipient addresses set, it will return an error indicating no recipient +// addresses are available. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) GetRecipients() ([]string, error) { var rcpts []string for _, addressType := range []AddrHeader{HeaderTo, HeaderCc, HeaderBcc} { @@ -820,12 +940,40 @@ func (m *Msg) GetRecipients() ([]string, error) { return rcpts, nil } -// GetAddrHeader returns the content of the requested address header of the Msg +// GetAddrHeader returns the content of the requested address header for the Msg. +// +// This method retrieves the addresses associated with the specified address header. It returns a +// slice of pointers to mail.Address structures representing the addresses found in the header. +// If the requested header does not exist or contains no addresses, it will return nil. +// +// Parameters: +// - header: The AddrHeader enum value indicating which address header to retrieve (e.g., "TO", +// "CC", "BCC", etc.). +// +// Returns: +// - A slice of pointers to mail.Address structures containing the addresses from the specified +// header. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6 func (m *Msg) GetAddrHeader(header AddrHeader) []*mail.Address { return m.addrHeader[header] } -// GetAddrHeaderString returns the address string of the requested address header of the Msg +// GetAddrHeaderString returns the address strings of the requested address header for the Msg. +// +// This method retrieves the addresses associated with the specified address header and returns them +// as a slice of strings. Each address is formatted as a string, which includes both the name (if +// available) and the email address. If the requested header does not exist or contains no addresses, +// it will return an empty slice. +// +// Parameters: +// - header: The AddrHeader enum value indicating which address header to retrieve (e.g., "TO", +// "CC", "BCC", etc.). +// +// Returns: +// - A slice of strings containing the formatted addresses from the specified header. +// +// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6 func (m *Msg) GetAddrHeaderString(header AddrHeader) []string { var addresses []string for _, mh := range m.addrHeader[header] { From 4890d9130b1b7b674fe7ed723a2b49c8b6454c9d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Oct 2024 20:06:51 +0200 Subject: [PATCH 157/181] Expand docstrings for MsgOption functions and methods. This commit enhances the docstrings for the MsgOption functions and related methods in msg.go, providing extensive explanations, parameters, and references. It helps users understand the functionality, usage, and context of each function, improving code readability and usability. --- msg.go | 415 +++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 318 insertions(+), 97 deletions(-) diff --git a/msg.go b/msg.go index 5d29233..af0e1a2 100644 --- a/msg.go +++ b/msg.go @@ -150,8 +150,22 @@ const SendmailPath = "/usr/sbin/sendmail" // MsgOption is a function type that modifies a Msg instance during its creation or initialization. type MsgOption func(*Msg) -// NewMsg creates a new email message with optional MsgOption functions that customize various aspects of the -// message. +// NewMsg creates a new email message with optional MsgOption functions that customize various aspects +// of the message. +// +// This function initializes a new Msg instance with default values for address headers, character set, +// encoding, general headers, and MIME version. It then applies any provided MsgOption functions to +// customize the message according to the user's needs. If an option is nil, it will be ignored. +// After applying the options, the function sets the appropriate MIME WordEncoder for the message. +// +// Parameters: +// - opts: A variadic list of MsgOption functions that can be used to customize the Msg instance. +// +// Returns: +// - A pointer to the newly created Msg instance. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5321 func NewMsg(opts ...MsgOption) *Msg { msg := &Msg{ addrHeader: make(map[AddrHeader][]*mail.Address), @@ -177,64 +191,140 @@ func NewMsg(opts ...MsgOption) *Msg { } // WithCharset sets the Charset type for a Msg during its creation or initialization. -func WithCharset(c Charset) MsgOption { +// +// This MsgOption function allows you to specify the character set to be used in the email message. +// The charset defines how the text in the message is encoded and interpreted by the email client. +// This option should be called when creating a new Msg instance to ensure that the desired charset +// is set correctly. +// +// Parameters: +// - charset: The Charset value that specifies the desired character set for the Msg. +// +// Returns: +// - A MsgOption function that can be used to customize the Msg instance. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2047#section-5 +func WithCharset(charset Charset) MsgOption { return func(m *Msg) { - m.charset = c + m.charset = charset } } // WithEncoding sets the Encoding type for a Msg during its creation or initialization. -func WithEncoding(e Encoding) MsgOption { +// +// This MsgOption function allows you to specify the encoding type to be used in the email message. +// The encoding defines how the message content is encoded, which affects how it is transmitted +// and decoded by email clients. This option should be called when creating a new Msg instance to +// ensure that the desired encoding is set correctly. +// +// Parameters: +// - encoding: The Encoding value that specifies the desired encoding type for the Msg. +// +// Returns: +// - A MsgOption function that can be used to customize the Msg instance. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2047#section-6 +func WithEncoding(encoding Encoding) MsgOption { return func(m *Msg) { - m.encoding = e + m.encoding = encoding } } // WithMIMEVersion sets the MIMEVersion type for a Msg during its creation or initialization. // -// Note that in the context of email, MIME Version 1.0 is the only officially standardized and supported -// version. While MIME has been updated and extended over time (via various RFCs), these updates and extensions -// do not introduce new MIME versions; they refine or add features within the framework of MIME 1.0. -// Therefore there should be no reason to ever use this MsgOption. -// https://datatracker.ietf.org/doc/html/rfc1521 +// Note that in the context of email, MIME Version 1.0 is the only officially standardized and +// supported version. While MIME has been updated and extended over time via various RFCs, these +// updates and extensions do not introduce new MIME versions; they refine or add features within +// the framework of MIME 1.0. Therefore, there should be no reason to ever use this MsgOption. // -// https://datatracker.ietf.org/doc/html/rfc2045 +// Parameters: +// - version: The MIMEVersion value that specifies the desired MIME version for the Msg. // -// https://datatracker.ietf.org/doc/html/rfc2049 -func WithMIMEVersion(mv MIMEVersion) MsgOption { - return func(m *Msg) { - m.mimever = mv - } -} - -// WithBoundary sets the boundary of a Msg to the provided string value during its creation or initialization. +// Returns: +// - A MsgOption function that can be used to customize the Msg instance. // -// Note that by default we create random MIME boundaries. This should only be used if a specific boundary is -// required. -func WithBoundary(b string) MsgOption { +// References: +// - https://datatracker.ietf.org/doc/html/rfc1521 +// - https://datatracker.ietf.org/doc/html/rfc2045 +// - https://datatracker.ietf.org/doc/html/rfc2049 +func WithMIMEVersion(version MIMEVersion) MsgOption { return func(m *Msg) { - m.boundary = b + m.mimever = version } } -// WithMiddleware adds the given Middleware to the end of the list of the Client middlewares slice. Middleware -// are processed in FIFO order. -func WithMiddleware(mw Middleware) MsgOption { - return func(m *Msg) { - m.middlewares = append(m.middlewares, mw) - } -} - -// WithPGPType sets the PGP type for the Msg during its creation or initialization, determining the encryption or -// signature method. -func WithPGPType(pt PGPType) MsgOption { - return func(m *Msg) { - m.pgptype = pt - } -} - -// WithNoDefaultUserAgent disables the inclusion of a default User-Agent header in the Msg during its creation or +// WithBoundary sets the boundary of a Msg to the provided string value during its creation or // initialization. +// +// Note that by default, random MIME boundaries are created. This option should only be used if +// a specific boundary is required for the email message. Using a predefined boundary can be +// helpful when constructing multipart messages with specific formatting or content separation. +// +// Parameters: +// - boundary: The string value that specifies the desired boundary for the Msg. +// +// Returns: +// - A MsgOption function that can be used to customize the Msg instance. +func WithBoundary(boundary string) MsgOption { + return func(m *Msg) { + m.boundary = boundary + } +} + +// WithMiddleware adds the given Middleware to the end of the list of the Client middlewares slice. +// Middleware are processed in FIFO order. +// +// This MsgOption function allows you to specify custom middleware that will be applied during the +// message handling process. Middleware can be used to modify the message, perform logging, or +// implement additional functionality as the message flows through the system. Each middleware +// is executed in the order it was added. +// +// Parameters: +// - middleware: The Middleware to be added to the list for processing. +// +// Returns: +// - A MsgOption function that can be used to customize the Msg instance. +func WithMiddleware(middleware Middleware) MsgOption { + return func(m *Msg) { + m.middlewares = append(m.middlewares, middleware) + } +} + +// WithPGPType sets the PGP type for the Msg during its creation or initialization, determining +// the encryption or signature method. +// +// This MsgOption function allows you to specify the PGP (Pretty Good Privacy) type to be used +// for securing the message. The chosen PGP type influences how the message is encrypted or +// signed, ensuring confidentiality and integrity of the content. This option should be called +// when creating a new Msg instance to set the desired PGP type appropriately. +// +// Parameters: +// - pgptype: The PGPType value that specifies the desired PGP type for the Msg. +// +// Returns: +// - A MsgOption function that can be used to customize the Msg instance. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc4880 +func WithPGPType(pgptype PGPType) MsgOption { + return func(m *Msg) { + m.pgptype = pgptype + } +} + +// WithNoDefaultUserAgent disables the inclusion of a default User-Agent header in the Msg during +// its creation or initialization. +// +// This MsgOption function allows you to customize the Msg instance by omitting the default +// User-Agent header, which is typically included to provide information about the software +// sending the email. This option can be useful when you want to have more control over the +// headers included in the message, such as when sending from a custom application or for +// privacy reasons. +// +// Returns: +// - A MsgOption function that can be used to customize the Msg instance. func WithNoDefaultUserAgent() MsgOption { return func(m *Msg) { m.noDefaultUserAgent = true @@ -242,67 +332,150 @@ func WithNoDefaultUserAgent() MsgOption { } // SetCharset sets or overrides the currently set encoding charset of the Msg. -func (m *Msg) SetCharset(c Charset) { - m.charset = c +// +// This method allows you to specify a character set for the email message. The charset is +// important for ensuring that the content of the message is correctly interpreted by +// mail clients. Common charset values include UTF-8, ISO-8859-1, and others. If a charset +// is not explicitly set, CharsetUTF8 is used as default. +// +// Parameters: +// - charset: The Charset value to set for the Msg, determining the encoding used for the message content. +func (m *Msg) SetCharset(charset Charset) { + m.charset = charset } // SetEncoding sets or overrides the currently set Encoding of the Msg. -func (m *Msg) SetEncoding(e Encoding) { - m.encoding = e +// +// This method allows you to specify the encoding type for the email message. The encoding +// determines how the message content is represented and can affect the size and compatibility +// of the email. Common encoding types include Base64 and Quoted-Printable. Setting a new +// encoding may also adjust how the message content is processed and transmitted. +// +// Parameters: +// - encoding: The Encoding value to set for the Msg, determining the method used to encode the +// message content. +func (m *Msg) SetEncoding(encoding Encoding) { + m.encoding = encoding m.setEncoder() } // SetBoundary sets or overrides the currently set boundary of the Msg. // -// Note that by default we create random MIME boundaries. This should only be used if a specific boundary is -// required. -func (m *Msg) SetBoundary(b string) { - m.boundary = b +// This method allows you to specify a custom boundary string for the MIME message. The +// boundary is used to separate different parts of the message, especially when dealing +// with multipart messages. By default, the Msg generates random MIME boundaries. This +// function should only be used if you have a specific boundary requirement for the +// message. Ensure that the boundary value does not conflict with any content within the +// message to avoid parsing errors. +// +// Parameters: +// - boundary: The string value representing the boundary to set for the Msg, used in +// multipart messages to delimit different sections. +func (m *Msg) SetBoundary(boundary string) { + m.boundary = boundary } // SetMIMEVersion sets or overrides the currently set MIME version of the Msg. // -// Note that in the context of email, MIME Version 1.0 is the only officially standardized and supported -// version. While MIME has been updated and extended over time (via various RFCs), these updates and extensions -// do not introduce new MIME versions; they refine or add features within the framework of MIME 1.0. -// Therefore there should be no reason to ever use this MsgOption. +// In the context of email, MIME Version 1.0 is the only officially standardized and +// supported version. Although MIME has been updated and extended over time through +// various RFCs, these updates do not introduce new MIME versions; they refine or add +// features within the framework of MIME 1.0. Therefore, there is generally no need to +// use this function to set a different MIME version. // -// https://datatracker.ietf.org/doc/html/rfc1521 +// Parameters: +// - version: The MIMEVersion value to set for the Msg, which determines the MIME +// version used in the email message. // -// https://datatracker.ietf.org/doc/html/rfc2045 -// -// https://datatracker.ietf.org/doc/html/rfc2049 -func (m *Msg) SetMIMEVersion(mv MIMEVersion) { - m.mimever = mv +// References: +// - https://datatracker.ietf.org/doc/html/rfc1521 +// - https://datatracker.ietf.org/doc/html/rfc2045 +// - https://datatracker.ietf.org/doc/html/rfc2049 +func (m *Msg) SetMIMEVersion(version MIMEVersion) { + m.mimever = version } -// SetPGPType sets or overrides the currently set PGP type for the Msg, determining the encryption or -// signature method. -func (m *Msg) SetPGPType(t PGPType) { - m.pgptype = t +// SetPGPType sets or overrides the currently set PGP type for the Msg, determining the +// encryption or signature method. +// +// This method allows you to specify the PGP type that will be used when encrypting or +// signing the message. Different PGP types correspond to various encryption and signing +// algorithms, and selecting the appropriate type is essential for ensuring the security +// and integrity of the message content. +// +// Parameters: +// - pgptype: The PGPType value to set for the Msg, which determines the encryption +// or signature method used for the email message. +func (m *Msg) SetPGPType(pgptype PGPType) { + m.pgptype = pgptype } -// Encoding returns the currently set Encoding of the Msg as string. +// Encoding returns the currently set Encoding of the Msg as a string. +// +// This method retrieves the encoding type that is currently applied to the message. The +// encoding type determines how the message content is encoded for transmission. Common +// encoding types include quoted-printable and base64, and the returned string will reflect +// the specific encoding method in use. +// +// Returns: +// - A string representation of the current Encoding of the Msg. func (m *Msg) Encoding() string { return m.encoding.String() } -// Charset returns the currently set Charset of the Msg as string. +// Charset returns the currently set Charset of the Msg as a string. +// +// This method retrieves the character set that is currently applied to the message. The +// charset defines the encoding for the text content of the message, ensuring that +// characters are displayed correctly across different email clients and platforms. The +// returned string will reflect the specific charset in use, such as UTF-8 or ISO-8859-1. +// +// Returns: +// - A string representation of the current Charset of the Msg. func (m *Msg) Charset() string { return m.charset.String() } // SetHeader sets a generic header field of the Msg. // -// Deprecated: This method only exists for compatibility reason. Please use SetGenHeader instead. -// For adding address headers like "To:" or "From", use SetAddrHeader instead. +// Deprecated: This method only exists for compatibility reasons. Please use SetGenHeader +// instead. For adding address headers like "To:" or "From", use SetAddrHeader instead. +// +// This method allows you to set a header field for the message, providing the header name +// and its corresponding values. However, it is recommended to utilize the newer methods +// for better clarity and functionality. Using SetGenHeader or SetAddrHeader is preferred +// for more specific header types, ensuring proper handling of the message headers. +// +// Parameters: +// - header: The header field to set in the Msg. +// - values: One or more string values to associate with the header field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3 +// - https://datatracker.ietf.org/doc/html/rfc2047 func (m *Msg) SetHeader(header Header, values ...string) { m.SetGenHeader(header, values...) } // SetGenHeader sets a generic header field of the Msg to the provided list of values. // -// Note: for adding email address related headers (like "To:" or "From") use SetAddrHeader instead. +// This method is intended for setting generic headers in the email message. It takes a +// header name and a variadic list of string values, encoding them as necessary before +// storing them in the message's internal header map. +// +// Note: For adding email address-related headers (like "To:", "From", "Cc", etc.), +// use SetAddrHeader instead to ensure proper formatting and validation. +// +// Parameters: +// - header: The header field to set in the Msg. +// - values: One or more string values to associate with the header field. +// +// This method ensures that all values are appropriately encoded for email transmission, +// adhering to the necessary standards. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3 +// - https://datatracker.ietf.org/doc/html/rfc2047 func (m *Msg) SetGenHeader(header Header, values ...string) { if m.genHeader == nil { m.genHeader = make(map[Header][]string) @@ -315,22 +488,36 @@ func (m *Msg) SetGenHeader(header Header, values ...string) { // SetHeaderPreformatted sets a generic header field of the Msg, which content is already preformatted. // -// Deprecated: This method only exists for compatibility reason. Please use SetGenHeaderPreformatted instead. +// Deprecated: This method only exists for compatibility reasons. Please use +// SetGenHeaderPreformatted instead for setting preformatted generic header fields. +// +// Parameters: +// - header: The header field to set in the Msg. +// - value: The preformatted string value to associate with the header field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3 +// - https://datatracker.ietf.org/doc/html/rfc2047 func (m *Msg) SetHeaderPreformatted(header Header, value string) { m.SetGenHeaderPreformatted(header, value) } -// SetGenHeaderPreformatted sets a generic header field of the Msg which content is already preformated. +// SetGenHeaderPreformatted sets a generic header field of the Msg which content is already preformatted. // // This method does not take a slice of values but only a single value. The reason for this is that we do not -// perform any content alteration on these kind of headers and expect the user to have already taken care of +// perform any content alteration on these kinds of headers and expect the user to have already taken care of // any kind of formatting required for the header. // -// Note: This method should be used only as a last resort. Since the user is respondible for the formatting of -// the message header, we cannot guarantee any compliance with the RFC 2822. It is advised to use SetGenHeader -// instead. +// Note: This method should be used only as a last resort. Since the user is responsible for the formatting of +// the message header, we cannot guarantee any compliance with RFC 2822. It is advised to use SetGenHeader +// instead for general header fields. // -// https://datatracker.ietf.org/doc/html/rfc2822 +// Parameters: +// - header: The header field to set in the Msg. +// - value: The preformatted string value to associate with the header field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2822 func (m *Msg) SetGenHeaderPreformatted(header Header, value string) { if m.preformHeader == nil { m.preformHeader = make(map[Header]string) @@ -340,11 +527,21 @@ func (m *Msg) SetGenHeaderPreformatted(header Header, value string) { // SetAddrHeader sets the specified AddrHeader for the Msg to the given values. // -// Addresses are parsed according to RFC 5322. If parsing of ANY of the provided values fails, -// and error is returned. If you cannot guarantee that all provided values are valid, you can -// use SetAddrHeaderIgnoreInvalid instead, which will skip any parsing error silently. +// Addresses are parsed according to RFC 5322. If parsing any of the provided values fails, +// an error is returned. If you cannot guarantee that all provided values are valid, you can +// use SetAddrHeaderIgnoreInvalid instead, which will silently skip any parsing errors. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.4 +// This method allows you to set address-related headers for the message, ensuring that the +// provided addresses are properly formatted and parsed. Using this method helps maintain the +// integrity of the email addresses within the message. +// +// Parameters: +// - header: The AddrHeader to set in the Msg (e.g., "From", "To", "Cc", "Bcc"). +// - values: One or more string values representing the email addresses to associate with +// the specified header. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.4 func (m *Msg) SetAddrHeader(header AddrHeader, values ...string) error { if m.addrHeader == nil { m.addrHeader = make(map[AddrHeader][]*mail.Address) @@ -370,10 +567,19 @@ func (m *Msg) SetAddrHeader(header AddrHeader, values ...string) error { // SetAddrHeaderIgnoreInvalid sets the specified AddrHeader for the Msg to the given values. // -// Addresses are parsed according to RFC 5322. If parsing of ANY of the provided values fails, -// the error is ignored and the address omiitted from the address list. +// Addresses are parsed according to RFC 5322. If parsing of any of the provided values fails, +// the error is ignored and the address is omitted from the address list. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.4 +// This method allows for setting address headers while ignoring invalid addresses. It is useful +// in scenarios where you want to ensure that only valid addresses are included without halting +// execution due to parsing errors. +// +// Parameters: +// - header: The AddrHeader field to set in the Msg. +// - values: One or more string values representing email addresses. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.4 func (m *Msg) SetAddrHeaderIgnoreInvalid(header AddrHeader, values ...string) { var addresses []*mail.Address for _, addrVal := range values { @@ -388,36 +594,51 @@ func (m *Msg) SetAddrHeaderIgnoreInvalid(header AddrHeader, values ...string) { // EnvelopeFrom sets the envelope from address for the Msg. // -// The HeaderEnvelopeFrom address is generally not included in the mail body but only used by the Client for the -// communication with the SMTP server. If the Msg has no "FROM" address set in the mail body, the msgWriter will -// try to use the envelope from address, if this has been set for the Msg. The provided address is validated -// according to RFC 5322 and will return an error if the validation fails. +// The HeaderEnvelopeFrom address is generally not included in the mail body but only used by the +// Client for communication with the SMTP server. If the Msg has no "FROM" address set in the +// mail body, the msgWriter will try to use the envelope from address if it has been set for the Msg. +// The provided address is validated according to RFC 5322 and will return an error if the validation fails. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.4 +// Parameters: +// - from: The envelope from address to set in the Msg. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.4 func (m *Msg) EnvelopeFrom(from string) error { return m.SetAddrHeader(HeaderEnvelopeFrom, from) } // EnvelopeFromFormat sets the provided name and mail address as HeaderEnvelopeFrom for the Msg. // -// The HeaderEnvelopeFrom address is generally not included in the mail body but only used by the Client for the -// communication with the SMTP server. If the Msg has no "FROM" address set in the mail body, the msgWriter will -// try to use the envelope from address, if this has been set for the Msg. The provided name and address adre -// validated according to RFC 5322 and will return an error if the validation fails. +// The HeaderEnvelopeFrom address is generally not included in the mail body but only used by the +// Client for communication with the SMTP server. If the Msg has no "FROM" address set in the mail +// body, the msgWriter will try to use the envelope from address if it has been set for the Msg. +// The provided name and address are validated according to RFC 5322 and will return an error if +// the validation fails. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.4 +// Parameters: +// - name: The name to associate with the envelope from address. +// - addr: The mail address to set as the envelope from address. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.4 func (m *Msg) EnvelopeFromFormat(name, addr string) error { return m.SetAddrHeader(HeaderEnvelopeFrom, fmt.Sprintf(`"%s" <%s>`, name, addr)) } // From sets the "FROM" address in the mail body for the Msg. // -// The "FROM" address is included in the mail body and indicates the sender of the message to the recipient. -// This address is visible in the email client and is typically displayed to the recipient. If the "FROM" address -// is not set, the msgWriter may attempt to use the envelope from address (if available) for sending. The provided -// address is validated according to RFC 5322 and will return an error if the validation fails. +// The "FROM" address is included in the mail body and indicates the sender of the message to +// the recipient. This address is visible in the email client and is typically displayed to the +// recipient. If the "FROM" address is not set, the msgWriter may attempt to use the envelope +// from address (if available) for sending. The provided address is validated according to RFC +// 5322 and will return an error if the validation fails. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 +// Parameters: +// - from: The "FROM" address to set in the mail body. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 func (m *Msg) From(from string) error { return m.SetAddrHeader(HeaderFrom, from) } From 864c5932083f2712d653d8655622b23fc9bcf388 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 5 Oct 2024 20:29:33 +0200 Subject: [PATCH 158/181] Add parameters and references to method comments in msg.go Enhanced method documentation by adding parameter descriptions and reference links. This improves the clarity and usability of the code, ensuring that users understand the purpose and usage of each method. --- msg.go | 153 ++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 120 insertions(+), 33 deletions(-) diff --git a/msg.go b/msg.go index af0e1a2..2f1a614 100644 --- a/msg.go +++ b/msg.go @@ -645,12 +645,18 @@ func (m *Msg) From(from string) error { // FromFormat sets the provided name and mail address as the "FROM" address in the mail body for the Msg. // -// The "FROM" address is included in the mail body and indicates the sender of the message to the recipient, -// and is visible in the email client. If the "FROM" address is not explicitly set, the msgWriter may use -// the envelope from address (if provided) when sending the message. The provided name and address are -// validated according to RFC 5322 and will return an error if the validation fails. +// The "FROM" address is included in the mail body and indicates the sender of the message to +// the recipient, and is visible in the email client. If the "FROM" address is not explicitly +// set, the msgWriter may use the envelope from address (if provided) when sending the message. +// The provided name and address are validated according to RFC 5322 and will return an error +// if the validation fails. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 +// Parameters: +// - name: The name of the sender to include in the "FROM" address. +// - addr: The email address of the sender to include in the "FROM" address. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 func (m *Msg) FromFormat(name, addr string) error { return m.SetAddrHeader(HeaderFrom, fmt.Sprintf(`"%s" <%s>`, name, addr)) } @@ -662,7 +668,11 @@ func (m *Msg) FromFormat(name, addr string) error { // can be set by passing them as variadic arguments to this method. Each provided address is validated // according to RFC 5322, and an error will be returned if ANY validation fails. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 +// Parameters: +// - rcpts: One or more recipient email addresses to include in the "TO" field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) To(rcpts ...string) error { return m.SetAddrHeader(HeaderTo, rcpts...) } @@ -674,7 +684,11 @@ func (m *Msg) To(rcpts ...string) error { // client. The provided address is validated according to RFC 5322, and an error will be returned if the // validation fails. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 +// Parameters: +// - rcpt: The recipient email address to add to the "TO" field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) AddTo(rcpt string) error { return m.addAddr(HeaderTo, rcpt) } @@ -687,7 +701,12 @@ func (m *Msg) AddTo(rcpt string) error { // visible in the mail client. The provided name and address are validated according to RFC 5322, and an error // will be returned if the validation fails. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 +// Parameters: +// - name: The name of the recipient to add to the "TO" field. +// - addr: The email address of the recipient to add to the "TO" field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) AddToFormat(name, addr string) error { return m.addAddr(HeaderTo, fmt.Sprintf(`"%s" <%s>`, name, addr)) } @@ -699,7 +718,11 @@ func (m *Msg) AddToFormat(name, addr string) error { // included in the "TO" field, which is visible in the recipient's mail client. Use this method with caution if // address validation is critical. Invalid addresses are determined according to RFC 5322. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 +// Parameters: +// - rcpts: One or more recipient addresses to add to the "TO" field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) ToIgnoreInvalid(rcpts ...string) { m.SetAddrHeaderIgnoreInvalid(HeaderTo, rcpts...) } @@ -712,7 +735,11 @@ func (m *Msg) ToIgnoreInvalid(rcpts ...string) { // fails, an error will be returned. The addresses are visible in the mail body and displayed to recipients in // the mail client. Any "TO" address applied previously will be overwritten. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 +// Parameters: +// - rcpts: A string containing multiple recipient addresses separated by commas. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) ToFromString(rcpts string) error { return m.To(strings.Split(rcpts, ",")...) } @@ -724,7 +751,11 @@ func (m *Msg) ToFromString(rcpts string) error { // Multiple "CC" addresses can be set by passing them as variadic arguments to this method. Each provided // address is validated according to RFC 5322, and an error will be returned if ANY validation fails. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 +// Parameters: +// - rcpts: One or more recipient addresses to be included in the "CC" field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) Cc(rcpts ...string) error { return m.SetAddrHeader(HeaderCc, rcpts...) } @@ -737,7 +768,11 @@ func (m *Msg) Cc(rcpts ...string) error { // in the "TO" field. The provided address is validated according to RFC 5322, and an error will be returned if // the validation fails. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 +// Parameters: +// - rcpt: The recipient address to be added to the "CC" field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) AddCc(rcpt string) error { return m.addAddr(HeaderCc, rcpt) } @@ -750,7 +785,12 @@ func (m *Msg) AddCc(rcpt string) error { // recipients, including those in the "TO" field. The provided name and address are validated according to // RFC 5322, and an error will be returned if the validation fails. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 +// Parameters: +// - name: The name of the recipient to be added to the "CC" field. +// - addr: The email address of the recipient to be added to the "CC" field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) AddCcFormat(name, addr string) error { return m.addAddr(HeaderCc, fmt.Sprintf(`"%s" <%s>`, name, addr)) } @@ -763,7 +803,11 @@ func (m *Msg) AddCcFormat(name, addr string) error { // be included in the "CC" field, which is visible to all recipients in the mail client. Use this method with // caution if address validation is critical, as invalid addresses are determined according to RFC 5322. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 +// Parameters: +// - rcpts: One or more recipient email addresses to be added to the "CC" field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) CcIgnoreInvalid(rcpts ...string) { m.SetAddrHeaderIgnoreInvalid(HeaderCc, rcpts...) } @@ -776,7 +820,11 @@ func (m *Msg) CcIgnoreInvalid(rcpts ...string) { // fails, an error will be returned. The addresses are visible in the mail body and displayed to recipients // in the mail client. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 +// Parameters: +// - rcpts: A string containing multiple email addresses separated by commas. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) CcFromString(rcpts string) error { return m.Cc(strings.Split(rcpts, ",")...) } @@ -789,7 +837,11 @@ func (m *Msg) CcFromString(rcpts string) error { // to this method. Each provided address is validated according to RFC 5322, and an error will be returned // if ANY validation fails. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 +// Parameters: +// - rcpts: One or more string values representing the BCC addresses to set in the Msg. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) Bcc(rcpts ...string) error { return m.SetAddrHeader(HeaderBcc, rcpts...) } @@ -802,7 +854,11 @@ func (m *Msg) Bcc(rcpts ...string) error { // recipients being aware of it. The provided address is validated according to RFC 5322, and an error will be // returned if the validation fails. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 +// Parameters: +// - rcpt: The BCC address to add to the existing list of recipients in the Msg. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) AddBcc(rcpt string) error { return m.addAddr(HeaderBcc, rcpt) } @@ -815,7 +871,12 @@ func (m *Msg) AddBcc(rcpt string) error { // a copy without other recipients being aware of it. The provided name and address are validated according to // RFC 5322, and an error will be returned if the validation fails. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 +// Parameters: +// - name: The name of the recipient to add to the BCC field. +// - addr: The email address of the recipient to add to the BCC field. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) AddBccFormat(name, addr string) error { return m.addAddr(HeaderBcc, fmt.Sprintf(`"%s" <%s>`, name, addr)) } @@ -828,7 +889,11 @@ func (m *Msg) AddBccFormat(name, addr string) error { // will still be included in the "BCC" field, which ensures the privacy of the BCC'd recipients. Use this method // with caution if address validation is critical, as invalid addresses are determined according to RFC 5322. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 +// Parameters: +// - rcpts: One or more string values representing the BCC email addresses to set. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) BccIgnoreInvalid(rcpts ...string) { m.SetAddrHeaderIgnoreInvalid(HeaderBcc, rcpts...) } @@ -841,7 +906,11 @@ func (m *Msg) BccIgnoreInvalid(rcpts ...string) { // fails, an error will be returned. The addresses are not visible in the mail body and ensure the privacy of // BCC'd recipients. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 +// Parameters: +// - rcpts: A string of comma-separated email addresses to set as BCC recipients. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) BccFromString(rcpts string) error { return m.Bcc(strings.Split(rcpts, ",")...) } @@ -853,7 +922,11 @@ func (m *Msg) BccFromString(rcpts string) error { // allowing the sender to specify an alternate address for responses. If the provided address cannot be parsed, // an error will be returned, indicating the parsing failure. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 +// Parameters: +// - addr: The email address to set as the "Reply-To" address. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 func (m *Msg) ReplyTo(addr string) error { replyTo, err := mail.ParseAddress(addr) if err != nil { @@ -871,7 +944,12 @@ func (m *Msg) ReplyTo(addr string) error { // reply address, providing clarity for recipients. If the constructed address cannot be parsed, an error will // be returned, indicating the parsing failure. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 +// Parameters: +// - name: The display name associated with the reply address. +// - addr: The email address to set as the "Reply-To" address. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 func (m *Msg) ReplyToFormat(name, addr string) error { return m.ReplyTo(fmt.Sprintf(`"%s" <%s>`, name, addr)) } @@ -881,7 +959,11 @@ func (m *Msg) ReplyToFormat(name, addr string) error { // This method takes a single string as input and sets it as the "Subject" of the email. The subject line provides // a brief summary of the content of the message, allowing recipients to quickly understand its purpose. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.5 +// Parameters: +// - subj: The subject line of the email. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.5 func (m *Msg) Subject(subj string) { m.SetGenHeader(HeaderSubject, subj) } @@ -895,7 +977,8 @@ func (m *Msg) Subject(subj string) { // The generated Message-ID follows the format // "". // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.4 +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.4 func (m *Msg) SetMessageID() { hostname, err := os.Hostname() if err != nil { @@ -917,7 +1000,8 @@ func (m *Msg) SetMessageID() { // is empty, it returns an empty string. This allows other components to access the unique identifier for the // message, which is useful for tracking and referencing in email systems. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.4 +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.4 func (m *Msg) GetMessageID() string { if msgidheader, ok := m.genHeader[HeaderMessageID]; ok { if len(msgidheader) > 0 { @@ -934,7 +1018,8 @@ func (m *Msg) GetMessageID() string { // to track and reference the message. There are no validations performed on the input messageID, so it should // be in a suitable format for use as a Message-ID. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.4 +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.4 func (m *Msg) SetMessageIDWithValue(messageID string) { m.SetGenHeader(HeaderMessageID, fmt.Sprintf("<%s>", messageID)) } @@ -945,12 +1030,10 @@ func (m *Msg) SetMessageIDWithValue(messageID string) { // The "Precedence: bulk" header indicates that the message is a bulk email, and the "X-Auto-Response-Suppress: All" // header instructs mail servers and clients to suppress automatic responses to this message. // This is particularly useful for reducing unnecessary replies to automated notifications or replies. -// For further details, refer to RFC 2076, Section 3.9, and Microsoft's documentation on -// handling automated emails. // -// https://www.rfc-editor.org/rfc/rfc2076#section-3.9 -// -// https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcmail/ced68690-498a-4567-9d14-5c01f974d8b1#Appendix_A_Target_51 +// References: +// - https://www.rfc-editor.org/rfc/rfc2076#section-3.9 +// - https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcmail/ced68690-498a-4567-9d14-5c01f974d8b1#Appendix_A_Target_51 func (m *Msg) SetBulk() { m.SetGenHeader(HeaderPrecedence, "bulk") m.SetGenHeader(HeaderXAutoResponseSuppress, "All") @@ -962,7 +1045,9 @@ func (m *Msg) SetBulk() { // header is compliant with email standards. The "Date" header indicates when the message was created, // providing recipients with context for the timing of the email. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.3 +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.3 +// - https://datatracker.ietf.org/doc/html/rfc1123 func (m *Msg) SetDate() { now := time.Now().Format(time.RFC1123Z) m.SetGenHeader(HeaderDate, now) @@ -975,7 +1060,9 @@ func (m *Msg) SetDate() { // providing recipients with context for the timing of the email. This allows for setting a custom date // rather than using the current time. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.3 +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.3 +// - https://datatracker.ietf.org/doc/html/rfc1123 func (m *Msg) SetDateWithValue(timeVal time.Time) { m.SetGenHeader(HeaderDate, timeVal.Format(time.RFC1123Z)) } From eafb9cb17ef8c8035ae73e43139c51d13edf10ae Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 6 Oct 2024 00:25:15 +0200 Subject: [PATCH 159/181] Add detailed parameter and return descriptions to msg.go Enhanced function documentation by adding detailed descriptions of parameters and return values for various methods. This clarifies expected inputs and outputs, improving code readability and maintainability. --- msg.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/msg.go b/msg.go index 2f1a614..9da94b8 100644 --- a/msg.go +++ b/msg.go @@ -48,9 +48,11 @@ const ( // NoPGP indicates that a message should not be treated as PGP encrypted or signed and is the default value // for a message NoPGP PGPType = iota + // PGPEncrypt indicates that a message should be treated as PGP encrypted. This works closely together with // the corresponding go-mail-middleware. PGPEncrypt + // PGPSignature indicates that a message should be treated as PGP signed. This works closely together with // the corresponding go-mail-middleware. PGPSignature @@ -1018,6 +1020,9 @@ func (m *Msg) GetMessageID() string { // to track and reference the message. There are no validations performed on the input messageID, so it should // be in a suitable format for use as a Message-ID. // +// Parameters: +// - messageID: The string to set as the "Message-ID" in the message header. +// // References: // - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.4 func (m *Msg) SetMessageIDWithValue(messageID string) { @@ -1060,6 +1065,9 @@ func (m *Msg) SetDate() { // providing recipients with context for the timing of the email. This allows for setting a custom date // rather than using the current time. // +// Parameters: +// - timeVal: The time value used to set the "Date" header. +// // References: // - https://datatracker.ietf.org/doc/html/rfc5322#section-3.3 // - https://datatracker.ietf.org/doc/html/rfc1123 @@ -1074,7 +1082,11 @@ func (m *Msg) SetDateWithValue(timeVal time.Time) { // "X-Priority", and "X-MSMail-Priority" headers accordingly, providing email clients with information on // how to prioritize the message. This allows the sender to indicate the significance of the email to recipients. // -// https://datatracker.ietf.org/doc/html/rfc2156 +// Parameters: +// - importance: The Importance value that determines the priority of the email message. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2156 func (m *Msg) SetImportance(importance Importance) { if importance == ImportanceNormal { return @@ -1091,7 +1103,11 @@ func (m *Msg) SetImportance(importance Importance) { // header provides recipients with information about the organization that is sending the message. // This can help establish context and credibility for the email communication. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.4 +// Parameters: +// - org: The name of the organization to be set in the "Organization" header. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.4 func (m *Msg) SetOrganization(org string) { m.SetGenHeader(HeaderOrganization, org) } @@ -1103,7 +1119,11 @@ func (m *Msg) SetOrganization(org string) { // or application that generated the message. This can be useful for identifying the source of the email, // particularly for troubleshooting or filtering purposes. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.7 +// Parameters: +// - userAgent: The user agent or mailer software to be set in the "User-Agent" and "X-Mailer" headers. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.7 func (m *Msg) SetUserAgent(userAgent string) { m.SetGenHeader(HeaderUserAgent, userAgent) m.SetGenHeader(HeaderXMailer, userAgent) @@ -1114,6 +1134,9 @@ func (m *Msg) SetUserAgent(userAgent string) { // This method checks the internal state of the message to determine if it has been successfully // delivered. It returns true if the message is marked as delivered and false otherwise. // This can be useful for tracking the status of the email communication. +// +// Returns: +// - A boolean value indicating the delivery status of the message (true if delivered, false otherwise). func (m *Msg) IsDelivered() bool { return m.isDelivered } @@ -1126,7 +1149,11 @@ func (m *Msg) IsDelivered() bool { // will be returned indicating the parsing failure. If the "Disposition-Notification-To" header // is already set, it will be updated with the new list of addresses. // -// https://datatracker.ietf.org/doc/html/rfc8098 +// Parameters: +// - rcpts: One or more recipient email addresses to request the MDN from. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc8098 func (m *Msg) RequestMDNTo(rcpts ...string) error { var addresses []string for _, addrVal := range rcpts { @@ -1149,7 +1176,12 @@ func (m *Msg) RequestMDNTo(rcpts ...string) error { // Address validation is performed according to RFC 5322 standards. If the provided address is invalid, // an error will be returned. This method internally calls RequestMDNTo to handle the actual setting of the header. // -// https://datatracker.ietf.org/doc/html/rfc8098 +// Parameters: +// - name: The name of the recipient for the MDN request. +// - addr: The email address of the recipient for the MDN request. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc8098 func (m *Msg) RequestMDNToFormat(name, addr string) error { return m.RequestMDNTo(fmt.Sprintf(`%s <%s>`, name, addr)) } @@ -1161,7 +1193,11 @@ func (m *Msg) RequestMDNToFormat(name, addr string) error { // an error will be returned indicating the parsing failure. If the "Disposition-Notification-To" // header is already set, the new recipient will be added to the existing list. // -// https://datatracker.ietf.org/doc/html/rfc8098 +// Parameters: +// - rcpt: The recipient email address to add to the "Disposition-Notification-To" header. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc8098 func (m *Msg) RequestMDNAddTo(rcpt string) error { address, err := mail.ParseAddress(rcpt) if err != nil { @@ -1184,7 +1220,12 @@ func (m *Msg) RequestMDNAddTo(rcpt string) error { // according to RFC 5322 standards. If the provided address is invalid, an error will be returned. // This method internally calls RequestMDNAddTo to handle the actual addition of the recipient. // -// https://datatracker.ietf.org/doc/html/rfc8098 +// Parameters: +// - name: The name of the recipient to add to the "Disposition-Notification-To" header. +// - addr: The email address of the recipient to add to the "Disposition-Notification-To" header. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc8098 func (m *Msg) RequestMDNAddToFormat(name, addr string) error { return m.RequestMDNAddTo(fmt.Sprintf(`"%s" <%s>`, name, addr)) } @@ -1204,7 +1245,8 @@ func (m *Msg) RequestMDNAddToFormat(name, addr string) error { // Returns: // - The sender's address as a string and an error if applicable. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 func (m *Msg) GetSender(useFullAddr bool) (string, error) { from, ok := m.addrHeader[HeaderEnvelopeFrom] if !ok || len(from) == 0 { @@ -1230,7 +1272,8 @@ func (m *Msg) GetSender(useFullAddr bool) (string, error) { // - If there are no recipient addresses set, it will return an error indicating no recipient // addresses are available. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) GetRecipients() ([]string, error) { var rcpts []string for _, addressType := range []AddrHeader{HeaderTo, HeaderCc, HeaderBcc} { @@ -1262,7 +1305,8 @@ func (m *Msg) GetRecipients() ([]string, error) { // - A slice of pointers to mail.Address structures containing the addresses from the specified // header. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6 +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6 func (m *Msg) GetAddrHeader(header AddrHeader) []*mail.Address { return m.addrHeader[header] } @@ -1281,7 +1325,8 @@ func (m *Msg) GetAddrHeader(header AddrHeader) []*mail.Address { // Returns: // - A slice of strings containing the formatted addresses from the specified header. // -// https://datatracker.ietf.org/doc/html/rfc5322#section-3.6 +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6 func (m *Msg) GetAddrHeaderString(header AddrHeader) []string { var addresses []string for _, mh := range m.addrHeader[header] { From 01278ccb30ae247ca7e6914eee70b66d3460d917 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 6 Oct 2024 00:49:09 +0200 Subject: [PATCH 160/181] Document message methods Added detailed comments to various methods in `msg.go` for clarity. Each method now includes a declaration, a detailed description, returned values, parameters, and relevant RFC references. This enhances code readability and maintainability. --- msg.go | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 229 insertions(+), 21 deletions(-) diff --git a/msg.go b/msg.go index 9da94b8..7dac30d 100644 --- a/msg.go +++ b/msg.go @@ -1335,67 +1335,179 @@ func (m *Msg) GetAddrHeaderString(header AddrHeader) []string { return addresses } -// GetFrom returns the content of the From address header of the Msg +// GetFrom returns the content of the "From" address header of the Msg. +// +// This method retrieves the list of email addresses set in the "From" header of the message. +// It returns a slice of pointers to `mail.Address` objects representing the sender(s) of the email. +// +// Returns: +// - A slice of `*mail.Address` containing the "From" header addresses. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 func (m *Msg) GetFrom() []*mail.Address { return m.GetAddrHeader(HeaderFrom) } -// GetFromString returns the content of the From address header of the Msg as string slice +// GetFromString returns the content of the "From" address header of the Msg as a string slice. +// +// This method retrieves the list of email addresses set in the "From" header of the message +// and returns them as a slice of strings, with each entry representing a formatted email address. +// +// Returns: +// - A slice of strings containing the "From" header addresses. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 func (m *Msg) GetFromString() []string { return m.GetAddrHeaderString(HeaderFrom) } -// GetTo returns the content of the To address header of the Msg +// GetTo returns the content of the "To" address header of the Msg. +// +// This method retrieves the list of email addresses set in the "To" header of the message. +// It returns a slice of pointers to `mail.Address` objects representing the primary recipient(s) of the email. +// +// Returns: +// - A slice of `*mail.Address` containing the "To" header addresses. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) GetTo() []*mail.Address { return m.GetAddrHeader(HeaderTo) } -// GetToString returns the content of the To address header of the Msg as string slice +// GetToString returns the content of the "To" address header of the Msg as a string slice. +// +// This method retrieves the list of email addresses set in the "To" header of the message +// and returns them as a slice of strings, with each entry representing a formatted email address. +// +// Returns: +// - A slice of strings containing the "To" header addresses. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) GetToString() []string { return m.GetAddrHeaderString(HeaderTo) } -// GetCc returns the content of the Cc address header of the Msg +// GetCc returns the content of the "Cc" address header of the Msg. +// +// This method retrieves the list of email addresses set in the "Cc" (carbon copy) header of the message. +// It returns a slice of pointers to `mail.Address` objects representing the secondary recipient(s) of the email. +// +// Returns: +// - A slice of `*mail.Address` containing the "Cc" header addresses. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) GetCc() []*mail.Address { return m.GetAddrHeader(HeaderCc) } -// GetCcString returns the content of the Cc address header of the Msg as string slice +// GetCcString returns the content of the "Cc" address header of the Msg as a string slice. +// +// This method retrieves the list of email addresses set in the "Cc" (carbon copy) header of the message +// and returns them as a slice of strings, with each entry representing a formatted email address. +// +// Returns: +// - A slice of strings containing the "Cc" header addresses. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) GetCcString() []string { return m.GetAddrHeaderString(HeaderCc) } -// GetBcc returns the content of the Bcc address header of the Msg +// GetBcc returns the content of the "Bcc" address header of the Msg. +// +// This method retrieves the list of email addresses set in the "Bcc" (blind carbon copy) header of the message. +// It returns a slice of pointers to `mail.Address` objects representing the Bcc recipient(s) of the email. +// +// Returns: +// - A slice of `*mail.Address` containing the "Bcc" header addresses. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) GetBcc() []*mail.Address { return m.GetAddrHeader(HeaderBcc) } -// GetBccString returns the content of the Bcc address header of the Msg as string slice +// GetBccString returns the content of the "Bcc" address header of the Msg as a string slice. +// +// This method retrieves the list of email addresses set in the "Bcc" (blind carbon copy) header of the message +// and returns them as a slice of strings, with each entry representing a formatted email address. +// +// Returns: +// - A slice of strings containing the "Bcc" header addresses. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3 func (m *Msg) GetBccString() []string { return m.GetAddrHeaderString(HeaderBcc) } -// GetGenHeader returns the content of the requested generic header of the Msg +// GetGenHeader returns the content of the requested generic header of the Msg. +// +// This method retrieves the list of string values associated with the specified generic header of the message. +// It returns a slice of strings representing the header's values. +// +// Parameters: +// - header: The Header field whose values are being retrieved. +// +// Returns: +// - A slice of strings containing the values of the specified generic header. func (m *Msg) GetGenHeader(header Header) []string { return m.genHeader[header] } -// GetParts returns the message parts of the Msg +// GetParts returns the message parts of the Msg. +// +// This method retrieves the list of parts that make up the email message. Each part may represent +// a different section of the email, such as a plain text body, HTML body, or attachments. +// +// Returns: +// - A slice of Part pointers representing the message parts of the email. func (m *Msg) GetParts() []*Part { return m.parts } -// GetAttachments returns the attachments of the Msg +// GetAttachments returns the attachments of the Msg. +// +// This method retrieves the list of files that have been attached to the email message. +// Each attachment includes details about the file, such as its name, content type, and data. +// +// Returns: +// - A slice of File pointers representing the attachments of the email. func (m *Msg) GetAttachments() []*File { return m.attachments } -// GetBoundary returns the boundary of the Msg +// GetBoundary returns the boundary of the Msg. +// +// This method retrieves the MIME boundary that is used to separate different parts of the message, +// particularly in multipart emails. The boundary helps to differentiate between various sections +// such as plain text, HTML content, and attachments. +// +// Returns: +// - A string representing the boundary of the message. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2046#section-5.1.1 func (m *Msg) GetBoundary() string { return m.boundary } // SetAttachments sets the attachments of the message. +// +// This method allows you to specify the attachments for the message by providing a slice of File pointers. +// Each file represents an attachment that will be included in the email. +// +// Parameters: +// - files: A slice of pointers to File structures representing the attachments to set for the message. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) SetAttachments(files []*File) { m.attachments = files } @@ -1407,33 +1519,83 @@ func (m *Msg) SetAttachements(files []*File) { m.SetAttachments(files) } -// UnsetAllAttachments unset the attachments of the message. +// UnsetAllAttachments unsets the attachments of the message. +// +// This method removes all attachments from the message by setting the attachments to nil, effectively +// clearing any previously set attachments. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) UnsetAllAttachments() { m.attachments = nil } -// GetEmbeds returns the embeds of the Msg +// GetEmbeds returns the embedded files of the Msg. +// +// This method retrieves the list of files that have been embedded in the message. Embeds are typically +// images or other media files that are referenced directly in the content of the email, such as inline +// images in HTML emails. +// +// Returns: +// - A slice of pointers to File structures representing the embedded files in the message. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) GetEmbeds() []*File { return m.embeds } -// SetEmbeds sets the embeds of the message. +// SetEmbeds sets the embedded files of the message. +// +// This method allows you to specify the files to be embedded in the message by providing a slice of File pointers. +// Embedded files, such as images or media, are typically used for inline content in HTML emails. +// +// Parameters: +// - files: A slice of pointers to File structures representing the embedded files to set for the message. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) SetEmbeds(files []*File) { m.embeds = files } -// UnsetAllEmbeds unset the embeds of the message. +// UnsetAllEmbeds unsets the embedded files of the message. +// +// This method removes all embedded files from the message by setting the embeds to nil, effectively +// clearing any previously set embedded files. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) UnsetAllEmbeds() { m.embeds = nil } -// UnsetAllParts unset the embeds and attachments of the message. +// UnsetAllParts unsets the embeds and attachments of the message. +// +// This method removes all embedded files and attachments from the message by unsetting both the +// embeds and attachments, effectively clearing all previously set message parts. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) UnsetAllParts() { m.UnsetAllAttachments() m.UnsetAllEmbeds() } // SetBodyString sets the body of the message. +// +// This method sets the body of the message using the provided content type and string content. The body can +// be set as plain text, HTML, or other formats based on the specified content type. Optional part settings +// can be passed through PartOption to customize the message body further. +// +// Parameters: +// - contentType: The ContentType of the body (e.g., plain text, HTML). +// - content: The string content to set as the body of the message. +// - opts: Optional parameters for customizing the body part. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2045 +// - https://datatracker.ietf.org/doc/html/rfc2046 func (m *Msg) SetBodyString(contentType ContentType, content string, opts ...PartOption) { buffer := bytes.NewBufferString(content) writeFunc := writeFuncFromBuffer(buffer) @@ -1441,6 +1603,20 @@ func (m *Msg) SetBodyString(contentType ContentType, content string, opts ...Par } // SetBodyWriter sets the body of the message. +// +// This method sets the body of the message using a write function, allowing content to be written +// directly to the body. The content type determines the format (e.g., plain text, HTML). +// Optional part settings can be provided via PartOption to customize the body further. +// +// Parameters: +// - contentType: The ContentType of the body (e.g., plain text, HTML). +// - writeFunc: A function that writes content to an io.Writer and returns the number of bytes written +// and an error, if any. +// - opts: Optional parameters for customizing the body part. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2045 +// - https://datatracker.ietf.org/doc/html/rfc2046 func (m *Msg) SetBodyWriter( contentType ContentType, writeFunc func(io.Writer) (int64, error), opts ...PartOption, @@ -1450,8 +1626,24 @@ func (m *Msg) SetBodyWriter( m.parts = []*Part{p} } -// SetBodyHTMLTemplate sets the body of the message from a given html/template.Template pointer -// The content type will be set to text/html automatically +// SetBodyHTMLTemplate sets the body of the message from a given html/template.Template pointer. +// +// This method sets the body of the message using the provided HTML template and data. The content type +// will be set to "text/html" automatically. The method executes the template with the provided data +// and writes the output to the message body. If the template is nil or fails to execute, an error will +// be returned. +// +// Parameters: +// - tpl: A pointer to the html/template.Template to be used for the message body. +// - data: The data to populate the template. +// - opts: Optional parameters for customizing the body part. +// +// Returns: +// - An error if the template is nil or fails to execute, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2045 +// - https://datatracker.ietf.org/doc/html/rfc2046 func (m *Msg) SetBodyHTMLTemplate(tpl *ht.Template, data interface{}, opts ...PartOption) error { if tpl == nil { return errors.New(errTplPointerNil) @@ -1465,8 +1657,24 @@ func (m *Msg) SetBodyHTMLTemplate(tpl *ht.Template, data interface{}, opts ...Pa return nil } -// SetBodyTextTemplate sets the body of the message from a given text/template.Template pointer -// The content type will be set to text/plain automatically +// SetBodyTextTemplate sets the body of the message from a given text/template.Template pointer. +// +// This method sets the body of the message using the provided text template and data. The content type +// will be set to "text/plain" automatically. The method executes the template with the provided data +// and writes the output to the message body. If the template is nil or fails to execute, an error will +// be returned. +// +// Parameters: +// - tpl: A pointer to the text/template.Template to be used for the message body. +// - data: The data to populate the template. +// - opts: Optional parameters for customizing the body part. +// +// Returns: +// - An error if the template is nil or fails to execute, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2045 +// - https://datatracker.ietf.org/doc/html/rfc2046 func (m *Msg) SetBodyTextTemplate(tpl *tt.Template, data interface{}, opts ...PartOption) error { if tpl == nil { return errors.New(errTplPointerNil) From b4197a136e930f86882f4d47337b08c3e452f27a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 6 Oct 2024 12:04:27 +0200 Subject: [PATCH 161/181] Enhance documentation for message methods Expanded docstrings for methods in msg.go to provide detailed explanations, parameters, and return values. This improves clarity and helps developers understand usage and behavior more effectively. --- msg.go | 768 ++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 681 insertions(+), 87 deletions(-) diff --git a/msg.go b/msg.go index 7dac30d..fc0bb57 100644 --- a/msg.go +++ b/msg.go @@ -1689,13 +1689,40 @@ func (m *Msg) SetBodyTextTemplate(tpl *tt.Template, data interface{}, opts ...Pa } // AddAlternativeString sets the alternative body of the message. +// +// This method adds an alternative representation of the message body using the specified content type +// and string content. This is typically used to provide both plain text and HTML versions of the email. +// Optional part settings can be provided via PartOption to further customize the message. +// +// Parameters: +// - contentType: The content type of the alternative body (e.g., plain text, HTML). +// - content: The string content to set as the alternative body. +// - opts: Optional parameters for customizing the alternative body part. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2045 +// - https://datatracker.ietf.org/doc/html/rfc2046 func (m *Msg) AddAlternativeString(contentType ContentType, content string, opts ...PartOption) { buffer := bytes.NewBufferString(content) writeFunc := writeFuncFromBuffer(buffer) m.AddAlternativeWriter(contentType, writeFunc, opts...) } -// AddAlternativeWriter sets the body of the message. +// AddAlternativeWriter sets the alternative body of the message. +// +// This method adds an alternative representation of the message body using a write function, allowing +// content to be written directly to the body. This is typically used to provide different formats, such +// as plain text and HTML. Optional part settings can be provided via PartOption to customize the message part. +// +// Parameters: +// - contentType: The content type of the alternative body (e.g., plain text, HTML). +// - writeFunc: A function that writes content to an io.Writer and returns the number of bytes written and +// an error, if any. +// - opts: Optional parameters for customizing the alternative body part. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2045 +// - https://datatracker.ietf.org/doc/html/rfc2046 func (m *Msg) AddAlternativeWriter( contentType ContentType, writeFunc func(io.Writer) (int64, error), opts ...PartOption, @@ -1705,8 +1732,23 @@ func (m *Msg) AddAlternativeWriter( m.parts = append(m.parts, part) } -// AddAlternativeHTMLTemplate sets the alternative body of the message to a html/template.Template output -// The content type will be set to text/html automatically +// AddAlternativeHTMLTemplate sets the alternative body of the message to an html/template.Template output. +// +// The content type will be set to "text/html" automatically. This method executes the provided HTML template +// with the given data and adds the result as an alternative version of the message body. If the template +// is nil or fails to execute, an error will be returned. +// +// Parameters: +// - tpl: A pointer to the html/template.Template to be used for the alternative body. +// - data: The data to populate the template. +// - opts: Optional parameters for customizing the alternative body part. +// +// Returns: +// - An error if the template is nil or fails to execute, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2045 +// - https://datatracker.ietf.org/doc/html/rfc2046 func (m *Msg) AddAlternativeHTMLTemplate(tpl *ht.Template, data interface{}, opts ...PartOption) error { if tpl == nil { return errors.New(errTplPointerNil) @@ -1720,8 +1762,23 @@ func (m *Msg) AddAlternativeHTMLTemplate(tpl *ht.Template, data interface{}, opt return nil } -// AddAlternativeTextTemplate sets the alternative body of the message to a text/template.Template output -// The content type will be set to text/plain automatically +// AddAlternativeTextTemplate sets the alternative body of the message to a text/template.Template output. +// +// The content type will be set to "text/plain" automatically. This method executes the provided text template +// with the given data and adds the result as an alternative version of the message body. If the template +// is nil or fails to execute, an error will be returned. +// +// Parameters: +// - tpl: A pointer to the text/template.Template to be used for the alternative body. +// - data: The data to populate the template. +// - opts: Optional parameters for customizing the alternative body part. +// +// Returns: +// - An error if the template is nil or fails to execute, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2045 +// - https://datatracker.ietf.org/doc/html/rfc2046 func (m *Msg) AddAlternativeTextTemplate(tpl *tt.Template, data interface{}, opts ...PartOption) error { if tpl == nil { return errors.New(errTplPointerNil) @@ -1735,7 +1792,18 @@ func (m *Msg) AddAlternativeTextTemplate(tpl *tt.Template, data interface{}, opt return nil } -// AttachFile adds an attachment File to the Msg +// AttachFile adds an attachment File to the Msg. +// +// This method attaches a file to the message by specifying the file name. The file is retrieved from the +// filesystem and added to the list of attachments. Optional FileOption parameters can be provided to customize +// the attachment, such as setting its content type or encoding. +// +// Parameters: +// - name: The name of the file to be attached. +// - opts: Optional parameters for customizing the attachment. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) AttachFile(name string, opts ...FileOption) { file := fileFromFS(name) if file == nil { @@ -1744,12 +1812,22 @@ func (m *Msg) AttachFile(name string, opts ...FileOption) { m.attachments = m.appendFile(m.attachments, file, opts...) } -// AttachReader adds an attachment File via io.Reader to the Msg +// AttachReader adds an attachment File via io.Reader to the Msg. // -// CAVEAT: For AttachReader to work it has to read all data of the io.Reader -// into memory first, so it can seek through it. Using larger amounts of -// data on the io.Reader should be avoided. For such, it is recommended to -// either use AttachFile or AttachReadSeeker instead +// This method allows you to attach a file to the message using an io.Reader. It reads all data from the +// io.Reader into memory before attaching the file, which may not be suitable for large data sources. +// For larger files, it is recommended to use AttachFile or AttachReadSeeker instead. +// +// Parameters: +// - name: The name of the file to be attached. +// - reader: The io.Reader providing the file data to be attached. +// - opts: Optional parameters for customizing the attachment. +// +// Returns: +// - An error if the file could not be read from the io.Reader, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) AttachReader(name string, reader io.Reader, opts ...FileOption) error { file, err := fileFromReader(name, reader) if err != nil { @@ -1759,13 +1837,41 @@ func (m *Msg) AttachReader(name string, reader io.Reader, opts ...FileOption) er return nil } -// AttachReadSeeker adds an attachment File via io.ReadSeeker to the Msg +// AttachReadSeeker adds an attachment File via io.ReadSeeker to the Msg. +// +// This method allows you to attach a file to the message using an io.ReadSeeker, which is more efficient +// for larger files compared to AttachReader, as it allows for seeking through the data without needing +// to load the entire content into memory. +// +// Parameters: +// - name: The name of the file to be attached. +// - reader: The io.ReadSeeker providing the file data to be attached. +// - opts: Optional parameters for customizing the attachment. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) AttachReadSeeker(name string, reader io.ReadSeeker, opts ...FileOption) { file := fileFromReadSeeker(name, reader) m.attachments = m.appendFile(m.attachments, file, opts...) } -// AttachHTMLTemplate adds the output of a html/template.Template pointer as File attachment to the Msg +// AttachHTMLTemplate adds the output of a html/template.Template pointer as a File attachment to the Msg. +// +// This method allows you to attach the rendered output of an HTML template as a file to the message. +// The template is executed with the provided data, and its output is attached as a file. If the template +// fails to execute, an error will be returned. +// +// Parameters: +// - name: The name of the file to be attached. +// - tpl: A pointer to the html/template.Template to be executed for the attachment. +// - data: The data to populate the template. +// - opts: Optional parameters for customizing the attachment. +// +// Returns: +// - An error if the template fails to execute or cannot be attached, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) AttachHTMLTemplate( name string, tpl *ht.Template, data interface{}, opts ...FileOption, ) error { @@ -1777,7 +1883,23 @@ func (m *Msg) AttachHTMLTemplate( return nil } -// AttachTextTemplate adds the output of a text/template.Template pointer as File attachment to the Msg +// AttachTextTemplate adds the output of a text/template.Template pointer as a File attachment to the Msg. +// +// This method allows you to attach the rendered output of a text template as a file to the message. +// The template is executed with the provided data, and its output is attached as a file. If the template +// fails to execute, an error will be returned. +// +// Parameters: +// - name: The name of the file to be attached. +// - tpl: A pointer to the text/template.Template to be executed for the attachment. +// - data: The data to populate the template. +// - opts: Optional parameters for customizing the attachment. +// +// Returns: +// - An error if the template fails to execute or cannot be attached, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) AttachTextTemplate( name string, tpl *tt.Template, data interface{}, opts ...FileOption, ) error { @@ -1789,7 +1911,22 @@ func (m *Msg) AttachTextTemplate( return nil } -// AttachFromEmbedFS adds an attachment File from an embed.FS to the Msg +// AttachFromEmbedFS adds an attachment File from an embed.FS to the Msg. +// +// This method allows you to attach a file from an embedded filesystem (embed.FS) to the message. +// The file is retrieved from the provided embed.FS and attached to the email. If the embedded filesystem +// is nil or the file cannot be retrieved, an error will be returned. +// +// Parameters: +// - name: The name of the file to be attached. +// - fs: A pointer to the embed.FS from which the file will be retrieved. +// - opts: Optional parameters for customizing the attachment. +// +// Returns: +// - An error if the embed.FS is nil or the file cannot be retrieved, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) AttachFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) error { if fs == nil { return fmt.Errorf("embed.FS must not be nil") @@ -1802,7 +1939,18 @@ func (m *Msg) AttachFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) e return nil } -// EmbedFile adds an embedded File to the Msg +// EmbedFile adds an embedded File to the Msg. +// +// This method embeds a file from the filesystem directly into the email message. The embedded file, +// typically an image or media file, can be referenced within the email's content (such as inline in HTML). +// If the file is not found or cannot be loaded, it will not be added. +// +// Parameters: +// - name: The name of the file to be embedded. +// - opts: Optional parameters for customizing the embedded file. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) EmbedFile(name string, opts ...FileOption) { file := fileFromFS(name) if file == nil { @@ -1811,12 +1959,22 @@ func (m *Msg) EmbedFile(name string, opts ...FileOption) { m.embeds = m.appendFile(m.embeds, file, opts...) } -// EmbedReader adds an embedded File from an io.Reader to the Msg +// EmbedReader adds an embedded File from an io.Reader to the Msg. // -// CAVEAT: For EmbedReader to work it has to read all data of the io.Reader -// into memory first, so it can seek through it. Using larger amounts of -// data on the io.Reader should be avoided. For such, it is recommended to -// either use EmbedFile or EmbedReadSeeker instead +// This method embeds a file into the email message by reading its content from an io.Reader. +// It reads all data into memory before embedding the file, which may not be efficient for large data sources. +// For larger files, it is recommended to use EmbedFile or EmbedReadSeeker instead. +// +// Parameters: +// - name: The name of the file to be embedded. +// - reader: The io.Reader providing the file data to be embedded. +// - opts: Optional parameters for customizing the embedded file. +// +// Returns: +// - An error if the file could not be read from the io.Reader, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) EmbedReader(name string, reader io.Reader, opts ...FileOption) error { file, err := fileFromReader(name, reader) if err != nil { @@ -1826,13 +1984,41 @@ func (m *Msg) EmbedReader(name string, reader io.Reader, opts ...FileOption) err return nil } -// EmbedReadSeeker adds an embedded File from an io.ReadSeeker to the Msg +// EmbedReadSeeker adds an embedded File from an io.ReadSeeker to the Msg. +// +// This method embeds a file into the email message by reading its content from an io.ReadSeeker. +// Using io.ReadSeeker allows for more efficient handling of large files since it can seek through the data +// without loading the entire content into memory. +// +// Parameters: +// - name: The name of the file to be embedded. +// - reader: The io.ReadSeeker providing the file data to be embedded. +// - opts: Optional parameters for customizing the embedded file. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) EmbedReadSeeker(name string, reader io.ReadSeeker, opts ...FileOption) { file := fileFromReadSeeker(name, reader) m.embeds = m.appendFile(m.embeds, file, opts...) } -// EmbedHTMLTemplate adds the output of a html/template.Template pointer as embedded File to the Msg +// EmbedHTMLTemplate adds the output of a html/template.Template pointer as an embedded File to the Msg. +// +// This method embeds the rendered output of an HTML template into the email message. The template is +// executed with the provided data, and its output is embedded as a file. If the template fails to execute, +// an error will be returned. +// +// Parameters: +// - name: The name of the embedded file. +// - tpl: A pointer to the html/template.Template to be executed for the embedded content. +// - data: The data to populate the template. +// - opts: Optional parameters for customizing the embedded file. +// +// Returns: +// - An error if the template fails to execute or cannot be embedded, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) EmbedHTMLTemplate( name string, tpl *ht.Template, data interface{}, opts ...FileOption, ) error { @@ -1844,7 +2030,23 @@ func (m *Msg) EmbedHTMLTemplate( return nil } -// EmbedTextTemplate adds the output of a text/template.Template pointer as embedded File to the Msg +// EmbedTextTemplate adds the output of a text/template.Template pointer as an embedded File to the Msg. +// +// This method embeds the rendered output of a text template into the email message. The template is +// executed with the provided data, and its output is embedded as a file. If the template fails to execute, +// an error will be returned. +// +// Parameters: +// - name: The name of the embedded file. +// - tpl: A pointer to the text/template.Template to be executed for the embedded content. +// - data: The data to populate the template. +// - opts: Optional parameters for customizing the embedded file. +// +// Returns: +// - An error if the template fails to execute or cannot be embedded, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) EmbedTextTemplate( name string, tpl *tt.Template, data interface{}, opts ...FileOption, ) error { @@ -1856,7 +2058,21 @@ func (m *Msg) EmbedTextTemplate( return nil } -// EmbedFromEmbedFS adds an embedded File from an embed.FS to the Msg +// EmbedFromEmbedFS adds an embedded File from an embed.FS to the Msg. +// +// This method embeds a file from an embedded filesystem (embed.FS) into the email message. If the +// embedded filesystem is nil or the file cannot be retrieved, an error will be returned. +// +// Parameters: +// - name: The name of the file to be embedded. +// - fs: A pointer to the embed.FS from which the file will be retrieved. +// - opts: Optional parameters for customizing the embedded file. +// +// Returns: +// - An error if the embed.FS is nil or the file cannot be retrieved, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) EmbedFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) error { if fs == nil { return fmt.Errorf("embed.FS must not be nil") @@ -1869,8 +2085,14 @@ func (m *Msg) EmbedFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) er return nil } -// Reset resets all headers, body parts and attachments/embeds of the Msg -// It leaves already set encodings, charsets, boundaries, etc. as is +// Reset resets all headers, body parts, attachments, and embeds of the Msg. +// +// This method clears all address headers, attachments, embeds, generic headers, and body parts of the message. +// However, it preserves the existing encoding, charset, boundary, and other message-level settings. +// Use this method to reset the message content while keeping certain configurations intact. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322 func (m *Msg) Reset() { m.addrHeader = make(map[AddrHeader][]*mail.Address) m.attachments = nil @@ -1879,7 +2101,17 @@ func (m *Msg) Reset() { m.parts = nil } -// ApplyMiddlewares apply the list of middlewares to a Msg +// ApplyMiddlewares applies the list of middlewares to a Msg. +// +// This method sequentially applies each middleware function in the list to the message (in FIFO order). +// The middleware functions can modify the message, such as adding headers or altering its content. +// The message is passed through each middleware in order, and the modified message is returned. +// +// Parameters: +// - msg: The Msg object to which the middlewares will be applied. +// +// Returns: +// - The modified Msg after all middleware functions have been applied. func (m *Msg) applyMiddlewares(msg *Msg) *Msg { for _, middleware := range m.middlewares { msg = middleware.Handle(msg) @@ -1887,15 +2119,44 @@ func (m *Msg) applyMiddlewares(msg *Msg) *Msg { return msg } -// WriteTo writes the formated Msg into a give io.Writer and satisfies the io.WriteTo interface +// WriteTo writes the formatted Msg into the given io.Writer and satisfies the io.WriterTo interface. +// +// This method writes the email message, including its headers, body, and attachments, to the provided +// io.Writer. It applies any middlewares to the message before writing it. The total number of bytes +// written and any error encountered during the writing process are returned. +// +// Parameters: +// - writer: The io.Writer to which the formatted message will be written. +// +// Returns: +// - The total number of bytes written. +// - An error if any occurred during the writing process, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322 func (m *Msg) WriteTo(writer io.Writer) (int64, error) { mw := &msgWriter{writer: writer, charset: m.charset, encoder: m.encoder} mw.writeMsg(m.applyMiddlewares(m)) return mw.bytesWritten, mw.err } -// WriteToSkipMiddleware writes the formated Msg into a give io.Writer and satisfies -// the io.WriteTo interface but will skip the given Middleware +// WriteToSkipMiddleware writes the formatted Msg into the given io.Writer, but skips the specified +// middleware type. +// +// This method writes the email message to the provided io.Writer after applying all middlewares, +// except for the specified middleware type, which will be skipped. It temporarily removes the +// middleware of the given type, writes the message, and then restores the original middleware list. +// +// Parameters: +// - writer: The io.Writer to which the formatted message will be written. +// - middleWareType: The MiddlewareType that should be skipped during the writing process. +// +// Returns: +// - The total number of bytes written. +// - An error if any occurred during the writing process, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322 func (m *Msg) WriteToSkipMiddleware(writer io.Writer, middleWareType MiddlewareType) (int64, error) { var origMiddlewares, middlewares []Middleware origMiddlewares = m.middlewares @@ -1912,30 +2173,39 @@ func (m *Msg) WriteToSkipMiddleware(writer io.Writer, middleWareType MiddlewareT return mw.bytesWritten, mw.err } -// Write is an alias method to WriteTo due to compatibility reasons +// Write is an alias method to WriteTo for compatibility reasons. +// +// This method provides a backward-compatible way to write the formatted Msg to the provided io.Writer +// by calling the WriteTo method. It writes the email message, including headers, body, and attachments, +// to the io.Writer and returns the number of bytes written and any error encountered. +// +// Parameters: +// - writer: The io.Writer to which the formatted message will be written. +// +// Returns: +// - The total number of bytes written. +// - An error if any occurred during the writing process, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322 func (m *Msg) Write(writer io.Writer) (int64, error) { return m.WriteTo(writer) } -// appendFile adds a File to the Msg (as attachment or embed) -func (m *Msg) appendFile(files []*File, file *File, opts ...FileOption) []*File { - // Override defaults with optionally provided FileOption functions - for _, opt := range opts { - if opt == nil { - continue - } - opt(file) - } - - if files == nil { - return []*File{file} - } - - return append(files, file) -} - -// WriteToFile stores the Msg as file on disk. It will try to create the given filename -// Already existing files will be overwritten +// WriteToFile stores the Msg as a file on disk. It will try to create the given filename, +// and if the file already exists, it will be overwritten. +// +// This method writes the email message, including its headers, body, and attachments, to a file on disk. +// If the file cannot be created or an error occurs during writing, an error is returned. +// +// Parameters: +// - name: The name of the file to be created or overwritten. +// +// Returns: +// - An error if the file cannot be created or if writing to the file fails, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322 func (m *Msg) WriteToFile(name string) error { file, err := os.Create(name) if err != nil { @@ -1949,22 +2219,58 @@ func (m *Msg) WriteToFile(name string) error { return file.Close() } -// WriteToSendmail returns WriteToSendmailWithCommand with a default sendmail path +// WriteToSendmail returns WriteToSendmailWithCommand with a default sendmail path. +// +// This method sends the email message using the default sendmail path. It calls WriteToSendmailWithCommand +// using the standard SendmailPath. If sending via sendmail fails, an error is returned. +// +// Returns: +// - An error if sending the message via sendmail fails, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5321 func (m *Msg) WriteToSendmail() error { return m.WriteToSendmailWithCommand(SendmailPath) } // WriteToSendmailWithCommand returns WriteToSendmailWithContext with a default timeout -// of 5 seconds and a given sendmail path +// of 5 seconds and a given sendmail path. +// +// This method sends the email message using the provided sendmail path, with a default timeout of 5 seconds. +// It creates a context with the specified timeout and then calls WriteToSendmailWithContext to send the message. +// +// Parameters: +// - sendmailPath: The path to the sendmail executable to be used for sending the message. +// +// Returns: +// - An error if sending the message via sendmail fails, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5321 func (m *Msg) WriteToSendmailWithCommand(sendmailPath string) error { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() return m.WriteToSendmailWithContext(ctx, sendmailPath) } -// WriteToSendmailWithContext opens an pipe to the local sendmail binary and tries to send the -// mail though that. It takes a context.Context, the path to the sendmail binary and additional -// arguments for the sendmail binary as parameters +// WriteToSendmailWithContext opens a pipe to the local sendmail binary and tries to send the +// email through it. It takes a context.Context, the path to the sendmail binary, and additional +// arguments for the sendmail binary as parameters. +// +// This method establishes a pipe to the sendmail executable using the provided context and arguments. +// It writes the email message to the sendmail process via STDIN. If any errors occur during the +// communication with the sendmail binary, they will be captured and returned. +// +// Parameters: +// - ctx: The context to control the timeout and cancellation of the sendmail process. +// - sendmailPath: The path to the sendmail executable. +// - args: Additional arguments for the sendmail binary. +// +// Returns: +// - An error if sending the message via sendmail fails, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5321 func (m *Msg) WriteToSendmailWithContext(ctx context.Context, sendmailPath string, args ...string) error { cmdCtx := exec.CommandContext(ctx, sendmailPath) cmdCtx.Args = append(cmdCtx.Args, "-oi", "-t") @@ -2017,10 +2323,19 @@ func (m *Msg) WriteToSendmailWithContext(ctx context.Context, sendmailPath strin // NewReader returns a Reader type that satisfies the io.Reader interface. // -// IMPORTANT: when creating a new Reader, the current state of the Msg is taken, as -// basis for the Reader. If you perform changes on Msg after creating the Reader, these -// changes will not be reflected in the Reader. You will have to use Msg.UpdateReader -// first to update the Reader's buffer with the current Msg content +// This method creates a new Reader for the Msg, capturing the current state of the message. +// Any subsequent changes made to the Msg after creating the Reader will not be reflected in the Reader's buffer. +// To reflect these changes in the Reader, you must call Msg.UpdateReader to update the Reader's content with +// the current state of the Msg. +// +// Returns: +// - A pointer to a Reader, which allows the Msg to be read as a stream of bytes. +// +// IMPORTANT: Any changes made to the Msg after creating the Reader will not be reflected in the Reader unless +// Msg.UpdateReader is called. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322 func (m *Msg) NewReader() *Reader { reader := &Reader{} buffer := bytes.Buffer{} @@ -2032,8 +2347,17 @@ func (m *Msg) NewReader() *Reader { return reader } -// UpdateReader will update a Reader with the content of the given Msg and reset the -// Reader position to the start +// UpdateReader updates a Reader with the current content of the Msg and resets the +// Reader's position to the start. +// +// This method rewrites the content of the provided Reader to reflect any changes made to the Msg. +// It resets the Reader's position to the beginning and updates the buffer with the latest message content. +// +// Parameters: +// - reader: A pointer to the Reader that will be updated with the Msg's current content. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322 func (m *Msg) UpdateReader(reader *Reader) { buffer := bytes.Buffer{} _, err := m.Write(&buffer) @@ -2042,14 +2366,27 @@ func (m *Msg) UpdateReader(reader *Reader) { reader.err = err } -// HasSendError returns true if the Msg experienced an error during the message delivery and the -// sendError field of the Msg is not nil +// HasSendError returns true if the Msg experienced an error during message delivery +// and the sendError field of the Msg is not nil. +// +// This method checks whether the message has encountered a delivery error by verifying if the +// sendError field is populated. +// +// Returns: +// - A boolean value indicating whether a send error occurred (true if an error is present). func (m *Msg) HasSendError() bool { return m.sendError != nil } -// SendErrorIsTemp returns true if the Msg experienced an error during the message delivery and the -// corresponding error was of temporary nature and should be retried later +// SendErrorIsTemp returns true if the Msg experienced a delivery error, and the corresponding +// error was of a temporary nature, meaning it can be retried later. +// +// This method checks whether the encountered sendError is a temporary error that can be retried. +// It uses the errors.As function to determine if the error is of type SendError and checks if +// the error is marked as temporary. +// +// Returns: +// - A boolean value indicating whether the send error is temporary (true if the error is temporary). func (m *Msg) SendErrorIsTemp() bool { var err *SendError if errors.As(m.sendError, &err) && err != nil { @@ -2058,12 +2395,32 @@ func (m *Msg) SendErrorIsTemp() bool { return false } -// SendError returns the sendError field of the Msg +// SendError returns the sendError field of the Msg. +// +// This method retrieves the error that occurred during the message delivery process, if any. +// It returns the sendError field, which holds the error encountered during sending. +// +// Returns: +// - The error encountered during message delivery, or nil if no error occurred. func (m *Msg) SendError() error { return m.sendError } -// addAddr adds an additional address to the given addrHeader of the Msg +// addAddr adds an additional address to the given addrHeader of the Msg. +// +// This method appends an email address to the specified address header (such as "To", "Cc", or "Bcc") +// without overwriting existing addresses. It first collects the current addresses in the header, then +// adds the new address and updates the header. +// +// Parameters: +// - header: The AddrHeader (e.g., HeaderTo, HeaderCc) to which the address will be added. +// - addr: The email address to add to the specified header. +// +// Returns: +// - An error if the address cannot be added, otherwise nil. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322 func (m *Msg) addAddr(header AddrHeader, addr string) error { var addresses []string for _, address := range m.addrHeader[header] { @@ -2073,13 +2430,69 @@ func (m *Msg) addAddr(header AddrHeader, addr string) error { return m.SetAddrHeader(header, addresses...) } +// appendFile adds a File to the Msg, either as an attachment or an embed. +// +// This method appends a File to the list of files (attachments or embeds) for the message. It applies +// optional FileOption functions to customize the file properties before adding it. If no files are +// already present, a new list is created. +// +// Parameters: +// - files: The current list of files (either attachments or embeds). +// - file: The File to be added. +// - opts: Optional FileOption functions to customize the file. +// +// Returns: +// - A slice of File pointers representing the updated list of files. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 +func (m *Msg) appendFile(files []*File, file *File, opts ...FileOption) []*File { + // Override defaults with optionally provided FileOption functions + for _, opt := range opts { + if opt == nil { + continue + } + opt(file) + } + + if files == nil { + return []*File{file} + } + + return append(files, file) +} + // encodeString encodes a string based on the configured message encoder and the corresponding -// charset for the Msg +// charset for the Msg. +// +// This method encodes the provided string using the message's charset and encoder settings. +// The encoding ensures that the string is properly formatted according to the message's +// character encoding (e.g., UTF-8, ISO-8859-1). +// +// Parameters: +// - str: The string to be encoded. +// +// Returns: +// - The encoded string based on the message's charset and encoder. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2047 func (m *Msg) encodeString(str string) string { return m.encoder.Encode(string(m.charset), str) } -// hasAlt returns true if the Msg has more than one part +// hasAlt returns true if the Msg has more than one part. +// +// This method checks whether the message contains more than one part, indicating that +// the message has alternative content (e.g., both plain text and HTML parts). It ignores +// any parts marked as deleted and returns true only if more than one valid part exists +// and no PGP type is set. +// +// Returns: +// - A boolean value indicating whether the message has multiple parts (true if more than one part exists). +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2046 func (m *Msg) hasAlt() bool { count := 0 for _, part := range m.parts { @@ -2090,22 +2503,66 @@ func (m *Msg) hasAlt() bool { return count > 1 && m.pgptype == 0 } -// hasMixed returns true if the Msg has mixed parts +// hasMixed returns true if the Msg has mixed parts. +// +// This method checks whether the message contains mixed content, such as attachments along with +// message parts (e.g., text or HTML). A message is considered to have mixed parts if there are both +// attachments and message parts, or if there are multiple attachments. +// +// Returns: +// - A boolean value indicating whether the message has mixed parts. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2046#section-5.1.3 func (m *Msg) hasMixed() bool { return m.pgptype == 0 && ((len(m.parts) > 0 && len(m.attachments) > 0) || len(m.attachments) > 1) } -// hasRelated returns true if the Msg has related parts +// hasRelated returns true if the Msg has related parts. +// +// This method checks whether the message contains related parts, such as inline embedded files +// (e.g., images) that are referenced within the message body. A message is considered to have +// related parts if there are both message parts and embedded files, or if there are multiple embedded files. +// +// Returns: +// - A boolean value indicating whether the message has related parts. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2387 func (m *Msg) hasRelated() bool { return m.pgptype == 0 && ((len(m.parts) > 0 && len(m.embeds) > 0) || len(m.embeds) > 1) } -// hasPGPType returns true if the Msg should be treated as PGP encoded message +// hasPGPType returns true if the Msg should be treated as a PGP-encoded message. +// +// This method checks whether the message is configured to be treated as a PGP-encoded message by examining +// the pgptype field. If the PGP type is set to a value greater than 0, the message is considered PGP-encoded. +// +// Returns: +// - A boolean value indicating whether the message is PGP-encoded. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc4880 func (m *Msg) hasPGPType() bool { return m.pgptype > 0 } -// newPart returns a new Part for the Msg +// newPart returns a new Part for the Msg. +// +// This method creates a new Part for the message with the specified content type, +// using the message's current charset and encoding settings. Optional PartOption +// functions can be applied to customize the Part further. +// +// Parameters: +// - contentType: The content type for the new Part (e.g., text/plain, text/html). +// - opts: Optional PartOption functions to customize the Part. +// +// Returns: +// - A pointer to the newly created Part structure. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2045 +// - https://datatracker.ietf.org/doc/html/rfc2046 func (m *Msg) newPart(contentType ContentType, opts ...PartOption) *Part { p := &Part{ contentType: contentType, @@ -2124,13 +2581,26 @@ func (m *Msg) newPart(contentType ContentType, opts ...PartOption) *Part { return p } -// setEncoder creates a new mime.WordEncoder based on the encoding setting of the message +// setEncoder creates a new mime.WordEncoder based on the encoding setting of the message. +// +// This method sets the message's encoder by creating a new mime.WordEncoder that matches the +// current encoding setting (e.g., quoted-printable or base64). The encoder is used to encode +// message headers and body content appropriately. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2047 func (m *Msg) setEncoder() { m.encoder = getEncoder(m.encoding) } -// checkUserAgent checks if a useragent/x-mailer is set and if not will set a default -// version string +// checkUserAgent checks if a User-Agent or X-Mailer header is set, and if not, sets a default version string. +// +// This method ensures that the message includes a User-Agent and X-Mailer header, unless the noDefaultUserAgent +// flag is set. If neither of these headers is present, a default User-Agent string with the current library +// version is added. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.7 func (m *Msg) checkUserAgent() { if m.noDefaultUserAgent { return @@ -2143,7 +2613,16 @@ func (m *Msg) checkUserAgent() { } } -// addDefaultHeader sets some default headers, if they haven't been set before +// addDefaultHeader sets default headers if they haven't been set before. +// +// This method ensures that essential headers such as "Date", "Message-ID", and "MIME-Version" are set +// in the message. If these headers are not already present, they will be set to default values. +// The "Date" and "Message-ID" headers are generated, and the "MIME-Version" is set to the message's current setting. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.1 (Date) +// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.4 (Message-ID) +// - https://datatracker.ietf.org/doc/html/rfc2045#section-4 (MIME-Version) func (m *Msg) addDefaultHeader() { if _, ok := m.genHeader[HeaderDate]; !ok { m.SetDate() @@ -2154,7 +2633,22 @@ func (m *Msg) addDefaultHeader() { m.SetGenHeader(HeaderMIMEVersion, string(m.mimever)) } -// fileFromEmbedFS returns a File pointer from a given file in the provided embed.FS +// fileFromEmbedFS returns a File pointer from a given file in the provided embed.FS. +// +// This method retrieves a file from the embedded filesystem (embed.FS) and returns a File structure +// that can be used as an attachment or embed in the email message. The file's content is read when +// writing to an io.Writer, and the file is identified by its base name. +// +// Parameters: +// - name: The name of the file to retrieve from the embedded filesystem. +// - fs: A pointer to the embed.FS from which the file will be opened. +// +// Returns: +// - A pointer to the File structure representing the embedded file. +// - An error if the file cannot be opened or read from the embedded filesystem. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func fileFromEmbedFS(name string, fs *embed.FS) (*File, error) { _, err := fs.Open(name) if err != nil { @@ -2178,7 +2672,21 @@ func fileFromEmbedFS(name string, fs *embed.FS) (*File, error) { }, nil } -// fileFromFS returns a File pointer from a given file in the system's file system +// fileFromFS returns a File pointer from a given file in the system's file system. +// +// This method retrieves a file from the system's file system and returns a File structure +// that can be used as an attachment or embed in the email message. The file is identified +// by its base name, and its content is read when writing to an io.Writer. +// +// Parameters: +// - name: The name of the file to retrieve from the system's file system. +// +// Returns: +// - A pointer to the File structure representing the file from the system's file system. +// - Nil if the file does not exist or cannot be accessed. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func fileFromFS(name string) *File { _, err := os.Stat(name) if err != nil { @@ -2203,7 +2711,22 @@ func fileFromFS(name string) *File { } } -// fileFromReader returns a File pointer from a given io.Reader +// fileFromReader returns a File pointer from a given io.Reader. +// +// This method reads all data from the provided io.Reader and creates a File structure +// that can be used as an attachment or embed in the email message. The file's content +// is stored in memory and written to an io.Writer when needed. +// +// Parameters: +// - name: The name of the file to be represented by the reader's content. +// - reader: The io.Reader from which the file content will be read. +// +// Returns: +// - A pointer to the File structure representing the content of the io.Reader. +// - An error if the content cannot be read from the io.Reader. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func fileFromReader(name string, reader io.Reader) (*File, error) { d, err := io.ReadAll(reader) if err != nil { @@ -2224,7 +2747,21 @@ func fileFromReader(name string, reader io.Reader) (*File, error) { }, nil } -// fileFromReadSeeker returns a File pointer from a given io.ReadSeeker +// fileFromReadSeeker returns a File pointer from a given io.ReadSeeker. +// +// This method creates a File structure from an io.ReadSeeker, allowing efficient handling of file content +// by seeking and reading from the source without fully loading it into memory. The content is written +// to an io.Writer when needed, and the reader's position is reset to the start after writing. +// +// Parameters: +// - name: The name of the file to be represented by the io.ReadSeeker. +// - reader: The io.ReadSeeker from which the file content will be read. +// +// Returns: +// - A pointer to the File structure representing the content of the io.ReadSeeker. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func fileFromReadSeeker(name string, reader io.ReadSeeker) *File { return &File{ Name: name, @@ -2240,7 +2777,23 @@ func fileFromReadSeeker(name string, reader io.ReadSeeker) *File { } } -// fileFromHTMLTemplate returns a File pointer form a given html/template.Template +// fileFromHTMLTemplate returns a File pointer from a given html/template.Template. +// +// This method executes the provided HTML template with the given data and creates a File structure +// representing the output. The rendered template content is stored in a buffer and then processed +// as a file attachment or embed. +// +// Parameters: +// - name: The name of the file to be created from the template output. +// - tpl: A pointer to the html/template.Template to be executed. +// - data: The data to populate the template. +// +// Returns: +// - A pointer to the File structure representing the rendered template. +// - An error if the template is nil or if it fails to execute. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func fileFromHTMLTemplate(name string, tpl *ht.Template, data interface{}) (*File, error) { if tpl == nil { return nil, errors.New(errTplPointerNil) @@ -2252,7 +2805,23 @@ func fileFromHTMLTemplate(name string, tpl *ht.Template, data interface{}) (*Fil return fileFromReader(name, &buffer) } -// fileFromTextTemplate returns a File pointer form a given text/template.Template +// fileFromTextTemplate returns a File pointer from a given text/template.Template. +// +// This method executes the provided text template with the given data and creates a File structure +// representing the output. The rendered template content is stored in a buffer and then processed +// as a file attachment or embed. +// +// Parameters: +// - name: The name of the file to be created from the template output. +// - tpl: A pointer to the text/template.Template to be executed. +// - data: The data to populate the template. +// +// Returns: +// - A pointer to the File structure representing the rendered template. +// - An error if the template is nil or if it fails to execute. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2183 func fileFromTextTemplate(name string, tpl *tt.Template, data interface{}) (*File, error) { if tpl == nil { return nil, errors.New(errTplPointerNil) @@ -2264,7 +2833,19 @@ func fileFromTextTemplate(name string, tpl *tt.Template, data interface{}) (*Fil return fileFromReader(name, &buffer) } -// getEncoder creates a new mime.WordEncoder based on the encoding setting of the message +// getEncoder creates a new mime.WordEncoder based on the encoding setting of the message. +// +// This function returns a mime.WordEncoder based on the specified encoding (e.g., quoted-printable or base64). +// The encoder is used for encoding message headers and body content according to the chosen encoding standard. +// +// Parameters: +// - enc: The Encoding type for the message (e.g., EncodingQP for quoted-printable or EncodingB64 for base64). +// +// Returns: +// - A mime.WordEncoder based on the encoding setting. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2047 func getEncoder(enc Encoding) mime.WordEncoder { switch enc { case EncodingQP: @@ -2276,8 +2857,21 @@ func getEncoder(enc Encoding) mime.WordEncoder { } } -// writeFuncFromBuffer is a common method to convert a byte buffer into a writeFunc as -// often required by this library +// writeFuncFromBuffer converts a byte buffer into a writeFunc, which is commonly required by go-mail. +// +// This function wraps a byte buffer into a write function that can be used to write the buffer's content +// to an io.Writer. It returns a function that writes the buffer's content to the given writer and returns +// the number of bytes written and any error that occurred during writing. +// +// Parameters: +// - buffer: A pointer to the bytes.Buffer containing the data to be written. +// +// Returns: +// - A function that writes the buffer's content to an io.Writer, returning the number of bytes written +// and any error encountered during the write operation. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc5322 func writeFuncFromBuffer(buffer *bytes.Buffer) func(io.Writer) (int64, error) { writeFunc := func(w io.Writer) (int64, error) { numBytes, err := w.Write(buffer.Bytes()) From d6426063ba57f3a15e58894231b60bc526e0be60 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 6 Oct 2024 12:39:02 +0200 Subject: [PATCH 162/181] Refactor comments and docstrings for clarity and detail Enhanced comments and docstrings for better readability and detail in client.go and b64linebreaker.go. Updated descriptions, added parameter and return details, and included RFC references where applicable for improved documentation quality. --- b64linebreaker.go | 29 +++- client.go | 372 ++++++++++++++++++++++++++++++++++------------ 2 files changed, 304 insertions(+), 97 deletions(-) diff --git a/b64linebreaker.go b/b64linebreaker.go index 8f82cda..cc83973 100644 --- a/b64linebreaker.go +++ b/b64linebreaker.go @@ -16,10 +16,14 @@ var newlineBytes = []byte(SingleNewLine) // ErrNoOutWriter is the error message returned when no io.Writer is set for Base64LineBreaker. const ErrNoOutWriter = "no io.Writer set for Base64LineBreaker" -// Base64LineBreaker is used to handle base64 encoding with the insertion of new lines after a certain -// number of characters. +// Base64LineBreaker handles base64 encoding with the insertion of new lines after a certain number +// of characters. // -// It satisfies the io.WriteCloser interface. +// This struct is used to manage base64 encoding while ensuring that new lines are inserted after +// reaching a specific line length. It satisfies the io.WriteCloser interface. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2045 (Base64 and line length limitations) type Base64LineBreaker struct { line [MaxBodyLength]byte used int @@ -27,7 +31,17 @@ type Base64LineBreaker struct { } // Write writes data to the Base64LineBreaker, ensuring lines do not exceed MaxBodyLength. -// It handles continuation if data length exceeds the limit and writes new lines accordingly. +// +// This method writes the provided data to the Base64LineBreaker. It ensures that the written +// lines do not exceed the MaxBodyLength. If the data exceeds the limit, it handles the +// continuation by splitting the data and writing new lines as necessary. +// +// Parameters: +// - data: A byte slice containing the data to be written. +// +// Returns: +// - numBytes: The number of bytes written. +// - err: An error if one occurred during the write operation. func (l *Base64LineBreaker) Write(data []byte) (numBytes int, err error) { if l.out == nil { err = errors.New(ErrNoOutWriter) @@ -60,6 +74,13 @@ func (l *Base64LineBreaker) Write(data []byte) (numBytes int, err error) { } // Close finalizes the Base64LineBreaker, writing any remaining buffered data and appending a newline. +// +// This method ensures that any remaining data in the buffer is written to the output and appends +// a newline. It is used to finalize the Base64LineBreaker and should be called when no more data +// is expected to be written. +// +// Returns: +// - err: An error if one occurred during the final write operation. func (l *Base64LineBreaker) Close() (err error) { if l.used > 0 { _, err = l.out.Write(l.line[0:l.used]) diff --git a/client.go b/client.go index 26a8bb5..dc7d46c 100644 --- a/client.go +++ b/client.go @@ -41,7 +41,6 @@ const ( ) const ( - // DSNMailReturnHeadersOnly requests that only the message headers of the mail message are returned in // a DSN (Delivery Status Notification). // @@ -84,7 +83,6 @@ const ( ) type ( - // DialContextFunc defines a function type for establishing a network connection using context, network // type, and address. It is used to specify custom DialContext function. // @@ -106,7 +104,15 @@ type ( // Option is a function type that modifies the configuration or behavior of a Client instance. Option func(*Client) error - // Client is the go-mail client that is responsible for connecting and interacting with an SMTP server. + // Client is responsible for connecting and interacting with an SMTP server. + // + // This struct represents the go-mail client, which manages the connection, authentication, and communication + // with an SMTP server. It contains various configuration options, including connection timeouts, encryption + // settings, authentication methods, and Delivery Status Notification (DSN) preferences. + // + // References: + // - https://datatracker.ietf.org/doc/html/rfc3207#section-2 + // - https://datatracker.ietf.org/doc/html/rfc8314 Client struct { // connTimeout specifies timeout for the connection to the SMTP server. connTimeout time.Duration @@ -236,10 +242,18 @@ var ( ) // NewClient creates a new Client instance with the provided host and optional configuration Option functions. -// It initializes default values for connection timeout, port, TLS settings, and HELO/EHLO hostname. -// Option functions, if provided, override default values. // -// Returns an error if critical defaults are unset. +// This function initializes a Client with default values, such as connection timeout, port, TLS settings, +// and the HELO/EHLO hostname. Option functions, if provided, can override the default configuration. +// It ensures that essential values, like the host, are set. An error is returned if critical defaults are unset. +// +// Parameters: +// - host: The hostname of the SMTP server to connect to. +// - opts: Optional configuration functions to override default settings. +// +// Returns: +// - A pointer to the initialized Client. +// - An error if any critical default values are missing or options fail to apply. func NewClient(host string, opts ...Option) (*Client, error) { c := &Client{ connTimeout: DefaultTimeout, @@ -272,8 +286,17 @@ func NewClient(host string, opts ...Option) (*Client, error) { return c, nil } -// WithPort sets the port number for the Client and overrides the default port. It validates the port number to -// ensure it is between 1 and 65535. An error is returned if the provided port number is invalid. +// WithPort sets the port number for the Client and overrides the default port. +// +// This function sets the specified port number for the Client, ensuring that the port number is valid +// (between 1 and 65535). If the provided port number is invalid, an error is returned. +// +// Parameters: +// - port: The port number to be used by the Client. Must be between 1 and 65535. +// +// Returns: +// - An Option function that applies the port setting to the Client. +// - An error if the port number is outside the valid range. func WithPort(port int) Option { return func(c *Client) error { if port < 1 || port > 65535 { @@ -284,8 +307,17 @@ func WithPort(port int) Option { } } -// WithTimeout sets the connection timeout for the Client to the provided duration and overrides the default -// timeout. An error is returned if the provided timeout is invalid. +// WithTimeout sets the connection timeout for the Client and overrides the default timeout. +// +// This function configures the Client with a specified connection timeout duration. It validates that the +// provided timeout is greater than zero. If the timeout is invalid, an error is returned. +// +// Parameters: +// - timeout: The duration to be set as the connection timeout. Must be greater than zero. +// +// Returns: +// - An Option function that applies the timeout setting to the Client. +// - An error if the timeout duration is invalid. func WithTimeout(timeout time.Duration) Option { return func(c *Client) error { if timeout <= 0 { @@ -297,6 +329,11 @@ func WithTimeout(timeout time.Duration) Option { } // WithSSL enables implicit SSL/TLS for the Client. +// +// This function configures the Client to use implicit SSL/TLS for secure communication. +// +// Returns: +// - An Option function that enables SSL/TLS for the Client. func WithSSL() Option { return func(c *Client) error { c.useSSL = true @@ -307,13 +344,16 @@ func WithSSL() Option { // WithSSLPort enables implicit SSL/TLS with an optional fallback for the Client. The correct port is // automatically set. // -// If this option is used with NewClient, the default port 25 will be overriden with port 465. If fallback -// is set to true and the SSL/TLS connection fails, the Client will attempt to connect on port 25 using -// using an unencrypted connection. +// When this option is used with NewClient, the default port 25 is overridden with port 465 for SSL/TLS connections. +// If fallback is set to true and the SSL/TLS connection fails, the Client attempts to connect on port 25 using an +// unencrypted connection. If WithPort has already been used to set a different port, that port takes precedence, +// and the automatic fallback mechanism is skipped. // -// Note: If a different port has already been set otherwise using WithPort, the selected port has higher -// precedence and is used to establish the SSL/TLS connection. In this case the authmatic fallback -// mechanism is skipped at all. +// Parameters: +// - fallback: A boolean indicating whether to fall back to port 25 without SSL/TLS if the connection fails. +// +// Returns: +// - An Option function that enables SSL/TLS and configures the fallback mechanism for the Client. func WithSSLPort(fallback bool) Option { return func(c *Client) error { c.SetSSLPort(true, fallback) @@ -321,12 +361,15 @@ func WithSSLPort(fallback bool) Option { } } -// WithDebugLog enables debug logging for the Client. The debug logger will log incoming and outgoing -// communication between the Client and the server to os.StdErr. +// WithDebugLog enables debug logging for the Client. // -// Note: The SMTP communication might include unencrypted authentication data, depending if you are -// using SMTP authentication and the type of authentication mechanism. This could pose a data -// protection problem. Use debug logging with care. +// 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. +// +// Returns: +// - An Option function that enables debug logging for the Client. func WithDebugLog() Option { return func(c *Client) error { c.useDebugLog = true @@ -334,10 +377,16 @@ func WithDebugLog() Option { } } -// WithLogger defines a custom logger for the Client. The logger has to satisfy the log.Logger -// interface and is only used when debug logging is enabled on the Client. +// WithLogger defines a custom logger for the Client. // -// By default we use log.Stdlog. +// This function sets a custom logger for the Client, which must satisfy the log.Logger interface. The custom +// logger is used only when debug logging is enabled. By default, log.Stdlog is used if no custom logger is provided. +// +// Parameters: +// - logger: A logger that satisfies the log.Logger interface. +// +// Returns: +// - An Option function that sets the custom logger for the Client. func WithLogger(logger log.Logger) Option { return func(c *Client) error { c.logger = logger @@ -345,9 +394,17 @@ func WithLogger(logger log.Logger) Option { } } -// WithHELO sets the HELO/EHLO string used for the the Client. +// WithHELO sets the HELO/EHLO string used by the Client. // -// By default we use os.Hostname to identify the HELO/EHLO string. +// This function configures the HELO/EHLO string sent by the Client when initiating communication +// with the SMTP server. By default, os.Hostname is used to identify the HELO/EHLO string. +// +// Parameters: +// - helo: The string to be used for the HELO/EHLO greeting. Must not be empty. +// +// Returns: +// - An Option function that sets the HELO/EHLO string for the Client. +// - An error if the provided HELO string is empty. func WithHELO(helo string) Option { return func(c *Client) error { if helo == "" { @@ -358,9 +415,18 @@ func WithHELO(helo string) Option { } } -// WithTLSPolicy sets the TLSPolicy of the Client and overrides the DefaultTLSPolicy +// WithTLSPolicy sets the TLSPolicy of the Client and overrides the DefaultTLSPolicy. +// +// This function configures the Client's TLSPolicy, specifying how the Client handles TLS for SMTP connections. +// It overrides the default policy. For best practices regarding SMTP TLS connections, it is recommended to use +// WithTLSPortPolicy instead. +// +// Parameters: +// - policy: The TLSPolicy to be applied to the Client. +// +// Returns: +// - An Option function that sets the TLSPolicy for the Client. // -// Note: To follow best-practices for SMTP TLS connections, it is recommended to use // WithTLSPortPolicy instead. func WithTLSPolicy(policy TLSPolicy) Option { return func(c *Client) error { @@ -372,13 +438,17 @@ func WithTLSPolicy(policy TLSPolicy) Option { // WithTLSPortPolicy enables explicit TLS via STARTTLS for the Client using the provided TLSPolicy. The // correct port is automatically set. // -// If TLSMandatory or TLSOpportunistic are provided as TLSPolicy, port 587 will be used for the connection. -// If the connection fails with TLSOpportunistic, the Client will attempt to connect on port 25 using -// using an unencrypted connection as a fallback. If NoTLS is provided, the Client will always use port 25. +// When TLSMandatory or TLSOpportunistic is provided as the TLSPolicy, port 587 is used for the connection. +// If the connection fails with TLSOpportunistic, the Client attempts to connect on port 25 using an unencrypted +// connection as a fallback. If NoTLS is specified, the Client will always use port 25. +// If WithPort has already been used to set a different port, that port takes precedence, and the automatic fallback +// mechanism is skipped. // -// Note: If a different port has already been set otherwise using WithPort, the selected port has higher -// precedence and is used to establish the SSL/TLS connection. In this case the authmatic fallback -// mechanism is skipped at all. +// Parameters: +// - policy: The TLSPolicy to be used for STARTTLS communication. +// +// Returns: +// - An Option function that sets the TLSPortPolicy for the Client. func WithTLSPortPolicy(policy TLSPolicy) Option { return func(c *Client) error { c.SetTLSPortPolicy(policy) @@ -386,8 +456,17 @@ func WithTLSPortPolicy(policy TLSPolicy) Option { } } -// WithTLSConfig sets the tls.Config for the Client and overrides the default. An error is returned -// if the provided tls.Config is invalid. +// WithTLSConfig sets the tls.Config for the Client and overrides the default configuration. +// +// This function configures the Client with a custom tls.Config. It overrides the default TLS settings. +// An error is returned if the provided tls.Config is nil or invalid. +// +// Parameters: +// - tlsconfig: A pointer to a tls.Config struct to be used for the Client. Must not be nil. +// +// Returns: +// - An Option function that sets the tls.Config for the Client. +// - An error if the provided tls.Config is invalid. func WithTLSConfig(tlsconfig *tls.Config) Option { return func(c *Client) error { if tlsconfig == nil { @@ -398,7 +477,15 @@ func WithTLSConfig(tlsconfig *tls.Config) Option { } } -// WithSMTPAuth configures the Client to use the specified SMTPAuthType for the SMTP authentication. +// WithSMTPAuth configures the Client to use the specified SMTPAuthType for SMTP authentication. +// +// This function sets the Client to use the specified SMTPAuthType for authenticating with the SMTP server. +// +// Parameters: +// - authtype: The SMTPAuthType to be used for SMTP authentication. +// +// Returns: +// - An Option function that configures the Client to use the specified SMTPAuthType. func WithSMTPAuth(authtype SMTPAuthType) Option { return func(c *Client) error { c.smtpAuthType = authtype @@ -406,8 +493,16 @@ func WithSMTPAuth(authtype SMTPAuthType) Option { } } -// WithSMTPAuthCustom sets a custom SMTP authentication mechanism for the Client. The provided -// authentication mechanism has to satisfy the smtp.Auth interface. +// WithSMTPAuthCustom sets a custom SMTP authentication mechanism for the Client. +// +// This function configures the Client to use a custom SMTP authentication mechanism. The provided +// mechanism must satisfy the smtp.Auth interface. +// +// Parameters: +// - smtpAuth: The custom SMTP authentication mechanism, which must implement the smtp.Auth interface. +// +// Returns: +// - An Option function that sets the custom SMTP authentication for the Client. func WithSMTPAuthCustom(smtpAuth smtp.Auth) Option { return func(c *Client) error { c.smtpAuth = smtpAuth @@ -416,7 +511,15 @@ func WithSMTPAuthCustom(smtpAuth smtp.Auth) Option { } } -// WithUsername sets the username, the Client will use for the SMTP authentication. +// WithUsername sets the username that the Client will use for SMTP authentication. +// +// This function configures the Client with the specified username for SMTP authentication. +// +// Parameters: +// - username: The username to be used for SMTP authentication. +// +// Returns: +// - An Option function that sets the username for the Client. func WithUsername(username string) Option { return func(c *Client) error { c.user = username @@ -424,7 +527,15 @@ func WithUsername(username string) Option { } } -// WithPassword sets the password, the Client will use for the SMTP authentication. +// WithPassword sets the password that the Client will use for SMTP authentication. +// +// This function configures the Client with the specified password for SMTP authentication. +// +// Parameters: +// - password: The password to be used for SMTP authentication. +// +// Returns: +// - An Option function that sets the password for the Client. func WithPassword(password string) Option { return func(c *Client) error { c.pass = password @@ -432,13 +543,17 @@ func WithPassword(password string) Option { } } -// WithDSN enables DSN (Delivery Status Notifications) for the Client as described in the RFC 1891. DSN -// only work if the server supports them. +// WithDSN enables DSN (Delivery Status Notifications) for the Client as described in RFC 1891. // -// By default we set DSNMailReturnOption to DSNMailReturnFull and DSNRcptNotifyOption to DSNRcptNotifySuccess -// and DSNRcptNotifyFailure. +// This function configures the Client to request DSN, which provides status notifications for email delivery. +// DSN is only effective if the SMTP server supports it. By default, DSNMailReturnOption is set to DSNMailReturnFull, +// and DSNRcptNotifyOption is set to DSNRcptNotifySuccess and DSNRcptNotifyFailure. // -// https://datatracker.ietf.org/doc/html/rfc1891 +// Returns: +// - An Option function that enables DSN for the Client. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc1891 func WithDSN() Option { return func(c *Client) error { c.requestDSN = true @@ -448,12 +563,21 @@ func WithDSN() Option { } } -// WithDSNMailReturnType enables DSN (Delivery Status Notifications) for the Client as described in the -// RFC 1891. DSN only work if the server supports them. +// WithDSNMailReturnType enables DSN (Delivery Status Notifications) for the Client as described in RFC 1891. // -// It will set the DSNMailReturnOption to the provided value. +// This function configures the Client to request DSN and sets the DSNMailReturnOption to the provided value. +// DSN is only effective if the SMTP server supports it. The provided option must be either DSNMailReturnHeadersOnly +// or DSNMailReturnFull; otherwise, an error is returned. // -// https://datatracker.ietf.org/doc/html/rfc1891 +// Parameters: +// - option: The DSNMailReturnOption to be used (DSNMailReturnHeadersOnly or DSNMailReturnFull). +// +// Returns: +// - An Option function that sets the DSNMailReturnOption for the Client. +// - An error if an invalid DSNMailReturnOption is provided. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc1891 func WithDSNMailReturnType(option DSNMailReturnOption) Option { return func(c *Client) error { switch option { @@ -469,12 +593,22 @@ func WithDSNMailReturnType(option DSNMailReturnOption) Option { } } -// WithDSNRcptNotifyType enables DSN (Delivery Status Notifications) for the Client as described in the -// RFC 1891. DSN only work if the server supports them. +// WithDSNRcptNotifyType enables DSN (Delivery Status Notifications) for the Client as described in RFC 1891. // -// It will set the DSNRcptNotifyOption to the provided values. +// This function configures the Client to request DSN and sets the DSNRcptNotifyOption to the provided values. +// The provided options must be valid DSNRcptNotifyOption types. If DSNRcptNotifyNever is combined with +// any other notification type (such as DSNRcptNotifySuccess, DSNRcptNotifyFailure, or DSNRcptNotifyDelay), +// an error is returned. // -// https://datatracker.ietf.org/doc/html/rfc1891 +// Parameters: +// - opts: A variadic list of DSNRcptNotifyOption values (e.g., DSNRcptNotifySuccess, DSNRcptNotifyFailure). +// +// Returns: +// - An Option function that sets the DSNRcptNotifyOption for the Client. +// - An error if invalid DSNRcptNotifyOption values are provided or incompatible combinations are used. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc1891 func WithDSNRcptNotifyType(opts ...DSNRcptNotifyOption) Option { return func(c *Client) error { var rcptOpts []string @@ -508,8 +642,11 @@ func WithDSNRcptNotifyType(opts ...DSNRcptNotifyOption) Option { // WithoutNoop indicates that the Client should skip the "NOOP" command during the dial. // -// This is useful for servers which delay potentially unwanted clients when they perform commands -// other than AUTH. For example Microsoft Exchange's Tarpit. +// This option is useful for servers that delay potentially unwanted clients when they perform +// commands other than AUTH, such as Microsoft's Exchange Tarpit. +// +// Returns: +// - An Option function that configures the Client to skip the "NOOP" command. func WithoutNoop() Option { return func(c *Client) error { c.noNoop = true @@ -517,8 +654,16 @@ func WithoutNoop() Option { } } -// WithDialContextFunc sets the provided DialContextFunc as DialContext and overrides the default DialContext for -// connecting to the SMTP server +// WithDialContextFunc sets the provided DialContextFunc as the DialContext for connecting to the SMTP server. +// +// This function overrides the default DialContext function used by the Client when establishing a connection +// to the SMTP server with the provided DialContextFunc. +// +// Parameters: +// - dialCtxFunc: The custom DialContextFunc to be used for connecting to the SMTP server. +// +// Returns: +// - An Option function that sets the custom DialContextFunc for the Client. func WithDialContextFunc(dialCtxFunc DialContextFunc) Option { return func(c *Client) error { c.dialContextFunc = dialCtxFunc @@ -526,35 +671,50 @@ func WithDialContextFunc(dialCtxFunc DialContextFunc) Option { } } -// TLSPolicy returns the TLSPolicy that is currently set on the Client as 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. +// +// Returns: +// - A string representing the currently set TLSPolicy for the Client. func (c *Client) TLSPolicy() string { return c.tlspolicy.String() } // ServerAddr returns the server address that is currently set on the Client in the format "host:port". +// +// This method constructs and returns the server address using the host and port currently configured +// for the Client. +// +// Returns: +// - A string representing the server address in the format "host:port". func (c *Client) ServerAddr() string { return fmt.Sprintf("%s:%d", c.host, c.port) } -// SetTLSPolicy sets or overrides the TLSPolicy that is currently set on the Client with the given -// TLSPolicy. +// SetTLSPolicy sets or overrides the TLSPolicy currently configured on the Client with the given TLSPolicy. // -// Note: To follow best-practices for SMTP TLS connections, it is recommended to use SetTLSPortPolicy -// instead. +// This method allows the user to set a new TLSPolicy for the Client. For best practices regarding +// SMTP TLS connections, it is recommended to use SetTLSPortPolicy instead. +// +// Parameters: +// - policy: The TLSPolicy to be set for the Client. func (c *Client) SetTLSPolicy(policy TLSPolicy) { c.tlspolicy = policy } -// SetTLSPortPolicy sets or overrides the TLSPolicy that is currently set on the Client with the given -// TLSPolicy. The correct port is automatically set. +// SetTLSPortPolicy sets or overrides the TLSPolicy currently configured on the Client with the given TLSPolicy. +// The correct port is automatically set based on the specified policy. // -// If TLSMandatory or TLSOpportunistic are provided as TLSPolicy, port 587 will be used for the connection. +// If TLSMandatory or TLSOpportunistic is provided as the TLSPolicy, port 587 will be used for the connection. // If the connection fails with TLSOpportunistic, the Client will attempt to connect on port 25 using -// using an unencrypted connection as a fallback. If NoTLS is provided, the Client will always use port 25. +// an unencrypted connection as a fallback. If NoTLS is provided, the Client will always use port 25. // -// Note: If a different port has already been set otherwise using WithPort, the selected port has higher -// precedence and is used to establish the SSL/TLS connection. In this case the authmatic fallback -// mechanism is skipped at all. +// Note: If a different port has already been set using WithPort, that port takes precedence and is used +// to establish the SSL/TLS connection, skipping the automatic fallback mechanism. +// +// Parameters: +// - policy: The TLSPolicy to be set for the Client. func (c *Client) SetTLSPortPolicy(policy TLSPolicy) { if c.port == DefaultPort { c.port = DefaultPortTLS @@ -570,21 +730,29 @@ func (c *Client) SetTLSPortPolicy(policy TLSPolicy) { c.tlspolicy = policy } -// SetSSL sets or overrides wether the Client should use implicit SSL/TLS. +// SetSSL sets or overrides whether the Client should use implicit SSL/TLS. +// +// This method configures the Client to either enable or disable implicit SSL/TLS for secure communication. +// +// Parameters: +// - ssl: A boolean value indicating whether to enable (true) or disable (false) implicit SSL/TLS. func (c *Client) SetSSL(ssl bool) { c.useSSL = ssl } -// SetSSLPort sets or overrides wether the Client should use implicit SSL/TLS with optional fallback. The -// correct port is automatically set. +// SetSSLPort sets or overrides whether the Client should use implicit SSL/TLS with optional fallback. +// The correct port is automatically set. // -// If ssl is set to true, the default port 25 will be overriden with port 465. If fallback is set to true -// and the SSL/TLS connection fails, the Client will attempt to connect on port 25 using using an -// unencrypted connection. +// If ssl is set to true, the default port 25 will be overridden with port 465. If fallback is set to true +// and the SSL/TLS connection fails, the Client will attempt to connect on port 25 using an unencrypted +// connection. // -// Note: If a different port has already been set otherwise using WithPort, the selected port has higher -// precedence and is used to establish the SSL/TLS connection. In this case the authmatic fallback -// mechanism is skipped at all. +// Note: If a different port has already been set using WithPort, that port takes precedence and is used +// to establish the SSL/TLS connection, skipping the automatic fallback mechanism. +// +// Parameters: +// - ssl: A boolean value indicating whether to enable implicit SSL/TLS. +// - fallback: A boolean value indicating whether to enable fallback to an unencrypted connection. func (c *Client) SetSSLPort(ssl bool, fallback bool) { if c.port == DefaultPort { if ssl { @@ -600,12 +768,15 @@ func (c *Client) SetSSLPort(ssl bool, fallback bool) { c.useSSL = ssl } -// SetDebugLog sets or overrides wether the Client is using debug logging. The debug logger will log -// incoming and outgoing communication between the Client and the server to os.StdErr. +// SetDebugLog sets or overrides whether the Client is using debug logging. The debug logger will log incoming +// and outgoing communication between the Client and the server to os.Stderr. // -// Note: The SMTP communication might include unencrypted authentication data, depending if you are -// using SMTP authentication and the type of authentication mechanism. This could pose a data -// protection problem. Use debug logging with care. +// Note: The SMTP communication might include unencrypted authentication data, depending on whether you are using +// SMTP authentication and the type of authentication mechanism. This could pose a data protection risk. Use +// debug logging with caution. +// +// Parameters: +// - val: A boolean value indicating whether to enable (true) or disable (false) debug logging. func (c *Client) SetDebugLog(val bool) { c.useDebugLog = val if c.smtpClient != nil { @@ -613,10 +784,14 @@ func (c *Client) SetDebugLog(val bool) { } } -// SetLogger sets of overrides the custom logger currently set for the Client. The logger has to satisfy -// the log.Logger interface and is only used when debug logging is enabled on the Client. +// SetLogger sets or overrides the custom logger currently used by the Client. The logger must +// satisfy the log.Logger interface and is only utilized when debug logging is enabled on the +// Client. // -// By default we use log.Stdlog. +// By default, log.Stdlog is used if no custom logger is provided. +// +// Parameters: +// - logger: A logger that satisfies the log.Logger interface to be set for the Client. func (c *Client) SetLogger(logger log.Logger) { c.logger = logger if c.smtpClient != nil { @@ -624,12 +799,18 @@ func (c *Client) SetLogger(logger log.Logger) { } } -// SetTLSConfig sets or overrides the tls.Config that is currently set for the Client with the given value. -// An error is returned if the provided tls.Config is invalid. +// SetTLSConfig sets or overrides the tls.Config currently configured for the Client with the +// given value. An error is returned if the provided tls.Config is invalid. +// +// This method ensures that the provided tls.Config is not nil before updating the Client's +// TLS configuration. +// +// Parameters: +// - tlsconfig: A pointer to the tls.Config struct to be set for the Client. Must not be nil. +// +// Returns: +// - An error if the provided tls.Config is invalid or nil. func (c *Client) SetTLSConfig(tlsconfig *tls.Config) error { - c.mutex.Lock() - defer c.mutex.Unlock() - if tlsconfig == nil { return ErrInvalidTLSConfig } @@ -637,7 +818,12 @@ func (c *Client) SetTLSConfig(tlsconfig *tls.Config) error { return nil } -// SetUsername sets or overrides the username, the Client will use for the SMTP authentication. +// SetUsername sets or overrides the username that the Client will use for SMTP authentication. +// +// This method updates the username used by the Client for authenticating with the SMTP server. +// +// Parameters: +// - username: The username to be set for SMTP authentication. func (c *Client) SetUsername(username string) { c.user = username } From 756269644ecb073c04ba2de5eef5ac66a8968139 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 6 Oct 2024 13:39:40 +0200 Subject: [PATCH 163/181] Update SMTP client documentation with detailed descriptions Enhance the SMTP client method documentation by adding detailed explanations of the methods' purposes, parameters, return values, and operational flow. This improves clarity, making it easier for developers to understand the functionality and usage of each method. --- client.go | 163 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 139 insertions(+), 24 deletions(-) diff --git a/client.go b/client.go index dc7d46c..45fd764 100644 --- a/client.go +++ b/client.go @@ -828,20 +828,38 @@ func (c *Client) SetUsername(username string) { c.user = username } -// SetPassword sets or overrides the password, the Client will use for the SMTP authentication. +// SetPassword sets or overrides the password that the Client will use for SMTP authentication. +// +// This method updates the password used by the Client for authenticating with the SMTP server. +// +// Parameters: +// - password: The password to be set for SMTP authentication. func (c *Client) SetPassword(password string) { c.pass = password } -// SetSMTPAuth sets or overrides the SMTPAuthType that is currently set on the Client for the SMTP +// SetSMTPAuth sets or overrides the SMTPAuthType currently configured on the Client for SMTP // authentication. +// +// This method updates the authentication type used by the Client for authenticating with the +// SMTP server and resets any custom SMTP authentication mechanism. +// +// Parameters: +// - authtype: The SMTPAuthType to be set for the Client. func (c *Client) SetSMTPAuth(authtype SMTPAuthType) { c.smtpAuthType = authtype c.smtpAuth = nil } -// SetSMTPAuthCustom sets or overrides the custom SMTP authentication mechanism currently set for -// the Client. The provided authentication mechanism has to satisfy the smtp.Auth interface. +// SetSMTPAuthCustom sets or overrides the custom SMTP authentication mechanism currently +// configured for the Client. The provided authentication mechanism must satisfy the +// smtp.Auth interface. +// +// This method updates the authentication mechanism used by the Client for authenticating +// with the SMTP server and sets the authentication type to SMTPAuthCustom. +// +// Parameters: +// - smtpAuth: The custom SMTP authentication mechanism to be set for the Client. func (c *Client) SetSMTPAuthCustom(smtpAuth smtp.Auth) { c.smtpAuth = smtpAuth c.smtpAuthType = SMTPAuthCustom @@ -849,15 +867,19 @@ func (c *Client) SetSMTPAuthCustom(smtpAuth smtp.Auth) { // DialWithContext establishes a connection to the server using the provided context.Context. // -// Before connecting to the server, the function will add a deadline of 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 +// before connecting to the server. After dialing the defined DialContextFunc and successfully +// establishing the connection, it sends the HELO/EHLO SMTP command, followed by optional +// STARTTLS and SMTP AUTH commands. If debug logging is enabled, it attaches the log.Logger. // -// After dialing the DialContextFunc defined in the Client and successfully establishing the -// connection to the SMTP server, it will send the HELO/EHLO SMTP command followed by the -// optional STARTTLS and SMTP AUTH commands. It will also attach the log.Logger in case -// debug logging is enabled on the Client. +// After this method is called, the Client will have an active (cancelable) connection to the +// SMTP server. // -// From this point in time the Client has an active (cancelable) connection to the SMTP server. +// Parameters: +// - dialCtx: The context.Context used to control the connection timeout and cancellation. +// +// Returns: +// - An error if the connection to the SMTP server fails or any subsequent command fails. func (c *Client) DialWithContext(dialCtx context.Context) error { c.mutex.Lock() defer c.mutex.Unlock() @@ -914,8 +936,15 @@ func (c *Client) DialWithContext(dialCtx context.Context) error { return nil } -// Close terminates the connection to the SMTP server, returning an error if the disconnection fails. -// If the connection is already closed, we considered this a no-op and disregard any error. +// Close terminates the connection to the SMTP server, returning an error if the disconnection +// fails. If the connection is already closed, this method is a no-op and disregards any error. +// +// This function checks if the Client's SMTP connection is active. If not, it simply returns +// without any action. If the connection is active, it attempts to gracefully close the +// connection using the Quit method. +// +// Returns: +// - An error if the disconnection fails; otherwise, returns nil. func (c *Client) Close() error { if !c.smtpClient.HasConnection() { return nil @@ -928,6 +957,13 @@ func (c *Client) Close() error { } // Reset sends an SMTP RSET command to reset the state of the current SMTP session. +// +// This method checks the connection to the SMTP server and, if the connection is valid, +// it sends an RSET command to reset the session state. If the connection is invalid or +// the command fails, an error is returned. +// +// Returns: +// - An error if the connection check fails or if sending the RSET command fails; otherwise, returns nil. func (c *Client) Reset() error { if err := c.checkConn(); err != nil { return err @@ -939,16 +975,38 @@ func (c *Client) Reset() error { return nil } -// DialAndSend establishes a connection to the server and sends out the provided Msg. It will call -// DialAndSendWithContext with an empty Context.Background +// DialAndSend establishes a connection to the server and sends out the provided Msg. +// It calls DialAndSendWithContext with an empty Context.Background. +// +// This method simplifies the process of connecting to the SMTP server and sending messages +// by using a default context. It prepares the messages for sending and ensures the connection +// is established before attempting to send them. +// +// Parameters: +// - messages: A variadic list of pointers to Msg objects to be sent. +// +// Returns: +// - An error if the connection fails or if sending the messages fails; otherwise, returns nil. func (c *Client) DialAndSend(messages ...*Msg) error { ctx := context.Background() return c.DialAndSendWithContext(ctx, messages...) } -// DialAndSendWithContext establishes a connection to the SMTP server using DialWithContext using the -// provided context.Context, then sends out the given Msg. After successful delivery the Client -// will close the connection to the server. +// DialAndSendWithContext establishes a connection to the SMTP server using DialWithContext +// with the provided context.Context, then sends out the given Msg. After successful delivery, +// the Client will close the connection to the server. +// +// This method first attempts to connect to the SMTP server using the provided context. +// Upon successful connection, it sends the specified messages and ensures that the connection +// is closed after the operation, regardless of success or failure in sending the messages. +// +// Parameters: +// - ctx: The context.Context to control the connection timeout and cancellation. +// - messages: A variadic list of pointers to Msg objects to be sent. +// +// Returns: +// - An error if the connection fails, if sending the messages fails, or if closing the +// connection fails; otherwise, returns nil. func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) error { if err := c.DialWithContext(ctx); err != nil { return fmt.Errorf("dial failed: %w", err) @@ -966,9 +1024,18 @@ func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) e return nil } -// auth attempts to authenticate the client using SMTP AUTH mechanisms. It checks the connection, determines -// the supported authentication methods, and applies the appropriate authentication type. Returns an error if -// authentication fails. +// auth attempts to authenticate the client using SMTP AUTH mechanisms. It checks the connection, +// 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. +// Finally, it attempts to authenticate the client using the selected method. +// +// Returns: +// - An error if the connection check fails, if no supported authentication method is found, +// or if the authentication process fails. func (c *Client) auth() error { if err := c.checkConn(); err != nil { return fmt.Errorf("failed to authenticate: %w", err) @@ -1041,8 +1108,21 @@ func (c *Client) auth() error { return nil } -// sendSingleMsg sends out a single message and returns an error if the transmission/delivery fails. -// It is invoked by the public Send methods +// sendSingleMsg sends out a single message and returns an error if the transmission or +// delivery fails. It is invoked by the public Send methods. +// +// This method handles the process of sending a single email message through the SMTP +// client. It performs several checks and operations, including verifying the encoding, +// retrieving the sender and recipient addresses, and managing delivery status notifications +// (DSN). It attempts to send the message and handles any errors that occur during the +// transmission process, ensuring that any necessary cleanup is performed (such as resetting +// the SMTP client if an error occurs). +// +// Parameters: +// - message: A pointer to the Msg object representing the email message to be sent. +// +// Returns: +// - An error if any part of the sending process fails; otherwise, returns nil. func (c *Client) sendSingleMsg(message *Msg) error { c.mutex.Lock() defer c.mutex.Unlock() @@ -1141,7 +1221,18 @@ func (c *Client) sendSingleMsg(message *Msg) error { return nil } -// checkConn makes sure that a required server connection is available and extends the connection deadline +// checkConn ensures that a required server connection is available and extends the connection +// deadline. +// +// This method verifies whether there is an active connection to the SMTP server. If there is no +// connection, it returns an error. If the "noNoop" flag is not set, it sends a NOOP command to +// the server to confirm the connection is still valid. Finally, it updates the connection +// deadline based on the specified timeout value. If any operation fails, the appropriate error +// is returned. +// +// Returns: +// - An error if there is no active connection, if the NOOP command fails, or if extending +// the deadline fails; otherwise, returns nil. func (c *Client) checkConn() error { if !c.smtpClient.HasConnection() { return ErrNoActiveConnection @@ -1160,11 +1251,25 @@ func (c *Client) checkConn() error { } // serverFallbackAddr returns the currently set combination of hostname and fallback port. +// +// This method constructs and returns the server address using the host and fallback port +// currently configured for the Client. It is useful for establishing a connection when +// the primary port is unavailable. +// +// Returns: +// - A string representing the server address in the format "host:fallbackPort". func (c *Client) serverFallbackAddr() string { return fmt.Sprintf("%s:%d", c.host, c.fallbackPort) } // setDefaultHelo sets the HELO/EHLO hostname to the local machine's hostname. +// +// This method retrieves the local hostname using the operating system's hostname function +// and sets it as the HELO/EHLO string for the Client. If retrieving the hostname fails, +// an error is returned. +// +// Returns: +// - An error if there is a failure in reading the local hostname; otherwise, returns nil. func (c *Client) setDefaultHelo() error { hostname, err := os.Hostname() if err != nil { @@ -1176,6 +1281,16 @@ func (c *Client) setDefaultHelo() error { // tls establishes a TLS connection based on the client's TLS policy and configuration. // Returns an error if no active connection exists or if a TLS error occurs. +// +// This method first checks if there is an active connection to the SMTP server. If SSL is not +// being used and the TLS policy is not set to NoTLS, it checks for STARTTLS support. Depending +// on the TLS policy (mandatory or opportunistic), it may initiate a TLS connection using the +// StartTLS method. The method also retrieves the TLS connection state to determine if the +// connection is encrypted and returns any errors encountered during these processes. +// +// Returns: +// - An error if there is no active connection, if STARTTLS is required but not supported, +// or if there are issues during the TLS handshake; otherwise, returns nil. func (c *Client) tls() error { if !c.smtpClient.HasConnection() { return ErrNoActiveConnection From 3e8706d52eb18edbb1a13c5f5c6be5516465ac89 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 6 Oct 2024 13:41:45 +0200 Subject: [PATCH 164/181] Refactor Send method documentation in client files Updated the documentation for the Send method in client_119.go and client_120.go to provide clearer explanations, include detailed descriptions of parameters, and specify return values. Ensured consistency across files by elaborating on error handling and connection checks. --- client_119.go | 19 ++++++++++++++++--- client_120.go | 17 ++++++++++++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/client_119.go b/client_119.go index 24a0118..093967e 100644 --- a/client_119.go +++ b/client_119.go @@ -10,9 +10,22 @@ package mail import "errors" // Send attempts to send one or more Msg using the Client connection to the SMTP server. -// If the Client has no active connection to the server, Send will fail with an error. For each of the -// provided Msg it will associate a SendError to the Msg in case there of a transmission or delivery -// error. +// If the Client has no active connection to the server, Send will fail with an error. For each +// of the provided Msg, it will associate a SendError with the Msg in case of a transmission +// or delivery error. +// +// This method first checks for an active connection to the SMTP server. If the connection is +// not valid, it returns a SendError. It then iterates over the provided messages, attempting +// to send each one. If an error occurs during sending, the method records the error and +// associates it with the corresponding Msg. If multiple errors are encountered, it aggregates +// them into a single SendError to be returned. +// +// Parameters: +// - messages: A variadic list of pointers to Msg objects to be sent. +// +// Returns: +// - An error that represents the sending result, which may include multiple SendErrors if +// any occurred; otherwise, returns nil. func (c *Client) Send(messages ...*Msg) error { if err := c.checkConn(); err != nil { return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} diff --git a/client_120.go b/client_120.go index c6049eb..012a4f7 100644 --- a/client_120.go +++ b/client_120.go @@ -12,9 +12,20 @@ import ( ) // Send attempts to send one or more Msg using the Client connection to the SMTP server. -// If the Client has no active connection to the server, Send will fail with an error. For each of the -// provided Msg it will associate a SendError to the Msg in case there of a transmission or delivery -// error. +// If the Client has no active connection to the server, Send will fail with an error. For each +// of the provided Msg, it will associate a SendError with the Msg in case of a transmission +// or delivery error. +// +// This method first checks for an active connection to the SMTP server. If the connection is +// not valid, it returns an error wrapped in a SendError. It then iterates over the provided +// messages, attempting to send each one. If an error occurs during sending, the method records +// the error and associates it with the corresponding Msg. +// +// Parameters: +// - messages: A variadic list of pointers to Msg objects to be sent. +// +// Returns: +// - An error that aggregates any SendErrors encountered during the sending process; otherwise, returns nil. func (c *Client) Send(messages ...*Msg) (returnErr error) { if err := c.checkConn(); err != nil { returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} From 2c1082fe42c22f1001aa47f7896fdb16df03e9d1 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 6 Oct 2024 13:51:36 +0200 Subject: [PATCH 165/181] Enhance EML parsing function documentation Added detailed doc comments to EML parsing functions, specifying parameters, return values, and providing thorough explanations of functionalities to improve code understandability and maintainability. This helps future developers and users comprehend the usage and behavior of these functions more effectively. --- eml.go | 185 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 176 insertions(+), 9 deletions(-) diff --git a/eml.go b/eml.go index b57ad3c..35cd90d 100644 --- a/eml.go +++ b/eml.go @@ -18,13 +18,35 @@ import ( "strings" ) -// EMLToMsgFromString will parse a given EML string and returns a pre-filled Msg pointer. +// EMLToMsgFromString parses a given EML string and returns a pre-filled Msg pointer. +// +// This function takes an EML formatted string, converts it into a bytes buffer, and then +// calls EMLToMsgFromReader to parse the buffer and create a Msg object. This provides a +// convenient way to convert EML strings directly into Msg objects. +// +// Parameters: +// - emlString: A string containing the EML formatted message. +// +// Returns: +// - A pointer to the Msg object populated with the parsed data, and an error if parsing +// fails. func EMLToMsgFromString(emlString string) (*Msg, error) { eb := bytes.NewBufferString(emlString) return EMLToMsgFromReader(eb) } -// EMLToMsgFromReader will parse a reader that holds EML content and returns a pre-filled Msg pointer. +// EMLToMsgFromReader parses a reader that holds EML content and returns a pre-filled Msg pointer. +// +// This function reads EML content from the provided io.Reader and populates a Msg object +// with the parsed data. It initializes the Msg and extracts headers and body parts from +// the EML content. Any errors encountered during parsing are returned. +// +// Parameters: +// - reader: An io.Reader containing the EML formatted message. +// +// Returns: +// - A pointer to the Msg object populated with the parsed data, and an error if parsing +// fails. func EMLToMsgFromReader(reader io.Reader) (*Msg, error) { msg := &Msg{ addrHeader: make(map[AddrHeader][]*netmail.Address), @@ -45,7 +67,19 @@ func EMLToMsgFromReader(reader io.Reader) (*Msg, error) { return msg, nil } -// EMLToMsgFromFile will open and parse a .eml file at a provided file path and returns a pre-filled Msg pointer. +// EMLToMsgFromFile opens and parses a .eml file at a provided file path and returns a +// pre-filled Msg pointer. +// +// This function attempts to read and parse an EML file located at the specified file path. +// It initializes a Msg object and populates it with the parsed headers and body. Any errors +// encountered during the file operations or parsing are returned. +// +// Parameters: +// - filePath: The path to the .eml file to be parsed. +// +// Returns: +// - A pointer to the Msg object populated with the parsed data, and an error if parsing +// fails. func EMLToMsgFromFile(filePath string) (*Msg, error) { msg := &Msg{ addrHeader: make(map[AddrHeader][]*netmail.Address), @@ -67,6 +101,18 @@ func EMLToMsgFromFile(filePath string) (*Msg, error) { } // parseEML parses the EML's headers and body and inserts the parsed values into the Msg. +// +// This function extracts relevant header fields and body content from the parsed EML message +// and stores them in the provided Msg object. It handles various header types and body +// parts, ensuring that the Msg is correctly populated with all necessary information. +// +// Parameters: +// - parsedMsg: A pointer to the netmail.Message containing the parsed EML data. +// - bodybuf: A bytes.Buffer containing the body content of the EML message. +// - msg: A pointer to the Msg object to be populated with the parsed data. +// +// Returns: +// - An error if any issues occur during the parsing process; otherwise, returns nil. func parseEML(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error { if err := parseEMLHeaders(&parsedMsg.Header, msg); err != nil { return fmt.Errorf("failed to parse EML headers: %w", err) @@ -78,6 +124,17 @@ func parseEML(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error } // readEML opens an EML file and uses net/mail to parse the header and body. +// +// This function opens the specified EML file for reading and utilizes the net/mail package +// to parse the message's headers and body. It returns the parsed message and a buffer +// containing the body content, along with any errors encountered during the process. +// +// Parameters: +// - filePath: The path to the EML file to be opened and parsed. +// +// Returns: +// - A pointer to the parsed netmail.Message, a bytes.Buffer containing the body, and an +// error if any issues occur during file operations or parsing. func readEML(filePath string) (*netmail.Message, *bytes.Buffer, error) { fileHandle, err := os.Open(filePath) if err != nil { @@ -90,6 +147,18 @@ func readEML(filePath string) (*netmail.Message, *bytes.Buffer, error) { } // readEMLFromReader uses net/mail to parse the header and body from a given io.Reader. +// +// This function reads the EML content from the provided io.Reader and uses the net/mail +// package to parse the message's headers and body. It returns the parsed netmail.Message +// along with a bytes.Buffer containing the body content. Any errors encountered during +// the parsing process are returned. +// +// Parameters: +// - reader: An io.Reader containing the EML formatted message. +// +// Returns: +// - A pointer to the parsed netmail.Message, a bytes.Buffer containing the body, and an +// error if any issues occur during parsing. func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error) { parsedMsg, err := netmail.ReadMessage(reader) if err != nil { @@ -104,8 +173,18 @@ func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error return parsedMsg, &buf, nil } -// parseEMLHeaders will check the EML headers for the most common headers and set the according settings -// in the Msg. +// parseEMLHeaders parses the EML's headers and populates the Msg with relevant information. +// +// This function checks the EML headers for common headers and sets the corresponding fields +// in the Msg object. It extracts address headers, content types, and other relevant data +// for further processing. +// +// Parameters: +// - mailHeader: A pointer to the netmail.Header containing the EML headers. +// - msg: A pointer to the Msg object to be populated with parsed header information. +// +// Returns: +// - An error if parsing the headers fails; otherwise, returns nil. func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error { commonHeaders := []Header{ HeaderContentType, HeaderImportance, HeaderInReplyTo, HeaderListUnsubscribe, @@ -173,7 +252,19 @@ func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error { return nil } -// parseEMLBodyParts parses the body of a EML based on the different content types and encodings. +// parseEMLBodyParts parses the body of an EML based on the different content types and encodings. +// +// This function examines the content type of the parsed EML message and processes the body +// parts accordingly. It handles both plain text and multipart types, ensuring that the +// Msg object is populated with the appropriate body content. +// +// Parameters: +// - parsedMsg: A pointer to the netmail.Message containing the parsed EML data. +// - bodybuf: A bytes.Buffer containing the body content of the EML message. +// - msg: A pointer to the Msg object to be populated with the parsed body content. +// +// Returns: +// - An error if any issues occur during the body parsing process; otherwise, returns nil. func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error { // Extract the transfer encoding of the body mediatype, params, err := mime.ParseMediaType(parsedMsg.Header.Get(HeaderContentType.String())) @@ -210,7 +301,20 @@ func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *M return nil } -// parseEMLBodyPlain parses the mail body of plain type mails. +// parseEMLBodyPlain parses the mail body of plain type messages. +// +// This function handles the parsing of plain text messages based on their encoding. It +// identifies the content transfer encoding and decodes the body content accordingly, +// storing the result in the provided Msg object. +// +// Parameters: +// - mediatype: The media type of the message (e.g., text/plain). +// - parsedMsg: A pointer to the netmail.Message containing the parsed EML data. +// - bodybuf: A bytes.Buffer containing the body content of the EML message. +// - msg: A pointer to the Msg object to be populated with the parsed body content. +// +// Returns: +// - An error if any issues occur during the parsing of the plain body; otherwise, returns nil. func parseEMLBodyPlain(mediatype string, parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error { contentTransferEnc := parsedMsg.Header.Get(HeaderContentTransferEnc.String()) // If no Content-Transfer-Encoding is set, we can imply 7bit US-ASCII encoding @@ -248,7 +352,20 @@ func parseEMLBodyPlain(mediatype string, parsedMsg *netmail.Message, bodybuf *by return fmt.Errorf("unsupported Content-Transfer-Encoding") } -// parseEMLMultipart parses a multipart body part of a EML +// parseEMLMultipart parses a multipart body part of an EML message. +// +// This function handles the parsing of multipart messages, extracting the individual parts +// and determining their content types. It processes each part according to its content type +// and ensures that all relevant data is stored in the Msg object. +// +// Parameters: +// - params: A map containing the parameters from the multipart content type. +// - bodybuf: A bytes.Buffer containing the body content of the EML message. +// - msg: A pointer to the Msg object to be populated with the parsed body parts. +// +// Returns: +// - An error if any issues occur during the parsing of the multipart body; otherwise, +// returns nil. func parseEMLMultipart(params map[string]string, bodybuf *bytes.Buffer, msg *Msg) error { boundary, ok := params["boundary"] if !ok { @@ -349,6 +466,14 @@ ReadNextPart: } // parseEMLEncoding parses and determines the encoding of the message. +// +// This function extracts the content transfer encoding from the EML headers and sets the +// corresponding encoding in the Msg object. It ensures that the correct encoding is used +// for further processing of the message content. +// +// Parameters: +// - mailHeader: A pointer to the netmail.Header containing the EML headers. +// - msg: A pointer to the Msg object to be updated with the encoding information. func parseEMLEncoding(mailHeader *netmail.Header, msg *Msg) { if value := mailHeader.Get(HeaderContentTransferEnc.String()); value != "" { switch { @@ -363,6 +488,14 @@ func parseEMLEncoding(mailHeader *netmail.Header, msg *Msg) { } // parseEMLContentTypeCharset parses and determines the charset and content type of the message. +// +// This function extracts the content type and charset from the EML headers, setting them +// appropriately in the Msg object. It ensures that the Msg object is configured with the +// correct content type for further processing. +// +// Parameters: +// - mailHeader: A pointer to the netmail.Header containing the EML headers. +// - msg: A pointer to the Msg object to be updated with content type and charset information. func parseEMLContentTypeCharset(mailHeader *netmail.Header, msg *Msg) { if value := mailHeader.Get(HeaderContentType.String()); value != "" { contentType, optional := parseMultiPartHeader(value) @@ -377,6 +510,17 @@ func parseEMLContentTypeCharset(mailHeader *netmail.Header, msg *Msg) { } // handleEMLMultiPartBase64Encoding sets the content body of a base64 encoded Part. +// +// This function decodes the base64 encoded content of a multipart part and stores the +// resulting content in the provided Part object. It handles any errors that occur during +// the decoding process. +// +// Parameters: +// - multiPartData: A byte slice containing the base64 encoded data. +// - part: A pointer to the Part object where the decoded content will be stored. +// +// Returns: +// - An error if the base64 decoding fails; otherwise, returns nil. func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error { part.SetEncoding(EncodingB64) content, err := base64.StdEncoding.DecodeString(string(multiPartData)) @@ -387,7 +531,17 @@ func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error { return nil } -// parseMultiPartHeader parses a multipart header and returns the value and optional parts as separate map. +// parseMultiPartHeader parses a multipart header and returns the value and optional parts as a map. +// +// This function splits a multipart header into its main value and any optional parameters, +// returning them separately. It helps in processing multipart messages by extracting +// relevant information from headers. +// +// Parameters: +// - multiPartHeader: A string representing the multipart header to be parsed. +// +// Returns: +// - The main header value as a string and a map of optional parameters. func parseMultiPartHeader(multiPartHeader string) (header string, optional map[string]string) { optional = make(map[string]string) headerSplit := strings.SplitN(multiPartHeader, ";", 2) @@ -403,6 +557,19 @@ func parseMultiPartHeader(multiPartHeader string) (header string, optional map[s } // parseEMLAttachmentEmbed parses a multipart that is an attachment or embed. +// +// This function handles the parsing of multipart sections that are marked as attachments or +// embedded content. It processes the content disposition and sets the appropriate fields in +// the Msg object based on the parsed data. +// +// Parameters: +// - contentDisposition: A slice of strings containing the content disposition header. +// - multiPart: A pointer to the multipart.Part to be parsed. +// - msg: A pointer to the Msg object to be populated with the attachment or embed data. +// +// Returns: +// - An error if any issues occur during the parsing of attachments or embeds; otherwise, +// returns nil. func parseEMLAttachmentEmbed(contentDisposition []string, multiPart *multipart.Part, msg *Msg) error { cdType, optional := parseMultiPartHeader(contentDisposition[0]) filename := "generic.attachment" From dab9cc947a831181ba7e9a0f2277941ad3e7f153 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 6 Oct 2024 13:53:20 +0200 Subject: [PATCH 166/181] Improve documentation for String methods Enhanced comments for String methods on Charset, ContentType, and Encoding types. Detailed the purpose and usage of each method, emphasizing their role in formatted output and logging. --- encoding.go | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/encoding.go b/encoding.go index 2b559c4..9ed5666 100644 --- a/encoding.go +++ b/encoding.go @@ -184,18 +184,38 @@ const ( MIMERelated MIMEType = "related" ) -// String satisfies the fmt.Stringer interface for the Charset type. It converts a Charset into a printable format. +// String satisfies the fmt.Stringer interface for the Charset type. +// It converts a Charset into a printable format. +// +// This method returns the string representation of the Charset, allowing it to be easily +// printed or logged. +// +// Returns: +// - A string representation of the Charset. func (c Charset) String() string { return string(c) } -// String satisfies the fmt.Stringer interface for the ContentType type. It converts a ContentType into a printable -// format. +// String satisfies the fmt.Stringer interface for the ContentType type. +// It converts a ContentType into a printable format. +// +// This method returns the string representation of the ContentType, enabling its use +// in formatted output such as logging or displaying information to the user. +// +// Returns: +// - A string representation of the ContentType. func (c ContentType) String() string { return string(c) } -// String satisfies the fmt.Stringer interface for the Encoding type. It converts an Encoding into a printable format. +// String satisfies the fmt.Stringer interface for the Encoding type. +// It converts an Encoding into a printable format. +// +// This method returns the string representation of the Encoding, which can be used +// for displaying or logging purposes. +// +// Returns: +// - A string representation of the Encoding. func (e Encoding) String() string { return string(e) } From ac7fa5771aa141a375c763b10986662128273b31 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 6 Oct 2024 16:16:49 +0200 Subject: [PATCH 167/181] Refactor documentation and enhance comments Updated documentation for the `File` struct and its methods, providing clearer explanations and detailed parameter and return descriptions. Improved readability and consistency of comments across the codebase. --- file.go | 91 +++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 75 insertions(+), 16 deletions(-) diff --git a/file.go b/file.go index 77e516c..8866d97 100644 --- a/file.go +++ b/file.go @@ -12,8 +12,12 @@ import ( // FileOption is a function type used to modify properties of a File type FileOption func(*File) -// File represents a file with properties like content type, description, encoding, headers, name, and -// writer function. This can either be an attachment or an embedded file for a Msg. +// File represents a file with properties such as content type, description, encoding, headers, name, and +// a writer function. +// +// This struct can represent either an attachment or an embedded file in a Msg, and it stores relevant +// metadata such as content type and encoding, as well as a function to write the file's content to an +// io.Writer. type File struct { ContentType ContentType Desc string @@ -23,7 +27,16 @@ type File struct { Writer func(w io.Writer) (int64, error) } -// WithFileContentID sets the "Content-ID" header in the File's MIME headers to the specified id. +// WithFileContentID sets the "Content-ID" header in the File's MIME headers to the specified ID. +// +// This function updates the File's MIME headers by setting the "Content-ID" to the provided string value, +// allowing the file to be referenced by this ID within the MIME structure. +// +// Parameters: +// - id: A string representing the content ID to be set in the "Content-ID" header. +// +// Returns: +// - A FileOption function that updates the File's "Content-ID" header. func WithFileContentID(id string) FileOption { return func(f *File) { f.Header.Set(HeaderContentID.String(), id) @@ -31,27 +44,51 @@ func WithFileContentID(id string) FileOption { } // WithFileName sets the name of a File to the provided value. +// +// This function assigns the specified name to the File, updating its Name field. +// +// Parameters: +// - name: A string representing the name to be assigned to the File. +// +// Returns: +// - A FileOption function that sets the File's name. func WithFileName(name string) FileOption { return func(f *File) { f.Name = name } } -// WithFileDescription sets an optional file description for the File. The description is used in the -// Content-Description header of the MIME output. +// WithFileDescription sets an optional description for the File, which is used in the Content-Description +// header of the MIME output. +// +// This function updates the File's description, allowing an additional text description to be added to +// the MIME headers for the file. +// +// Parameters: +// - description: A string representing the description to be set in the Content-Description header. +// +// Returns: +// - A FileOption function that sets the File's description. func WithFileDescription(description string) FileOption { return func(f *File) { f.Desc = description } } -// WithFileEncoding sets the encoding type for a file. +// WithFileEncoding sets the encoding type for a File. // -// By default one should always use Base64 encoding for attachments and embeds, but there might be exceptions in -// which this might come handy. +// This function allows the specification of an encoding type for the file, typically used for attachments +// or embedded files. By default, Base64 encoding should be used, but this function can override the +// default if needed. // -// Note: that quoted-printable must never be used for attachments or embeds. If EncodingQP is provided as encoding -// to this method, it will be automatically overwritten with EncodingB64. +// Note: Quoted-printable encoding (EncodingQP) must never be used for attachments or embeds. If EncodingQP +// is passed to this function, it will be ignored and the encoding will remain unchanged. +// +// Parameters: +// - encoding: The Encoding type to be assigned to the File, unless it's EncodingQP. +// +// Returns: +// - A FileOption function that sets the File's encoding. func WithFileEncoding(encoding Encoding) FileOption { return func(f *File) { if encoding == EncodingQP { @@ -63,22 +100,44 @@ func WithFileEncoding(encoding Encoding) FileOption { // WithFileContentType sets the content type of the File. // -// By default we will try to guess the file type and its corresponding content type and fall back to -// application/octet-stream if the file type, if no matching type could be guessed. This FileOption can -// be used to override this type, in case a specific type is required. +// By default, the content type is guessed based on the file type, and if no matching type is identified, +// the default "application/octet-stream" is used. This FileOption allows overriding the guessed content +// type with a specific one if required. +// +// Parameters: +// - contentType: The ContentType to be assigned to the File. +// +// Returns: +// - A FileOption function that sets the File's content type. func WithFileContentType(contentType ContentType) FileOption { return func(f *File) { f.ContentType = contentType } } -// setHeader sets the value of a given MIME header field for the File. +// setHeader sets the value of a specified MIME header field for the File. +// +// This method updates the MIME headers of the File by assigning the provided value to the specified +// header field. +// +// Parameters: +// - header: The Header field to be updated. +// - value: A string representing the value to be set for the given header. func (f *File) setHeader(header Header, value string) { f.Header.Set(string(header), value) } -// getHeader retrieves the value of the specified MIME header field. It returns the header value and a boolean -// indicating whether the header was present or not. +// getHeader retrieves the value of the specified MIME header field. +// +// This method returns the value of the given header and a boolean indicating whether the header was found +// in the File's MIME headers. +// +// Parameters: +// - header: The Header field whose value is to be retrieved. +// +// Returns: +// - A string containing the value of the header. +// - A boolean indicating whether the header was present (true) or not (false). func (f *File) getHeader(header Header) (string, bool) { v := f.Header.Get(string(header)) return v, v != "" From 3333c784a6deb97712cc65de2118cddcf1564f41 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 6 Oct 2024 16:19:53 +0200 Subject: [PATCH 168/181] Refactor documentation for Importance methods Updated the documentation for NumString, XPrioString, and String methods in the Importance type to provide clearer descriptions of their behavior and return values. Enhanced comments for better readability and maintainability of the code. --- header.go | 43 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/header.go b/header.go index 2b25cd2..217823a 100644 --- a/header.go +++ b/header.go @@ -136,8 +136,14 @@ const ( ImportanceUrgent ) -// NumString returns a numerical string representation of the Importance, mapping ImportanceHigh and -// ImportanceUrgent to "1" and others to "0". +// NumString returns a numerical string representation of the Importance level. +// +// This method maps ImportanceHigh and ImportanceUrgent to "1", while ImportanceNonUrgent and ImportanceLow +// are mapped to "0". Other values return an empty string. +// +// Returns: +// - A string representing the numerical value of the Importance level ("1" or "0"), or an empty string +// if the Importance level is unrecognized. func (i Importance) NumString() string { switch i { case ImportanceNonUrgent: @@ -153,8 +159,14 @@ func (i Importance) NumString() string { } } -// XPrioString returns the X-Priority string representation of the Importance, mapping ImportanceHigh and -// ImportanceUrgent to "1" and others to "5". +// XPrioString returns the X-Priority string representation of the Importance level. +// +// This method maps ImportanceHigh and ImportanceUrgent to "1", while ImportanceNonUrgent and ImportanceLow +// are mapped to "5". Other values return an empty string. +// +// Returns: +// - A string representing the X-Priority value of the Importance level ("1" or "5"), or an empty string +// if the Importance level is unrecognized. func (i Importance) XPrioString() string { switch i { case ImportanceNonUrgent: @@ -170,8 +182,14 @@ func (i Importance) XPrioString() string { } } -// String satisfies the fmt.Stringer interface for the Importance type and returns the string representation of the -// Importance level. +// String satisfies the fmt.Stringer interface for the Importance type and returns the string +// representation of the Importance level. +// +// This method provides a human-readable string for each Importance level. +// +// Returns: +// - A string representing the Importance level ("non-urgent", "low", "high", or "urgent"), or an empty +// string if the Importance level is unrecognized. func (i Importance) String() string { switch i { case ImportanceNonUrgent: @@ -187,13 +205,20 @@ func (i Importance) String() string { } } -// String satisfies the fmt.Stringer interface for the Header type and returns the string representation of the Header. +// String satisfies the fmt.Stringer interface for the Header type and returns the string +// representation of the Header. +// +// Returns: +// - A string representing the Header. func (h Header) String() string { return string(h) } -// String satisfies the fmt.Stringer interface for the AddrHeader type and returns the string representation of the -// AddrHeader. +// String satisfies the fmt.Stringer interface for the AddrHeader type and returns the string +// representation of the AddrHeader. +// +// Returns: +// - A string representing the AddrHeader. func (a AddrHeader) String() string { return string(a) } From 0b105048e654fd48517958de9280b06330d402bd Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 6 Oct 2024 16:21:37 +0200 Subject: [PATCH 169/181] Refine WriteToTempFile docstring Clarify the documentation for WriteToTempFile to better explain its functionality, ensure consistency, and detail its return values. --- msg_totmpfile.go | 10 ++++++++-- msg_totmpfile_116.go | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/msg_totmpfile.go b/msg_totmpfile.go index 44c427d..b7d2ada 100644 --- a/msg_totmpfile.go +++ b/msg_totmpfile.go @@ -12,8 +12,14 @@ import ( "os" ) -// WriteToTempFile will create a temporary file and output the Msg to this file -// The method will return the filename of the temporary file +// WriteToTempFile creates a temporary file and writes the Msg content to this file. +// +// This method generates a temporary file with a ".eml" extension, writes the Msg to it, and returns the +// filename of the created temporary file. +// +// Returns: +// - A string representing the filename of the temporary file. +// - An error if the file creation or writing process fails. func (m *Msg) WriteToTempFile() (string, error) { f, err := os.CreateTemp("", "go-mail_*.eml") if err != nil { diff --git a/msg_totmpfile_116.go b/msg_totmpfile_116.go index 4611106..bb93411 100644 --- a/msg_totmpfile_116.go +++ b/msg_totmpfile_116.go @@ -12,8 +12,14 @@ import ( "io/ioutil" ) -// WriteToTempFile will create a temporary file and output the Msg to this file -// The method will return the filename of the temporary file +// WriteToTempFile creates a temporary file and writes the Msg content to this file. +// +// This method generates a temporary file with a ".eml" extension, writes the Msg to it, and returns the +// filename of the created temporary file. +// +// Returns: +// - A string representing the filename of the temporary file. +// - An error if the file creation or writing process fails. func (m *Msg) WriteToTempFile() (string, error) { f, err := ioutil.TempFile("", "go-mail_*.eml") if err != nil { From 295155ba674c91ff0356e5de4450b8a56f1e01bb Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 6 Oct 2024 16:42:50 +0200 Subject: [PATCH 170/181] Refactor and document msgWriter methods Streamline msgWriter by adding detailed documentation for each method and constant. This includes parameter descriptions, return values, and references to relevant RFCs, improving code readability and maintainability. --- msgwriter.go | 168 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 144 insertions(+), 24 deletions(-) diff --git a/msgwriter.go b/msgwriter.go index ff1b47b..ed2b41e 100644 --- a/msgwriter.go +++ b/msgwriter.go @@ -18,22 +18,39 @@ import ( "strings" ) -// MaxHeaderLength defines the maximum line length for a mail header -// RFC 2047 suggests 76 characters -const MaxHeaderLength = 76 +const ( + // MaxHeaderLength defines the maximum line length for a mail header. + // + // This constant follows the recommendation of RFC 2047, which suggests a maximum length of 76 characters. + // + // References: + // - https://datatracker.ietf.org/doc/html/rfc2047 + MaxHeaderLength = 76 -// MaxBodyLength defines the maximum line length for the mail body -// RFC 2047 suggests 76 characters -const MaxBodyLength = 76 + // MaxBodyLength defines the maximum line length for the mail body. + // + // This constant follows the recommendation of RFC 2047, which suggests a maximum length of 76 characters. + // + // References: + // - https://datatracker.ietf.org/doc/html/rfc2047 + MaxBodyLength = 76 -// SingleNewLine represents a new line that can be used by the msgWriter to issue a carriage return -const SingleNewLine = "\r\n" + // SingleNewLine represents a single newline character sequence ("\r\n"). + // + // This constant can be used by the msgWriter to issue a carriage return when writing mail content. + SingleNewLine = "\r\n" -// DoubleNewLine represents a double new line that can be used by the msgWriter to -// indicate a new segement of the mail -const DoubleNewLine = "\r\n\r\n" + // DoubleNewLine represents a double newline character sequence ("\r\n\r\n"). + // + // This constant can be used by the msgWriter to indicate a new segment of the mail when writing mail content. + DoubleNewLine = "\r\n\r\n" +) -// msgWriter handles the I/O to the io.WriteCloser of the SMTP client +// msgWriter handles the I/O operations for writing to the io.WriteCloser of the SMTP client. +// +// This struct keeps track of the number of bytes written, the character set used, and the depth of the +// current multipart section. It also handles encoding, error tracking, and managing multipart and part +// writers for constructing the email message body. type msgWriter struct { bytesWritten int64 charset Charset @@ -45,7 +62,18 @@ type msgWriter struct { writer io.Writer } -// Write implements the io.Writer interface for msgWriter +// Write implements the io.Writer interface for msgWriter. +// +// This method writes the provided payload to the underlying writer. It keeps track of the number of bytes +// written and handles any errors encountered during the writing process. If a previous error exists, it +// prevents further writing and returns the error. +// +// Parameters: +// - payload: A byte slice containing the data to be written. +// +// Returns: +// - The number of bytes successfully written. +// - An error if the writing process fails, or if a previous error was encountered. func (mw *msgWriter) Write(payload []byte) (int, error) { if mw.err != nil { return 0, fmt.Errorf("failed to write due to previous error: %w", mw.err) @@ -57,7 +85,19 @@ func (mw *msgWriter) Write(payload []byte) (int, error) { return n, mw.err } -// writeMsg formats the message and sends it to its io.Writer +// writeMsg formats the message and writes it to the msgWriter's io.Writer. +// +// This method handles the process of writing the message headers and body content, including handling +// multipart structures (e.g., mixed, related, alternative), PGP types, and attachments/embeds. It sets the +// required headers (e.g., "From", "To", "Cc") and iterates over the message parts, writing them to the +// output writer. +// +// Parameters: +// - msg: A pointer to the Msg struct containing the message data and headers to be written. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2045 (Multipurpose Internet Mail Extensions - MIME) +// - https://datatracker.ietf.org/doc/html/rfc5322 (Internet Message Format) func (mw *msgWriter) writeMsg(msg *Msg) { msg.addDefaultHeader() msg.checkUserAgent() @@ -136,7 +176,13 @@ func (mw *msgWriter) writeMsg(msg *Msg) { } } -// writeGenHeader writes out all generic headers to the msgWriter +// writeGenHeader writes out all generic headers to the msgWriter. +// +// This function extracts all generic headers from the provided Msg object, sorts them, and writes them +// to the msgWriter in alphabetical order. +// +// Parameters: +// - msg: The Msg object containing the headers to be written. func (mw *msgWriter) writeGenHeader(msg *Msg) { keys := make([]string, 0, len(msg.genHeader)) for key := range msg.genHeader { @@ -148,14 +194,32 @@ func (mw *msgWriter) writeGenHeader(msg *Msg) { } } -// writePreformatedHeader writes out all preformated generic headers to the msgWriter +// writePreformattedGenHeader writes out all preformatted generic headers to the msgWriter. +// +// This function iterates over all preformatted generic headers from the provided Msg object and writes +// them to the msgWriter in the format "key: value" followed by a newline. +// +// Parameters: +// - msg: The Msg object containing the preformatted headers to be written. func (mw *msgWriter) writePreformattedGenHeader(msg *Msg) { for key, val := range msg.preformHeader { mw.writeString(fmt.Sprintf("%s: %s%s", key, val, SingleNewLine)) } } -// startMP writes a multipart beginning +// startMP writes a multipart beginning. +// +// This function initializes a multipart writer for the msgWriter using the specified MIME type and +// boundary. It sets the Content-Type header to indicate the multipart type and writes the boundary +// information. If a boundary is provided, it is set explicitly; otherwise, a default boundary is +// generated. It also handles writing a new part when nested multipart structures are used. +// +// Parameters: +// - mimeType: The MIME type of the multipart content (e.g., "mixed", "alternative"). +// - boundary: The boundary string separating different parts of the multipart message. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2046 func (mw *msgWriter) startMP(mimeType MIMEType, boundary string) { multiPartWriter := multipart.NewWriter(mw) if boundary != "" { @@ -175,7 +239,10 @@ func (mw *msgWriter) startMP(mimeType MIMEType, boundary string) { mw.depth++ } -// stopMP closes the multipart +// stopMP closes the multipart. +// +// This function closes the current multipart writer if there is an active multipart structure. +// It decreases the depth level of multipart nesting. func (mw *msgWriter) stopMP() { if mw.depth > 0 { mw.err = mw.multiPartWriter[mw.depth-1].Close() @@ -183,7 +250,17 @@ func (mw *msgWriter) stopMP() { } } -// addFiles adds the attachments/embeds file content to the mail body +// addFiles adds the attachments/embeds file content to the mail body. +// +// This function iterates through the list of files, setting necessary headers for each file, +// including Content-Type, Content-Transfer-Encoding, Content-Disposition, and Content-ID +// (if the file is an embed). It determines the appropriate MIME type for each file based on +// its extension or the provided ContentType. It writes file headers and file content +// to the mail body using the appropriate encoding. +// +// Parameters: +// - files: A slice of File objects to be added to the mail body. +// - isAttachment: A boolean indicating whether the files are attachments (true) or embeds (false). func (mw *msgWriter) addFiles(files []*File, isAttachment bool) { for _, file := range files { encoding := EncodingB64 @@ -242,12 +319,29 @@ func (mw *msgWriter) addFiles(files []*File, isAttachment bool) { } } -// newPart creates a new MIME multipart io.Writer and sets the partwriter to it +// newPart creates a new MIME multipart io.Writer and sets the partWriter to it. +// +// This function creates a new MIME part using the provided header information and assigns it +// to the partWriter. It interacts with the current multipart writer at the specified depth +// to create the part. +// +// Parameters: +// - header: A map containing the header fields and their corresponding values for the new part. func (mw *msgWriter) newPart(header map[string][]string) { mw.partWriter, mw.err = mw.multiPartWriter[mw.depth-1].CreatePart(header) } -// writePart writes the corresponding part to the Msg body +// writePart writes the corresponding part to the Msg body. +// +// This function writes a MIME part to the message body, setting the appropriate headers such +// as Content-Type and Content-Transfer-Encoding. It determines the charset for the part, +// either using the part's own charset or a fallback charset if none is specified. If the part +// is at the top level (depth 0), headers are written directly. For nested parts, it creates +// a new MIME part with the provided headers. +// +// Parameters: +// - part: The Part object containing the data to be written. +// - charset: The Charset used as a fallback if the part does not specify one. func (mw *msgWriter) writePart(part *Part, charset Charset) { partCharset := part.charset if partCharset.String() == "" { @@ -272,7 +366,14 @@ func (mw *msgWriter) writePart(part *Part, charset Charset) { mw.writeBody(part.writeFunc, part.encoding) } -// writeString writes a string into the msgWriter's io.Writer interface +// writeString writes a string into the msgWriter's io.Writer interface. +// +// This function writes the given string to the msgWriter's underlying writer. It checks for +// existing errors before performing the write operation. It also tracks the number of bytes +// written and updates the bytesWritten field accordingly. +// +// Parameters: +// - s: The string to be written. func (mw *msgWriter) writeString(s string) { if mw.err != nil { return @@ -282,7 +383,16 @@ func (mw *msgWriter) writeString(s string) { mw.bytesWritten += int64(n) } -// writeHeader writes a header into the msgWriter's io.Writer +// writeHeader writes a header into the msgWriter's io.Writer. +// +// This function writes a header key and its associated values to the msgWriter. It ensures +// proper formatting of long headers by inserting line breaks as needed. The header values +// are joined and split into words to ensure compliance with the maximum header length +// (MaxHeaderLength). After processing the header, it is written to the underlying writer. +// +// Parameters: +// - key: The Header key to be written. +// - values: A variadic parameter representing the values associated with the header. func (mw *msgWriter) writeHeader(key Header, values ...string) { buffer := strings.Builder{} charLength := MaxHeaderLength - 2 @@ -317,7 +427,17 @@ func (mw *msgWriter) writeHeader(key Header, values ...string) { mw.writeString("\r\n") } -// writeBody writes an io.Reader into an io.Writer using provided Encoding +// writeBody writes an io.Reader into an io.Writer using the provided Encoding. +// +// This function writes data from an io.Reader to the underlying writer using a specified +// encoding (quoted-printable, base64, or no encoding). It handles encoding of the content +// and manages writing the encoded data to the appropriate writer, depending on the depth +// (whether the data is part of a multipart structure or not). It also tracks the number +// of bytes written and manages any errors encountered during the process. +// +// Parameters: +// - writeFunc: A function that writes the body content to the given io.Writer. +// - encoding: The encoding type to use when writing the content (e.g., base64, quoted-printable). func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encoding Encoding) { var writer io.Writer var encodedWriter io.WriteCloser From e640f2df4612fe55656f8036292dc818b9ef45d0 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 6 Oct 2024 16:54:18 +0200 Subject: [PATCH 171/181] Add detailed comments and return descriptions to `Part` methods Enhanced the documentation of `Part` struct and its methods to provide clearer explanations, including parameter and return descriptions. This improves code readability and helps developers understand the functionality and usage of each method. --- part.go | 136 ++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 118 insertions(+), 18 deletions(-) diff --git a/part.go b/part.go index 7c76b7d..5be8059 100644 --- a/part.go +++ b/part.go @@ -12,7 +12,11 @@ import ( // PartOption returns a function that can be used for grouping Part options type PartOption func(*Part) -// Part is a part of the Msg +// Part is a part of the Msg. +// +// This struct represents a single part of a multipart message. Each part has a content type, +// charset, optional description, encoding, and a function to write its content to an io.Writer. +// It also includes a flag to mark the part as deleted. type Part struct { contentType ContentType charset Charset @@ -22,7 +26,14 @@ type Part struct { writeFunc func(io.Writer) (int64, error) } -// GetContent executes the WriteFunc of the Part and returns the content as byte slice +// GetContent executes the WriteFunc of the Part and returns the content as a byte slice. +// +// This function runs the part's writeFunc to write its content into a buffer and then returns +// the content as a byte slice. If an error occurs during the writing process, it is returned. +// +// Returns: +// - A byte slice containing the part's content. +// - An error if the writeFunc encounters an issue. func (p *Part) GetContent() ([]byte, error) { var b bytes.Buffer if _, err := p.writeFunc(&b); err != nil { @@ -31,83 +42,172 @@ func (p *Part) GetContent() ([]byte, error) { return b.Bytes(), nil } -// GetCharset returns the currently set Charset of the Part +// GetCharset returns the currently set Charset of the Part. +// +// This function returns the Charset that is currently set for the Part. +// +// Returns: +// - The Charset of the Part. func (p *Part) GetCharset() Charset { return p.charset } -// GetContentType returns the currently set ContentType of the Part +// GetContentType returns the currently set ContentType of the Part. +// +// This function returns the ContentType that is currently set for the Part. +// +// Returns: +// - The ContentType of the Part. func (p *Part) GetContentType() ContentType { return p.contentType } -// GetEncoding returns the currently set Encoding of the Part +// GetEncoding returns the currently set Encoding of the Part. +// +// This function returns the Encoding that is currently set for the Part. +// +// Returns: +// - The Encoding of the Part. func (p *Part) GetEncoding() Encoding { return p.encoding } -// GetWriteFunc returns the currently set WriterFunc of the Part +// GetWriteFunc returns the currently set WriteFunc of the Part. +// +// This function returns the WriteFunc that is currently set for the Part, which writes +// the part's content to an io.Writer. +// +// Returns: +// - The WriteFunc of the Part, which is a function that takes an io.Writer and returns +// the number of bytes written and an error (if any). func (p *Part) GetWriteFunc() func(io.Writer) (int64, error) { return p.writeFunc } -// GetDescription returns the currently set Content-Description of the Part +// GetDescription returns the currently set Content-Description of the Part. +// +// This function returns the Content-Description that is currently set for the Part. +// +// Returns: +// - The Content-Description of the Part as a string. func (p *Part) GetDescription() string { return p.description } -// SetContent overrides the content of the Part with the given string +// SetContent overrides the content of the Part with the given string. +// +// This function sets the content of the Part by creating a new writeFunc that writes the +// provided string content to an io.Writer. +// +// Parameters: +// - content: The string that will replace the current content of the Part. func (p *Part) SetContent(content string) { buffer := bytes.NewBufferString(content) p.writeFunc = writeFuncFromBuffer(buffer) } -// SetContentType overrides the ContentType of the Part +// SetContentType overrides the ContentType of the Part. +// +// This function sets a new ContentType for the Part, replacing the existing one. +// +// Parameters: +// - contentType: The new ContentType to be set for the Part. func (p *Part) SetContentType(contentType ContentType) { p.contentType = contentType } -// SetCharset overrides the Charset of the Part +// SetCharset overrides the Charset of the Part. +// +// This function sets a new Charset for the Part, replacing the existing one. +// +// Parameters: +// - charset: The new Charset to be set for the Part. func (p *Part) SetCharset(charset Charset) { p.charset = charset } -// SetEncoding creates a new mime.WordEncoder based on the encoding setting of the message +// SetEncoding creates a new mime.WordEncoder based on the encoding setting of the message. +// +// This function sets a new Encoding for the Part, replacing the existing one. +// +// Parameters: +// - encoding: The new Encoding to be set for the Part. func (p *Part) SetEncoding(encoding Encoding) { p.encoding = encoding } -// SetDescription overrides the Content-Description of the Part +// SetDescription overrides the Content-Description of the Part. +// +// This function sets a new Content-Description for the Part, replacing the existing one. +// +// Parameters: +// - description: The new Content-Description to be set for the Part. func (p *Part) SetDescription(description string) { p.description = description } -// SetWriteFunc overrides the WriteFunc of the Part +// SetWriteFunc overrides the WriteFunc of the Part. +// +// This function sets a new WriteFunc for the Part, replacing the existing one. The WriteFunc +// is responsible for writing the Part's content to an io.Writer. +// +// Parameters: +// - writeFunc: A function that writes the Part's content to an io.Writer and returns +// the number of bytes written and an error (if any). func (p *Part) SetWriteFunc(writeFunc func(io.Writer) (int64, error)) { p.writeFunc = writeFunc } -// Delete removes the current part from the parts list of the Msg by setting the -// isDeleted flag to true. The msgWriter will skip it then +// Delete removes the current part from the parts list of the Msg by setting the isDeleted flag to true. +// +// This function marks the Part as deleted by setting the isDeleted flag to true. The msgWriter +// will skip over this Part during processing. func (p *Part) Delete() { p.isDeleted = true } -// WithPartCharset overrides the default Part charset +// WithPartCharset overrides the default Part charset. +// +// This function returns a PartOption that allows the charset of a Part to be overridden +// with the specified Charset. +// +// Parameters: +// - charset: The Charset to be set for the Part. +// +// Returns: +// - A PartOption function that sets the Part's charset. func WithPartCharset(charset Charset) PartOption { return func(p *Part) { p.charset = charset } } -// WithPartEncoding overrides the default Part encoding +// WithPartEncoding overrides the default Part encoding. +// +// This function returns a PartOption that allows the encoding of a Part to be overridden +// with the specified Encoding. +// +// Parameters: +// - encoding: The Encoding to be set for the Part. +// +// Returns: +// - A PartOption function that sets the Part's encoding. func WithPartEncoding(encoding Encoding) PartOption { return func(p *Part) { p.encoding = encoding } } -// WithPartContentDescription overrides the default Part Content-Description +// WithPartContentDescription overrides the default Part Content-Description. +// +// This function returns a PartOption that allows the Content-Description of a Part +// to be overridden with the specified description. +// +// Parameters: +// - description: The Content-Description to be set for the Part. +// +// Returns: +// - A PartOption function that sets the Part's Content-Description. func WithPartContentDescription(description string) PartOption { return func(p *Part) { p.description = description From cd90c3ddf3f06320e34c75d39244588282f4f94a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 6 Oct 2024 17:00:49 +0200 Subject: [PATCH 172/181] Update randNum function and documentation for multiple versions Expanded randNum function comments to provide detailed behavior and usage across different Go versions. Enhanced the randomStringSecure function doc to explain its cryptographic secure implementation and error handling. --- random.go | 29 ++++++++++++++++++++++++----- random_119.go | 12 +++++++++++- random_121.go | 12 +++++++++++- random_122.go | 12 ++++++++++-- 4 files changed, 56 insertions(+), 9 deletions(-) diff --git a/random.go b/random.go index 3a3f16b..2478c75 100644 --- a/random.go +++ b/random.go @@ -14,14 +14,33 @@ import ( const cr = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" // Bitmask sizes for the string generators (based on 93 chars total) +// +// These constants define bitmask-related values used for efficient random string generation. +// The bitmask operates over 93 possible characters, and the constants help determine the +// number of bits and indices used in the process. const ( - letterIdxBits = 7 // 7 bits to represent a letter index - letterIdxMask = 1< Date: Sun, 6 Oct 2024 17:05:04 +0200 Subject: [PATCH 173/181] Enhance and clarify Reader struct documentation Improved documentation for the Reader struct, its methods, and error handling. Added detailed explanations for `Read`, `Error`, `Reset`, and `empty` functions to better describe their parameters, return values, and behavior. --- reader.go | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/reader.go b/reader.go index 3c44f4c..e635e3a 100644 --- a/reader.go +++ b/reader.go @@ -8,19 +8,41 @@ import ( "io" ) -// Reader is a type that implements the io.Reader interface for a Msg +// Reader is a type that implements the io.Reader interface for a Msg. +// +// This struct represents a reader that reads from a byte slice buffer. It keeps track of the +// current read position (offset) and any initialization error. The buffer holds the data to be +// read from the message. type Reader struct { buffer []byte // contents are the bytes buffer[offset : len(buffer)] offset int // read at &buffer[offset], write at &buffer[len(buffer)] err error // initialization error } -// Error returns an error if the Reader err field is not nil +// Error returns an error if the Reader err field is not nil. +// +// This function checks the Reader's err field and returns it if it is not nil. If no error +// occurred during initialization, it returns nil. +// +// Returns: +// - The error stored in the err field, or nil if no error is present. func (r *Reader) Error() error { return r.err } -// Read reads the length of p of the Msg buffer to satisfy the io.Reader interface +// Read reads the content of the Msg buffer into the provided payload to satisfy the io.Reader interface. +// +// This function reads data from the Reader's buffer into the provided byte slice (payload). +// It checks for errors or an empty buffer and resets the Reader if necessary. If no data is available, +// it returns io.EOF. Otherwise, it copies the content from the buffer into the payload and updates +// the read offset. +// +// Parameters: +// - payload: A byte slice where the data will be copied. +// +// Returns: +// - n: The number of bytes copied into the payload. +// - err: An error if any issues occurred during the read operation or io.EOF if the buffer is empty. func (r *Reader) Read(payload []byte) (n int, err error) { if r.err != nil { return 0, r.err @@ -37,12 +59,20 @@ func (r *Reader) Read(payload []byte) (n int, err error) { return n, err } -// Reset resets the Reader buffer to be empty, but it retains the underlying storage -// for use by future writes. +// Reset resets the Reader buffer to be empty, but it retains the underlying storage for future use. +// +// This function clears the Reader's buffer by setting its length to 0 and resets the read offset +// to the beginning. The underlying storage is retained, allowing future writes to reuse the buffer. func (r *Reader) Reset() { r.buffer = r.buffer[:0] r.offset = 0 } // empty reports whether the unread portion of the Reader buffer is empty. +// +// This function checks if the unread portion of the Reader's buffer is empty by comparing +// the length of the buffer to the current read offset. +// +// Returns: +// - true if the unread portion is empty, false otherwise. func (r *Reader) empty() bool { return len(r.buffer) <= r.offset } From 6ce5c2a8606d495cf4d5c559a40d06977814df8e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 6 Oct 2024 17:12:52 +0200 Subject: [PATCH 174/181] Enhance documentation for SendError methods and fields Improved comments for better clarity and detail in SendError-related methods and fields. Updated comments provide more in-depth explanations of functionality, parameters, return values, and usage. --- senderror.go | 75 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 65 insertions(+), 10 deletions(-) diff --git a/senderror.go b/senderror.go index 90c8c4a..1943e28 100644 --- a/senderror.go +++ b/senderror.go @@ -54,7 +54,11 @@ const ( ErrAmbiguous ) -// SendError is an error wrapper for delivery errors of the Msg +// SendError is an error wrapper for delivery errors of the Msg. +// +// This struct represents an error that occurs during the delivery of a message. It holds +// details about the affected message, a list of errors, the recipient list, and whether +// the error is temporary or permanent. It also includes a reason code for the error. type SendError struct { affectedMsg *Msg errlist []error @@ -66,7 +70,16 @@ type SendError struct { // SendErrReason represents a comparable reason on why the delivery failed type SendErrReason int -// Error implements the error interface for the SendError type +// Error implements the error interface for the SendError type. +// +// This function returns a detailed error message string for the SendError, including the +// reason for failure, list of errors, affected recipients, and the message ID of the +// affected message (if available). If the reason is unknown (greater than 10), it returns +// "unknown reason". The error message is built dynamically based on the content of the +// error list, recipient list, and message ID. +// +// Returns: +// - A string representing the error message. func (e *SendError) Error() string { if e.Reason > 10 { return "unknown reason" @@ -101,7 +114,17 @@ func (e *SendError) Error() string { return errMessage.String() } -// Is implements the errors.Is functionality and compares the SendErrReason +// Is implements the errors.Is functionality and compares the SendErrReason. +// +// This function allows for comparison between two errors by checking if the provided +// error matches the SendError type and, if so, compares the SendErrReason and the +// temporary status (isTemp) of both errors. +// +// Parameters: +// - errType: The error to compare against the current SendError. +// +// Returns: +// - true if the errors have the same reason and temporary status, false otherwise. func (e *SendError) Is(errType error) bool { var t *SendError if errors.As(errType, &t) && t != nil { @@ -110,7 +133,13 @@ func (e *SendError) Is(errType error) bool { return false } -// IsTemp returns true if the delivery error is of temporary nature and can be retried +// IsTemp returns true if the delivery error is of a temporary nature and can be retried. +// +// This function checks whether the SendError indicates a temporary error, which suggests +// that the delivery can be retried. If the SendError is nil, it returns false. +// +// Returns: +// - true if the error is temporary, false otherwise. func (e *SendError) IsTemp() bool { if e == nil { return false @@ -118,8 +147,13 @@ func (e *SendError) IsTemp() bool { return e.isTemp } -// MessageID returns the message ID of the affected Msg that caused the error -// If no message ID was set for the Msg, an empty string will be returned +// MessageID returns the message ID of the affected Msg that caused the error. +// +// This function retrieves the message ID of the Msg associated with the SendError. +// If no message ID was set or if the SendError or Msg is nil, it returns an empty string. +// +// Returns: +// - The message ID as a string, or an empty string if no ID is available. func (e *SendError) MessageID() string { if e == nil || e.affectedMsg == nil { return "" @@ -127,7 +161,13 @@ func (e *SendError) MessageID() string { return e.affectedMsg.GetMessageID() } -// Msg returns the pointer to the affected message that caused the error +// Msg returns the pointer to the affected message that caused the error. +// +// This function retrieves the Msg associated with the SendError. If the SendError or +// the affectedMsg is nil, it returns nil. +// +// Returns: +// - A pointer to the Msg that caused the error, or nil if not available. func (e *SendError) Msg() *Msg { if e == nil || e.affectedMsg == nil { return nil @@ -135,7 +175,14 @@ func (e *SendError) Msg() *Msg { return e.affectedMsg } -// String implements the Stringer interface for the SendErrReason +// String satisfies the fmt.Stringer interface for the SendErrReason type. +// +// This function converts the SendErrReason into a human-readable string representation based +// on the error type. If the error reason does not match any predefined case, it returns +// "unknown reason". +// +// Returns: +// - A string representation of the SendErrReason. func (r SendErrReason) String() string { switch r { case ErrGetSender: @@ -164,8 +211,16 @@ func (r SendErrReason) String() string { return "unknown reason" } -// isTempError checks the given SMTP error and returns true if the given error is of temporary nature -// and should be retried +// isTempError checks if the given SMTP error is of a temporary nature and should be retried. +// +// This function inspects the error message and returns true if the first character of the +// error message is '4', indicating a temporary SMTP error that can be retried. +// +// Parameters: +// - err: The error to check. +// +// Returns: +// - true if the error is temporary, false otherwise. func isTempError(err error) bool { return err.Error()[0] == '4' } From f0388ec600f956f78c1505d5c4928e0cffa3c346 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 6 Oct 2024 17:15:39 +0200 Subject: [PATCH 175/181] Refactor TLSPolicy documentation and String method Updated the TLSPolicy type documentation to improve clarity and consistency. Enhanced the String method with detailed commentary and return value explanation, ensuring it better adheres to the fmt.Stringer interface. --- tls.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tls.go b/tls.go index bb7ad14..3adc0cc 100644 --- a/tls.go +++ b/tls.go @@ -4,25 +4,32 @@ package mail -// TLSPolicy type describes a int alias for the different TLS policies we allow +// TLSPolicy is a type wrapper for an int type and describes the different TLS policies we allow. type TLSPolicy int const ( // TLSMandatory requires that the connection to the server is // encrypting using STARTTLS. If the server does not support STARTTLS - // the connection will be terminated with an error + // the connection will be terminated with an error. TLSMandatory TLSPolicy = iota // TLSOpportunistic tries to establish an encrypted connection via the // STARTTLS protocol. If the server does not support this, it will fall - // back to non-encrypted plaintext transmission + // back to non-encrypted plaintext transmission. TLSOpportunistic - // NoTLS forces the transaction to be not encrypted + // NoTLS forces the transaction to be not encrypted. NoTLS ) -// String is a standard method to convert a TLSPolicy into a printable format +// String satisfies the fmt.Stringer interface for the TLSPolicy type. +// +// This function returns a string representation of the TLSPolicy. It matches the policy +// value to predefined constants and returns the corresponding string. If the policy does +// not match any known values, it returns "UnknownPolicy". +// +// Returns: +// - A string representing the TLSPolicy. func (p TLSPolicy) String() string { switch p { case TLSMandatory: From a94e721161a5dea330814bea1f97f74228738571 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 6 Oct 2024 17:29:26 +0200 Subject: [PATCH 176/181] Update doc.go Bump version for v0.5.0 release --- doc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc.go b/doc.go index de16f5d..c775015 100644 --- a/doc.go +++ b/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.4.4" +const VERSION = "0.5.0" From cdb9463ec8bed94ffcbcc49826014ed62d4f4a77 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 7 Oct 2024 15:02:49 +0200 Subject: [PATCH 177/181] Delete redundant random number generation methods Removed `randNum` functions and associated tests from `random_119.go`, `random_121.go`, and `random_122.go`. These functions are no longer necessary. Additionally, replaced `TestRandomNum` and `TestRandomNumZero` with benchmarking for `randomStringSecure`. --- random_119.go | 32 -------------------------------- random_121.go | 30 ------------------------------ random_122.go | 30 ------------------------------ random_test.go | 36 +++++++----------------------------- 4 files changed, 7 insertions(+), 121 deletions(-) delete mode 100644 random_119.go delete mode 100644 random_121.go delete mode 100644 random_122.go diff --git a/random_119.go b/random_119.go deleted file mode 100644 index 98d761c..0000000 --- a/random_119.go +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors -// -// SPDX-License-Identifier: MIT - -//go:build !go1.20 -// +build !go1.20 - -package mail - -import ( - "math/rand" - "time" -) - -// randNum returns a random number with a maximum value of maxval. -// -// This function generates a random integer between 0 and maxval (exclusive). It seeds the -// random number generator with the current time in nanoseconds to ensure different results -// each time the function is called. -// -// Parameters: -// - maxval: The upper bound for the random number generation (exclusive). -// -// Returns: -// - A random integer between 0 and maxval. If maxval is less than or equal to 0, it returns 0. -func randNum(maxval int) int { - if maxval <= 0 { - return 0 - } - rand.Seed(time.Now().UnixNano()) - return rand.Intn(maxval) -} diff --git a/random_121.go b/random_121.go deleted file mode 100644 index ea1e399..0000000 --- a/random_121.go +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors -// -// SPDX-License-Identifier: MIT - -//go:build go1.20 && !go1.22 -// +build go1.20,!go1.22 - -package mail - -import ( - "math/rand" -) - -// randNum returns a random number with a maximum value of maxval. -// -// This function generates a random integer between 0 and maxval (exclusive). If maxval is less -// than or equal to 0, it returns 0. The random number generator uses the default seed provided -// by the rand package. -// -// Parameters: -// - maxval: The upper bound for the random number generation (exclusive). -// -// Returns: -// - A random integer between 0 and maxval. If maxval is less than or equal to 0, it returns 0. -func randNum(maxval int) int { - if maxval <= 0 { - return 0 - } - return rand.Intn(maxval) -} diff --git a/random_122.go b/random_122.go deleted file mode 100644 index 27ded3d..0000000 --- a/random_122.go +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors -// -// SPDX-License-Identifier: MIT - -//go:build go1.22 -// +build go1.22 - -package mail - -import ( - "math/rand/v2" -) - -// randNum returns a random number with a maximum value of maxval. -// -// This function generates a random integer between 0 and maxval (exclusive). It utilizes -// the math/rand/v2 interface for Go 1.22+ and will default to math/rand for older Go versions. -// If maxval is less than or equal to 0, it returns 0. -// -// Parameters: -// - maxval: The upper bound for the random number generation (exclusive). -// -// Returns: -// - A random integer between 0 and maxval. If maxval is less than or equal to 0, it returns 0. -func randNum(maxval int) int { - if maxval <= 0 { - return 0 - } - return rand.IntN(maxval) -} diff --git a/random_test.go b/random_test.go index e69b4e7..a608c2a 100644 --- a/random_test.go +++ b/random_test.go @@ -38,34 +38,12 @@ func TestRandomStringSecure(t *testing.T) { } } -// TestRandomNum tests the randomNum method -func TestRandomNum(t *testing.T) { - tt := []struct { - testName string - max int - }{ - {"Max: 1", 1}, - {"Max: 20", 20}, - {"Max: 50", 50}, - {"Max: 100", 100}, - {"Max: 1000", 1000}, - {"Max: 10000", 10000}, - {"Max: 100000000", 100000000}, - } - - for _, tc := range tt { - t.Run(tc.testName, func(t *testing.T) { - rn := randNum(tc.max) - if rn > tc.max { - t.Errorf("random number generation failed: %d is bigger than given value %d", rn, tc.max) - } - }) - } -} - -func TestRandomNumZero(t *testing.T) { - rn := randNum(0) - if rn != 0 { - t.Errorf("random number generation failed: %d is not zero", rn) +func BenchmarkGenerator_RandomStringSecure(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _, err := randomStringSecure(22) + if err != nil { + b.Errorf("RandomStringFromCharRange() failed: %s", err) + } } } From 5c8b2fc371113a859409fd1e0ecf51e1c213473e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 7 Oct 2024 15:03:07 +0200 Subject: [PATCH 178/181] Update character set and bit size for secure string generation Revised the character set to include a larger variety of symbols and adjusted the bit size calculations to correspond with the new set size. This ensures more efficient and secure random string generation by effectively utilizing the bitmask. --- random.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/random.go b/random.go index 2478c75..e987f00 100644 --- a/random.go +++ b/random.go @@ -11,16 +11,17 @@ import ( ) // Range of characters for the secure string generation -const cr = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" +const cr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-" // Bitmask sizes for the string generators (based on 93 chars total) // // These constants define bitmask-related values used for efficient random string generation. -// The bitmask operates over 93 possible characters, and the constants help determine the +// The bitmask operates over 66 possible characters, and the constants help determine the // number of bits and indices used in the process. const ( - // letterIdxBits: Number of bits (7) needed to represent a letter index. - letterIdxBits = 7 + // letterIdxBits: Number of bits needed to represent a letter index. We have 64 possible characters + // which fit into 6 bits. + letterIdxBits = 6 // letterIdxMask: Bitmask to extract letter indices (all 1-bits for letterIdxBits). letterIdxMask = 1< Date: Mon, 7 Oct 2024 15:04:04 +0200 Subject: [PATCH 179/181] Simplify Message-ID generation Updated the SetMessageID method to generate a "Message-ID" using a single randomly generated string combined with the hostname, replacing the prior complex format that included process ID and multiple random numbers. This change simplifies the code of the generated IDs. --- msg.go | 18 +++++++----------- msg_test.go | 2 +- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/msg.go b/msg.go index fc0bb57..61feda1 100644 --- a/msg.go +++ b/msg.go @@ -972,12 +972,12 @@ func (m *Msg) Subject(subj string) { // SetMessageID generates and sets a unique "Message-ID" header for the Msg. // -// This method creates a "Message-ID" string using the current process ID, random numbers, and the hostname -// of the machine. The generated ID helps uniquely identify the message in email systems, facilitating tracking -// and preventing duplication. If the hostname cannot be retrieved, it defaults to "localhost.localdomain". +// This method creates a "Message-ID" string using a randomly generated string and the hostname of the machine. +// The generated ID helps uniquely identify the message in email systems, facilitating tracking and preventing +// duplication. If the hostname cannot be retrieved, it defaults to "localhost.localdomain". // // The generated Message-ID follows the format -// "". +// "". // // References: // - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.4 @@ -986,13 +986,9 @@ func (m *Msg) SetMessageID() { if err != nil { hostname = "localhost.localdomain" } - randNumPrimary := randNum(100000000) - randNumSecondary := randNum(10000) - randString, _ := randomStringSecure(17) - procID := os.Getpid() * randNumSecondary - messageID := fmt.Sprintf("%d.%d%d.%s@%s", procID, randNumPrimary, randNumSecondary, - randString, hostname) - m.SetMessageIDWithValue(messageID) + // We have 64 possible characters, which for a 22 character string, provides approx. 132 bits of entropy. + randString, _ := randomStringSecure(22) + m.SetMessageIDWithValue(fmt.Sprintf("%s@%s", randString, hostname)) } // GetMessageID retrieves the "Message-ID" header from the Msg. diff --git a/msg_test.go b/msg_test.go index f570b02..7fcbd99 100644 --- a/msg_test.go +++ b/msg_test.go @@ -786,8 +786,8 @@ func TestMsg_SetMessageIDWithValue(t *testing.T) { // TestMsg_SetMessageIDRandomness tests the randomness of Msg.SetMessageID methods func TestMsg_SetMessageIDRandomness(t *testing.T) { var mids []string + m := NewMsg() for i := 0; i < 50_000; i++ { - m := NewMsg() m.SetMessageID() mid := m.GetMessageID() mids = append(mids, mid) From 4d0e3e221579cdb39ad098fabf1e645df6e47a9a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 13:54:42 +0000 Subject: [PATCH 180/181] Bump github/codeql-action from 3.26.11 to 3.26.12 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.11 to 3.26.12. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea...c36620d31ac7c881962c3d9dd939c40ec9434f2b) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d5668f3..0a65963 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -54,7 +54,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 + uses: github/codeql-action/init@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 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@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 + uses: github/codeql-action/autobuild@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 # ℹī¸ 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@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 + uses: github/codeql-action/analyze@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index b2e161f..e0550b8 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -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@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 + uses: github/codeql-action/upload-sarif@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 with: sarif_file: results.sarif From 44d6a3333fac9162d8d6865fbe1a80d212e6428b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 13:54:45 +0000 Subject: [PATCH 181/181] Bump actions/upload-artifact from 4.4.0 to 4.4.1 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.4.0 to 4.4.1. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/50769540e7f4bd5e21e526ee35c689e35e0d6874...604373da6381bf24206979c74d06a550515601b9) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/scorecards.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index b2e161f..fa45d31 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -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@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4.4.1 with: name: SARIF file path: results.sarif