Compare commits

...

50 commits

Author SHA1 Message Date
Michael Fuchs
1e3f4fe757
Merge 3154649420 into 077c85bea0 2024-09-26 09:38:07 +00:00
077c85bea0
Merge pull request #305 from wneessen/dependabot/github_actions/sonarsource/sonarqube-scan-action-884b79409bbd464b2a59edc326a4b77dc56b2195
Bump sonarsource/sonarqube-scan-action from f885e52a7572cf7943f28637e75730227df2dbf2 to 884b79409bbd464b2a59edc326a4b77dc56b2195
2024-09-25 16:00:56 +02:00
909e699b99
Merge pull request #306 from wneessen/dependabot/github_actions/github/codeql-action-3.26.9
Bump github/codeql-action from 3.26.8 to 3.26.9
2024-09-25 16:00:44 +02:00
dependabot[bot]
b97073db19
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](294a9d9291...461ef6c76d)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-25 13:58:44 +00:00
dependabot[bot]
d75d990124
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](f885e52a75...884b79409b)

---
updated-dependencies:
- dependency-name: sonarsource/sonarqube-scan-action
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-25 13:58:36 +00:00
0676d99f20
Merge pull request #304 from wneessen/dependabot/github_actions/sonarsource/sonarqube-scan-action-f885e52a7572cf7943f28637e75730227df2dbf2
Bump sonarsource/sonarqube-scan-action from 0c0f3958d90fc466625f1d1af1f47bddd4cc6bd1 to f885e52a7572cf7943f28637e75730227df2dbf2
2024-09-24 15:50:50 +02:00
dependabot[bot]
d6725b2d63
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](0c0f3958d9...f885e52a75)

---
updated-dependencies:
- dependency-name: sonarsource/sonarqube-scan-action
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-24 13:48:36 +00:00
580d0b0e4e
Merge pull request #303 from wneessen/more-test-coverage
More test coverage
2024-09-20 22:21:49 +02:00
3f0ac027e2
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.
2024-09-20 22:06:11 +02:00
0b9a215e7d
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.
2024-09-20 21:42:10 +02:00
4053457020
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.
2024-09-20 21:15:13 +02:00
f3633e1913
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.
2024-09-20 21:11:19 +02:00
52061f97c6
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.
2024-09-20 20:58:29 +02:00
77920be1a1
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.
2024-09-20 20:39:50 +02:00
f5d4cdafea
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.
2024-09-20 20:39:30 +02:00
19330fc108
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.
2024-09-20 20:30:23 +02:00
af9915e4e7
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.
2024-09-20 19:45:49 +02:00
6f869e4efd
Merge pull request #302 from wneessen/feature/improve-client-tests
Improved client error testing
2024-09-20 17:51:19 +02:00
44df830348
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.
2024-09-20 17:00:08 +02:00
4ee11e8406
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.
2024-09-20 16:44:15 +02:00
bd5a8a40b9
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
2024-09-20 16:40:12 +02:00
8dfb121aec
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.
2024-09-20 16:24:59 +02:00
d5437f6b7a
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.
2024-09-20 16:23:31 +02:00
157c138142
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.
2024-09-20 15:58:51 +02:00
482194b4b3
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.
2024-09-20 15:52:05 +02:00
b8f0462ce3
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.
2024-09-20 15:49:03 +02:00
6af6a28f78
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.
2024-09-20 15:05:43 +02:00
0aa24c6f3a
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.
2024-09-20 15:05:26 +02:00
fbebcf96d8
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.
2024-09-20 14:48:24 +02:00
619318ba0d
Merge pull request #301 from wneessen/feature/168_improve-error-handling
Improved error handling
2024-09-20 11:02:49 +02:00
d400379e2f
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.
2024-09-20 10:31:19 +02:00
fcbd202595
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.
2024-09-20 10:30:30 +02:00
db2ec99cd6
Merge pull request #300 from wneessen/dependabot/github_actions/github/codeql-action-3.26.8
Bump github/codeql-action from 3.26.7 to 3.26.8
2024-09-19 15:45:49 +02:00
dependabot[bot]
664b7299e6
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](8214744c54...294a9d9291)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-19 13:44:59 +00:00
508a2f2a6c
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.
2024-09-19 15:21:17 +02:00
f469ba977d
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.
2024-09-19 12:12:48 +02:00
0239318d94
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.
2024-09-19 12:09:23 +02:00
69e211682c
Add GetMessageID method to Msg
Introduced GetMessageID to retrieve the Message ID from the Msg
2024-09-19 11:46:53 +02:00
3bdb6f7cca
Refactor variable naming in Send method
Renamed variable `cerr` to `err` for consistency. This improves readability and standardizes error variable naming within the method.
2024-09-19 10:59:22 +02:00
277ae9be19
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.
2024-09-19 10:56:01 +02:00
2e7156182a
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.
2024-09-19 10:35:32 +02:00
8ee37abca2
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.
2024-09-18 12:29:42 +02:00
ee726487f1
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.
2024-09-18 11:06:48 +02:00
1dd73778d4
Merge branch 'main' into feature/168_improve-error-handling 2024-09-18 10:33:06 +02:00
d3bea90761
Merge pull request #299 from wneessen/dependabot/github_actions/github/codeql-action-3.26.7
Bump github/codeql-action from 3.26.6 to 3.26.7
2024-09-16 15:51:08 +02:00
dependabot[bot]
68109ed40d
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](4dd16135b6...8214744c54)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-16 13:46:00 +00:00
94d5f3f7a1
Merge pull request #297 from wneessen/dependabot/github_actions/step-security/harden-runner-2.10.1
Bump step-security/harden-runner from 2.9.1 to 2.10.1
2024-09-11 16:14:56 +02:00
dependabot[bot]
6d9829776a
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](5c7944e73c...91182cccc0)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-11 14:09:18 +00:00
a747f5f74c
Merge branch 'main' into feature/168_improve-error-handling 2024-09-06 11:15:50 +02:00
0ea9631855
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.
2024-07-16 13:07:20 +02:00
22 changed files with 1057 additions and 244 deletions

View file

@ -36,10 +36,10 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
go: ['1.20', '1.21', '1.22', '1.23'] go: ['1.19', '1.20', '1.23']
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with: with:
egress-policy: audit egress-policy: audit

View file

@ -45,7 +45,7 @@ jobs:
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with: with:
egress-policy: audit egress-policy: audit
@ -54,7 +54,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 uses: github/codeql-action/init@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@ -65,7 +65,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 uses: github/codeql-action/autobuild@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@ -79,4 +79,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 uses: github/codeql-action/analyze@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9

View file

@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with: with:
egress-policy: audit egress-policy: audit

View file

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with: with:
egress-policy: audit egress-policy: audit

View file

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with: with:
egress-policy: audit egress-policy: audit
- name: Run govulncheck - name: Run govulncheck

View file

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with: with:
egress-policy: audit egress-policy: audit

View file

@ -35,7 +35,7 @@ jobs:
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with: with:
egress-policy: audit egress-policy: audit
@ -75,6 +75,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard. # Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning" - name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9
with: with:
sarif_file: results.sarif sarif_file: results.sarif

View file

@ -27,7 +27,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with: with:
egress-policy: audit egress-policy: audit
@ -44,7 +44,7 @@ jobs:
run: | run: |
go test -v -race --coverprofile=./cov.out ./... go test -v -race --coverprofile=./cov.out ./...
- uses: sonarsource/sonarqube-scan-action@0c0f3958d90fc466625f1d1af1f47bddd4cc6bd1 # master - uses: sonarsource/sonarqube-scan-action@884b79409bbd464b2a59edc326a4b77dc56b2195 # master
env: env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

View file

@ -787,3 +787,100 @@ func (c *Client) auth() error {
} }
return nil 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, affectedMsg: message}
}
}
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),
affectedMsg: message,
}
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
retError.errlist = append(retError.errlist, resetSendErr)
}
return retError
}
hasError := false
rcptSendErr := &SendError{affectedMsg: message}
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),
affectedMsg: message,
}
}
_, err = message.WriteTo(writer)
if err != nil {
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,
}
}
if err = c.Reset(); err != nil {
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 nil
}

View file

@ -7,111 +7,23 @@
package mail package mail
import "strings" import "errors"
// Send sends out the mail message // Send sends out the mail message
func (c *Client) Send(messages ...*Msg) error { func (c *Client) Send(messages ...*Msg) error {
if cerr := c.checkConn(); cerr != nil { if err := c.checkConn(); err != nil {
return &SendError{Reason: ErrConnCheck, errlist: []error{cerr}, isTemp: isTempError(cerr)} return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
} }
var errs []*SendError var errs []*SendError
for _, message := range messages { for id, message := range messages {
message.sendError = nil if sendErr := c.sendSingleMsg(message); sendErr != nil {
if message.encoding == NoEncoding { messages[id].sendError = sendErr
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
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 { var msgSendErr *SendError
if c.dsnmrtype != "" { if errors.As(sendErr, &msgSendErr) {
c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype)) errs = append(errs, msgSendErr)
} }
} }
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
}
} }
if len(errs) > 0 { if len(errs) > 0 {

View file

@ -9,7 +9,6 @@ package mail
import ( import (
"errors" "errors"
"strings"
) )
// Send sends out the mail message // Send sends out the mail message
@ -18,92 +17,16 @@ func (c *Client) Send(messages ...*Msg) (returnErr error) {
returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
return return
} }
for _, 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)
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)
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)
continue
}
if c.dsn { var errs []error
if c.dsnmrtype != "" { defer func() {
c.smtpClient.SetDSNMailReturnOption(string(c.dsnmrtype)) returnErr = errors.Join(errs...)
} }()
}
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)
if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil {
returnErr = errors.Join(returnErr, 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 {
returnErr = errors.Join(returnErr, resetSendErr)
}
message.sendError = rcptSendErr
returnErr = errors.Join(returnErr, 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)
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)
continue
}
message.isDelivered = true
if err = writer.Close(); err != nil { for id, message := range messages {
message.sendError = &SendError{Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err)} if sendErr := c.sendSingleMsg(message); sendErr != nil {
returnErr = errors.Join(returnErr, message.sendError) messages[id].sendError = sendErr
continue errs = append(errs, sendErr)
}
if err = c.Reset(); err != nil {
message.sendError = &SendError{Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err)}
returnErr = errors.Join(returnErr, 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)
} }
} }

View file

@ -5,6 +5,7 @@
package mail package mail
import ( import (
"bufio"
"context" "context"
"crypto/tls" "crypto/tls"
"errors" "errors"
@ -21,11 +22,18 @@ import (
"github.com/wneessen/go-mail/smtp" "github.com/wneessen/go-mail/smtp"
) )
// DefaultHost is used as default hostname for the Client const (
const DefaultHost = "localhost" // DefaultHost is used as default hostname for the Client
DefaultHost = "localhost"
// TestRcpt // TestRcpt is a trash mail address to send test mails to
const TestRcpt = "go-mail@mytrashmailer.com" 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"
// TestServerPortBase is the base port for the simple SMTP test server
TestServerPortBase = 2025
)
// TestNewClient tests the NewClient() method with its default options // TestNewClient tests the NewClient() method with its default options
func TestNewClient(t *testing.T) { func TestNewClient(t *testing.T) {
@ -629,7 +637,7 @@ func TestClient_DialWithContext(t *testing.T) {
// TestClient_DialWithContext_Fallback tests the Client.DialWithContext method with the fallback // TestClient_DialWithContext_Fallback tests the Client.DialWithContext method with the fallback
// port functionality // port functionality
func TestClient_DialWithContext_Fallback(t *testing.T) { func TestClient_DialWithContext_Fallback(t *testing.T) {
c, err := getTestConnection(true) c, err := getTestConnectionNoTestPort(true)
if err != nil { if err != nil {
t.Skipf("failed to create test client: %s. Skipping tests", err) t.Skipf("failed to create test client: %s. Skipping tests", err)
} }
@ -1251,6 +1259,475 @@ 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"
serverPort := TestServerPortBase + 1
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 * 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")
message.SetEncoding(NoEncoding)
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)
}
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(), "<this.is.a.message.id>") {
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
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_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, serverPort); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 300)
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(serverPort),
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(), "<this.is.a.message.id>") {
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
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()
serverPort := TestServerPortBase + 3
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)
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(serverPort),
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(), "<this.is.a.message.id>") {
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
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: <invalid-from@domain.tld>") {
t.Errorf("expected error: %q, but got %q",
"503 5.1.2 Invalid from: <invalid-from@domain.tld>", 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()
serverPort := TestServerPortBase + 4
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)
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(serverPort),
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(), "<this.is.a.message.id>") {
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
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: <invalid-to@domain.tld>") {
t.Errorf("expected error: %q, but got %q",
"500 5.1.2 Invalid to: <invalid-to@domain.tld>", 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()
serverPort := TestServerPortBase + 5
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 * 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(serverPort),
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(), "<this.is.a.message.id>") {
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
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()
serverPort := TestServerPortBase + 6
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 * 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(serverPort),
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(), "<this.is.a.message.id>") {
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
sendErr.MessageID())
}
}
}
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, serverPort); 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(serverPort),
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(), "<this.is.a.message.id>") {
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
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 // getTestConnection takes environment variables to establish a connection to a real
// SMTP server to test all functionality that requires a connection // SMTP server to test all functionality that requires a connection
func getTestConnection(auth bool) (*Client, error) { func getTestConnection(auth bool) (*Client, error) {
@ -1302,6 +1779,50 @@ func getTestConnection(auth bool) (*Client, error) {
return c, nil 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 // getTestClient takes environment variables to establish a client without connecting
// to the SMTP server // to the SMTP server
func getTestClient(auth bool) (*Client, error) { func getTestClient(auth bool) (*Client, error) {
@ -1357,7 +1878,14 @@ func getTestConnectionWithDSN(auth bool) (*Client, error) {
if th == "" { if th == "" {
return nil, fmt.Errorf("no TEST_HOST set") 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 { if err != nil {
return c, err return c, err
} }
@ -1494,3 +2022,155 @@ func (f faker) RemoteAddr() net.Addr { return nil }
func (f faker) SetDeadline(time.Time) error { return nil } func (f faker) SetDeadline(time.Time) error { return nil }
func (f faker) SetReadDeadline(time.Time) error { return nil } func (f faker) SetReadDeadline(time.Time) error { return nil }
func (f faker) SetWriteDeadline(time.Time) error { return nil } 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, 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)
}
defer func() {
if err := listener.Close(); err != nil {
fmt.Printf("unable to close listener: %s\n", err)
os.Exit(1)
}
}()
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, failReset)
}
}
}
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)
}
}()
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)
return
}
if !strings.HasPrefix(data, "EHLO") && !strings.HasPrefix(data, "HELO") {
fmt.Printf("expected EHLO, got %q", data)
return
}
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 errors.Is(err, io.EOF) {
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, "<valid-from@domain.tld>") {
_ = 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:")
to = strings.TrimSpace(to)
if !strings.EqualFold(to, "<valid-to@domain.tld>") {
_ = 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 <CR><LF>.<CR><LF>")
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 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, "vrfy"):
writeOK()
case strings.EqualFold(data, "rset"):
if failReset {
_ = writeLine("500 5.1.2 Error: reset failed")
break
}
writeOK()
case strings.EqualFold(data, "quit"):
_ = writeLine("221 2.0.0 Bye")
default:
_ = writeLine("500 5.5.2 Error: bad syntax")
}
}
}

15
msg.go
View file

@ -482,8 +482,8 @@ func (m *Msg) SetMessageID() {
if err != nil { if err != nil {
hostname = "localhost.localdomain" hostname = "localhost.localdomain"
} }
randNumPrimary, _ := randNum(100000000) randNumPrimary := randNum(100000000)
randNumSecondary, _ := randNum(10000) randNumSecondary := randNum(10000)
randString, _ := randomStringSecure(17) randString, _ := randomStringSecure(17)
procID := os.Getpid() * randNumSecondary procID := os.Getpid() * randNumSecondary
messageID := fmt.Sprintf("%d.%d%d.%s@%s", procID, randNumPrimary, randNumSecondary, messageID := fmt.Sprintf("%d.%d%d.%s@%s", procID, randNumPrimary, randNumSecondary,
@ -491,6 +491,17 @@ func (m *Msg) SetMessageID() {
m.SetMessageIDWithValue(messageID) 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 // SetMessageIDWithValue sets the message id for the mail
func (m *Msg) SetMessageIDWithValue(messageID string) { func (m *Msg) SetMessageIDWithValue(messageID string) {
m.SetGenHeader(HeaderMessageID, fmt.Sprintf("<%s>", messageID)) m.SetGenHeader(HeaderMessageID, fmt.Sprintf("<%s>", messageID))

View file

@ -61,3 +61,25 @@ func TestMsg_WriteToSendmail(t *testing.T) {
t.Errorf("WriteToSendmail failed: %s", err) t.Errorf("WriteToSendmail failed: %s", err)
} }
} }
func TestMsg_WriteToTempFileFailed(t *testing.T) {
m := NewMsg()
_ = m.From("Toni Tester <tester@example.com>")
_ = m.To("Ellenor Tester <ellinor@example.com>")
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)
}
_, err := m.WriteToTempFile()
if err == nil {
t.Errorf("WriteToTempFile() did not fail as expected")
}
}

View file

@ -786,13 +786,11 @@ func TestMsg_SetMessageIDWithValue(t *testing.T) {
// TestMsg_SetMessageIDRandomness tests the randomness of Msg.SetMessageID methods // TestMsg_SetMessageIDRandomness tests the randomness of Msg.SetMessageID methods
func TestMsg_SetMessageIDRandomness(t *testing.T) { func TestMsg_SetMessageIDRandomness(t *testing.T) {
var mids []string var mids []string
for i := 0; i < 100; i++ { for i := 0; i < 50_000; i++ {
m := NewMsg() m := NewMsg()
m.SetMessageID() m.SetMessageID()
mid := m.GetGenHeader(HeaderMessageID) mid := m.GetMessageID()
if len(mid) > 0 { mids = append(mids, mid)
mids = append(mids, mid[0])
}
} }
c := make(map[string]int) c := make(map[string]int)
for i := range mids { for i := range mids {
@ -805,6 +803,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 // TestMsg_FromFormat tests the FromFormat and EnvelopeFrom methods for the Msg object
func TestMsg_FromFormat(t *testing.T) { func TestMsg_FromFormat(t *testing.T) {
tests := []struct { tests := []struct {

View file

@ -7,8 +7,6 @@ package mail
import ( import (
"crypto/rand" "crypto/rand"
"encoding/binary" "encoding/binary"
"fmt"
"math/big"
"strings" "strings"
) )
@ -52,23 +50,3 @@ func randomStringSecure(length int) (string, error) {
return randString.String(), nil 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
}

22
random_119.go Normal file
View file

@ -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)
}

20
random_121.go Normal file
View file

@ -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)
}

22
random_122.go Normal file
View file

@ -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)
}

View file

@ -55,16 +55,17 @@ func TestRandomNum(t *testing.T) {
for _, tc := range tt { for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) { t.Run(tc.testName, func(t *testing.T) {
rn, err := randNum(tc.max) rn := 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)
}
if rn > tc.max { if rn > tc.max {
t.Errorf("random number generation failed: %d is bigger than given value %d", 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)
}
}

View file

@ -56,10 +56,11 @@ const (
// SendError is an error wrapper for delivery errors of the Msg // SendError is an error wrapper for delivery errors of the Msg
type SendError struct { type SendError struct {
Reason SendErrReason affectedMsg *Msg
isTemp bool errlist []error
errlist []error isTemp bool
rcpt []string rcpt []string
Reason SendErrReason
} }
// SendErrReason represents a comparable reason on why the delivery failed // SendErrReason represents a comparable reason on why the delivery failed
@ -92,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() return errMessage.String()
} }
@ -112,6 +118,23 @@ func (e *SendError) IsTemp() bool {
return e.isTemp 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 // String implements the Stringer interface for the SendErrReason
func (r SendErrReason) String() string { func (r SendErrReason) String() string {
switch r { switch r {

View file

@ -83,7 +83,96 @@ 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")
}
}
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(), "<this.is.a.message.id>") {
t.Errorf("sendError message-id expected: %s, but got: %s", "<this.is.a.message.id>",
se.MessageID())
}
}
}
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)
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], "<toni.tester@domain.tld>") {
t.Errorf("sendError message from expected: %s, but got: %s", "<toni.tester@domain.tld>",
from[0])
}
}
}
func TestSendError_MsgNil(t *testing.T) {
var se *SendError
if se.Msg() != nil {
t.Error("expected nil on nil-senderror")
}
}
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): <email1@domain.tld>, <email2@domain.tld>`
err := &SendError{
Reason: ErrAmbiguous, isTemp: false, affectedMsg: nil,
rcpt: []string{"<email1@domain.tld>", "<email2@domain.tld>"},
}
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 // returnSendError is a helper method to retunr a SendError with a specific reason
func returnSendError(r SendErrReason, t bool) error { 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}
} }