Compare commits

...

220 commits

Author SHA1 Message Date
Michael Fuchs
7f7bf80e39
Merge branch 'main' into main 2024-10-09 16:29:21 +02:00
theexiile1305
12076cf64a
feat: last tests 2024-10-09 16:15:23 +02:00
theexiile1305
43ba8e3af2
feat: improved tests 2024-10-09 15:45:22 +02:00
theexiile1305
5913fc1540
feat: improved tests 2024-10-09 15:40:05 +02:00
theexiile1305
6ea0974156
feat: improved tests 2024-10-09 14:43:43 +02:00
theexiile1305
2c2ee4c1fb
feat: begin implementation of tests 2024-10-09 13:53:31 +02:00
theexiile1305
4700691380
fix: detached signature is now used 2024-10-09 13:53:15 +02:00
theexiile1305
b4370ded12
fix: micalg part of content-type 2024-10-09 12:10:03 +02:00
0ee1449850
Merge pull request #330 from wneessen/dependabot/github_actions/github/codeql-action-3.26.12
Bump github/codeql-action from 3.26.11 to 3.26.12
2024-10-08 16:00:46 +02:00
72a9f68444
Merge pull request #331 from wneessen/dependabot/github_actions/actions/upload-artifact-4.4.1
Bump actions/upload-artifact from 4.4.0 to 4.4.1
2024-10-08 16:00:30 +02:00
dependabot[bot]
44d6a3333f
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](50769540e7...604373da63)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-08 13:54:45 +00:00
dependabot[bot]
4d0e3e2215
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](6db8d6351f...c36620d31a)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-08 13:54:42 +00:00
8faac3d101
Merge pull request #329 from wneessen/feature/327_simplify-message-id-generation-and-get-rid-of-randnum
Simplify message id generation and get rid of randnum
2024-10-07 15:12:46 +02:00
5874911c91
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.
2024-10-07 15:04:04 +02:00
5c8b2fc371
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.
2024-10-07 15:03:07 +02:00
cdb9463ec8
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`.
2024-10-07 15:02:49 +02:00
a94e721161
Update doc.go
Bump version for v0.5.0 release
2024-10-06 17:29:26 +02:00
46ca42e1b7
Merge pull request #324 from wneessen/better-godoc
Revision of the GoDoc documentation
2024-10-06 17:28:39 +02:00
f0388ec600
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.
2024-10-06 17:15:39 +02:00
6ce5c2a860
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.
2024-10-06 17:12:52 +02:00
5d79ff69c3
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.
2024-10-06 17:05:04 +02:00
cd90c3ddf3
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.
2024-10-06 17:00:49 +02:00
e640f2df46
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.
2024-10-06 16:54:18 +02:00
295155ba67
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.
2024-10-06 16:42:50 +02:00
0b105048e6
Refine WriteToTempFile docstring
Clarify the documentation for WriteToTempFile to better explain its functionality, ensure consistency, and detail its return values.
2024-10-06 16:21:37 +02:00
3333c784a6
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.
2024-10-06 16:19:53 +02:00
ac7fa5771a
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.
2024-10-06 16:16:49 +02:00
dab9cc947a
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.
2024-10-06 13:53:20 +02:00
2c1082fe42
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.
2024-10-06 13:51:36 +02:00
3e8706d52e
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.
2024-10-06 13:41:45 +02:00
756269644e
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.
2024-10-06 13:39:40 +02:00
d6426063ba
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.
2024-10-06 12:39:02 +02:00
b4197a136e
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.
2024-10-06 12:04:27 +02:00
01278ccb30
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.
2024-10-06 00:49:09 +02:00
eafb9cb17e
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.
2024-10-06 00:25:15 +02:00
864c593208
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.
2024-10-05 20:29:33 +02:00
4890d9130b
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.
2024-10-05 20:06:51 +02:00
b37f8995da
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.
2024-10-05 19:34:37 +02:00
c520925457
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.
2024-10-05 19:11:16 +02:00
3d5435c138
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.
2024-10-05 13:59:24 +02:00
1dcdad9da1
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.
2024-10-05 13:56:47 +02:00
78e2857782
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.
2024-10-05 13:39:21 +02:00
c186cba2c2
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.
2024-10-05 12:48:09 +02:00
682f7a6ca5
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.
2024-10-05 12:35:15 +02:00
cd4c0194dc
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.
2024-10-05 12:15:01 +02:00
a820ba3cee
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.
2024-10-05 12:14:00 +02:00
96466facdd
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.
2024-10-05 12:00:10 +02:00
493f8fc657
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.
2024-10-05 11:54:37 +02:00
94f47d4369
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.
2024-10-05 11:47:15 +02:00
476130d6e3
Fumpt files to make golangci-lint happy 2024-10-05 11:43:50 +02:00
a0a7f74121
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.
2024-10-05 11:42:21 +02:00
ecd0bff5ad
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.
2024-10-05 11:23:44 +02:00
5653df373b
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.
2024-10-05 11:11:17 +02:00
869e8db6c5
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.
2024-10-05 11:06:46 +02:00
fa3c6f956e
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.
2024-10-05 11:02:26 +02:00
159c1bf850
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.
2024-10-05 10:55:25 +02:00
9163943684
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.
2024-10-05 10:15:43 +02:00
fbbf17acd0
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.
2024-10-04 23:25:43 +02:00
48b469faf7
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.
2024-10-04 23:23:16 +02:00
adcb8ac41d
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.
2024-10-04 23:15:01 +02:00
dfdadc5da2
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.
2024-10-04 22:38:18 +02:00
8942b08424
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.
2024-10-04 22:36:28 +02:00
972a3c51c7
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.
2024-10-04 22:33:42 +02:00
d900f5403e
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.
2024-10-04 22:23:18 +02:00
eeaee3f60a
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.
2024-10-04 21:57:38 +02:00
bae0ac6cde
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.
2024-10-04 21:45:12 +02:00
3e5c93a418
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.
2024-10-04 21:38:39 +02:00
a34f400a05
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.
2024-10-04 21:26:29 +02:00
779a3f3942
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.
2024-10-04 21:12:05 +02:00
6cd3cfd2f7
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.
2024-10-04 20:57:51 +02:00
aab04672f8
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.
2024-10-04 20:39:14 +02:00
f7c12d412b
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.
2024-10-04 20:30:43 +02:00
ef3da39840
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.
2024-10-04 20:29:14 +02:00
92c411454b
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.
2024-10-04 20:13:13 +02:00
59e91eb936
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.
2024-10-04 20:00:49 +02:00
6a9c8bb56b
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.
2024-10-04 19:50:10 +02:00
ea90352ef4
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.
2024-10-04 19:45:37 +02:00
e8739b88b0
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.
2024-10-04 19:44:41 +02:00
b9888929f8
Merge pull request #323 from wneessen/more-scram-auth-test
Add support for SCRAM-SHA-1-PLUS and SCRAM-SHA-256-PLUS
2024-10-04 18:38:22 +02:00
711ce2ac65
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.
2024-10-04 18:31:58 +02:00
84f562554c
Merge pull request #322 from wneessen/gh-workflow-updates
Update GitHub Actions paths for Go and workflow files
2024-10-04 15:33:44 +02:00
d931050a6f
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`.
2024-10-04 15:28:22 +02:00
04023a1a25
Merge pull request #321 from wneessen/dependabot/github_actions/github/codeql-action-3.26.11
Bump github/codeql-action from 3.26.10 to 3.26.11
2024-10-04 15:23:46 +02:00
dependabot[bot]
fe36f3b294
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](e2b3eafc8d...6db8d6351f)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-04 13:17:57 +00:00
28103ede26
Merge pull request #319 from wneessen/codecov-testing
Update GH test workflows
2024-10-03 16:15:19 +02:00
19dcba620a
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.
2024-10-03 16:15:07 +02:00
6f10892d0b
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.
2024-10-03 16:01:58 +02:00
8f596ffae7
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.
2024-10-03 16:00:58 +02:00
f80b4dd8ac
Merge pull request #318 from wneessen/dependabot/github_actions/golangci/golangci-lint-action-6.1.1
Bump golangci/golangci-lint-action from 6.1.0 to 6.1.1
2024-10-03 15:44:55 +02:00
dependabot[bot]
94ed5646c5
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](aaa42aa062...971e284b60)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-03 13:39:45 +00:00
ff5454a61f
Merge pull request #317 from wneessen/more-auth-test-coverage
More test coverage for smtp/auth
2024-10-03 12:41:28 +02:00
4c8c0d855e
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.
2024-10-03 12:38:39 +02:00
03062c5183
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.
2024-10-03 12:32:06 +02:00
a8e89a1258
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.
2024-10-02 18:02:46 +02:00
e4dd62475a
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.
2024-10-02 18:02:34 +02:00
580981b158
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.
2024-10-02 18:02:23 +02:00
a41639ec07
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`.
2024-10-02 16:50:45 +02:00
d19c2fd87d
Merge pull request #316 from wneessen/trashmail-update
Update test recipient email in client tests
2024-10-02 16:29:13 +02:00
c8a8e9772a
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.
2024-10-02 16:25:41 +02:00
2bde374d2c
Merge pull request #313 from wneessen/dependabot/github_actions/codecov/codecov-action-4.6.0
Bump codecov/codecov-action from 4.5.0 to 4.6.0
2024-10-02 15:57:23 +02:00
97ad132965
Merge pull request #314 from wneessen/dependabot/github_actions/golang/govulncheck-action-1.0.4
Bump golang/govulncheck-action from 1.0.3 to 1.0.4
2024-10-02 15:57:09 +02:00
b7fa04e0cb
Merge pull request #315 from wneessen/fix-github-actions
Fix GitHub actions
2024-10-02 15:55:11 +02:00
0c3bf239f1
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.
2024-10-02 15:54:34 +02:00
cbba4d83d1
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.
2024-10-02 15:51:56 +02:00
dependabot[bot]
3f3b21348f
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](dd0578b371...b625fbe08f)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-02 13:45:37 +00:00
dependabot[bot]
e037df43a7
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](e28ff129e5...b9fd7d16f6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-02 13:45:34 +00:00
c8e45477bb
Merge pull request #312 from wneessen/bug/311_smtp-auth-login-should-follow-ietf-draft-more-closely
Enhance SMTP LOGIN auth and add comprehensive tests
2024-10-02 14:18:38 +02:00
761e205049
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.
2024-10-02 13:10:10 +02:00
9d70283af9
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.
2024-10-02 13:09:55 +02:00
93752280aa
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.
2024-10-02 12:54:32 +02:00
547f78dbee
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.
2024-10-02 12:37:54 +02:00
9bafa969b8
Merge pull request #310 from wneessen/feature/242_support-scram-sha
SCRAM-SHA-1(-PLUS) / SCRAM-SHA-256(-PLUS) support
2024-10-01 20:52:08 +02:00
72b3f53eb7
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.
2024-10-01 20:45:07 +02:00
986a988c5d
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.
2024-10-01 20:44:50 +02:00
f823112a4d
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.
2024-10-01 20:32:41 +02:00
15b9ddf067
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.
2024-10-01 17:23:29 +02:00
5058fd5222
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.
2024-10-01 17:01:10 +02:00
8838414c38
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.
2024-10-01 17:00:57 +02:00
b69ad27de3
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.
2024-10-01 17:00:43 +02:00
7499bae3eb
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.
2024-10-01 16:45:02 +02:00
324be9d032
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.
2024-10-01 16:43:36 +02:00
bcf7084982
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.
2024-10-01 16:39:13 +02:00
abab0af2a3
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.
2024-10-01 16:01:58 +02:00
687843ee53
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.
2024-10-01 15:57:01 +02:00
cace4890bc
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.
2024-10-01 15:47:07 +02:00
e5b87db448
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.
2024-10-01 15:41:26 +02:00
5b5991f17d
Merge branch 'main' into feature/242_support-scram-sha 2024-10-01 15:30:22 +02:00
e8f3c444e6
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.
2024-10-01 15:28:53 +02:00
27838f5b1f
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.
2024-10-01 15:28:10 +02:00
3013975c6a
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.
2024-10-01 15:27:31 +02:00
c797f0be17
Add REUSE.toml
Replaced deprecated .reuse/dep5 with REUSE.toml config file
2024-10-01 15:27:05 +02:00
b96badbd59
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.
2024-10-01 15:25:51 +02:00
738f43e289
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.
2024-10-01 11:05:42 +02:00
ebd171005d
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.
2024-10-01 11:05:07 +02:00
4f1a60760d
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.
2024-10-01 11:04:16 +02:00
e8fc6cd78f
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.
2024-10-01 11:04:01 +02:00
9069c9cdff
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.
2024-10-01 11:03:44 +02:00
627216425f
Merge pull request #309 from wneessen/dependabot/github_actions/github/codeql-action-3.26.10
Bump github/codeql-action from 3.26.9 to 3.26.10
2024-09-30 15:42:23 +02:00
dependabot[bot]
012082978d
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](461ef6c76d...e2b3eafc8d)

---
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-30 13:36:41 +00:00
65a91a2711
Merge pull request #307 from wneessen/feature/269_goroutineconcurrency-safety
go-mail goroutine-/thread-safety
2024-09-27 17:17:00 +02:00
c1f6ef07d4
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.
2024-09-27 17:09:00 +02:00
6e98d7e47d
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.
2024-09-27 17:00:21 +02:00
8791ce5a33
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.
2024-09-27 17:00:07 +02:00
2d98c40cb6
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.
2024-09-27 14:04:02 +02:00
253d065c83
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.
2024-09-27 14:03:50 +02:00
6bd9a9c735
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`.
2024-09-27 14:03:26 +02:00
f59aa23ed8
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.
2024-09-27 11:58:08 +02:00
2234f0c5bc
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.
2024-09-27 11:43:22 +02:00
fdb80ad9dd
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.
2024-09-27 11:10:23 +02:00
fec2f2075a
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.
2024-09-27 10:52:30 +02:00
2084526c77
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.
2024-09-27 10:36:09 +02:00
23c71d608f
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.
2024-09-27 10:33:28 +02:00
371b950bc7
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.
2024-09-27 10:33:19 +02:00
b2c4b533d7
Merge branch 'main' into feature/269_goroutineconcurrency-safety 2024-09-27 10:26:39 +02:00
3871b2be44
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.
2024-09-26 11:51:30 +02:00
8683917c3d
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
2024-09-26 11:49:48 +02: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
fd115d5173
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.
2024-09-23 14:15:43 +02:00
4f6224131e
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.
2024-09-23 13:46:41 +02:00
c8684886ed
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.
2024-09-23 13:44:03 +02:00
07e7b17ae8
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.
2024-09-23 11:45:22 +02:00
2abdee743d
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.
2024-09-23 11:27:05 +02:00
5503be8451
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.
2024-09-23 11:26:54 +02:00
774925078a
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.
2024-09-23 11:17:58 +02:00
f1188bdad7
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.
2024-09-23 11:15:56 +02:00
2cbd0c4aef
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.
2024-09-23 11:09:11 +02:00
9a9e0c936d
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.
2024-09-23 11:09:03 +02:00
33d4eb5b21
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.
2024-09-23 10:33:06 +02:00
d6e5034bba
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.
2024-09-23 10:09:38 +02:00
1394f1fc20
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.
2024-09-23 09:56:23 +02:00
26ff177fb0
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.
2024-09-22 21:12:59 +02:00
5ec8a5a5fe
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.
2024-09-22 20:48:09 +02: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
53 changed files with 7133 additions and 1288 deletions

View file

@ -14,12 +14,10 @@ freebsd_task:
image_family: freebsd-14-0 image_family: freebsd-14-0
env: env:
TEST_ALLOW_SEND: 0
TEST_SKIP_SENDMAIL: 1 TEST_SKIP_SENDMAIL: 1
pkginstall_script: pkginstall_script:
- pkg update -f
- pkg install -y go - pkg install -y go
test_script: test_script:
- go test -v -race -cover -shuffle=on ./... - go test -race -cover -shuffle=on ./...

View file

@ -10,7 +10,7 @@ on:
paths: paths:
- '**.go' - '**.go'
- 'go.*' - 'go.*'
- '.github/**' - '.github/workflows/codecov.yml'
- 'codecov.yml' - 'codecov.yml'
pull_request: pull_request:
branches: branches:
@ -18,7 +18,7 @@ on:
paths: paths:
- '**.go' - '**.go'
- 'go.*' - 'go.*'
- '.github/**' - '.github/workflows/codecov.yml'
- 'codecov.yml' - 'codecov.yml'
env: env:
TEST_HOST: ${{ secrets.TEST_HOST }} TEST_HOST: ${{ secrets.TEST_HOST }}
@ -27,6 +27,10 @@ env:
TEST_SMTPAUTH_USER: ${{ secrets.TEST_USER }} TEST_SMTPAUTH_USER: ${{ secrets.TEST_USER }}
TEST_SMTPAUTH_PASS: ${{ secrets.TEST_PASS }} TEST_SMTPAUTH_PASS: ${{ secrets.TEST_PASS }}
TEST_SMTPAUTH_TYPE: "LOGIN" 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 }}
permissions: permissions:
contents: read contents: read
@ -36,10 +40,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.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
@ -55,9 +59,9 @@ jobs:
sudo apt-get -y install sendmail; which sendmail sudo apt-get -y install sendmail; which sendmail
- name: Run Tests - name: Run Tests
run: | run: |
go test -v -race --coverprofile=coverage.coverprofile --covermode=atomic ./... go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
if: success() && matrix.go == '1.23' && matrix.os == 'ubuntu-latest' 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: with:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos

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@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
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@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
# 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@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12

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
@ -29,7 +29,7 @@ jobs:
go-version: '1.23' go-version: '1.23'
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0 uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1
with: with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version # 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 version: latest

View file

@ -14,8 +14,8 @@ 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
uses: golang/govulncheck-action@dd0578b371c987f96d1185abb54344b44352bd58 # v1.0.3 uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4

45
.github/workflows/offline-tests.yml vendored Normal file
View file

@ -0,0 +1,45 @@
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
#
# SPDX-License-Identifier: CC0-1.0
name: Offline tests workflow
on:
push:
branches:
- main
paths:
- '**.go'
- 'go.*'
- '.github/workflows/offline-tests.yml'
pull_request:
branches:
- main
paths:
- '**.go'
- 'go.*'
- '.github/workflows/offline-tests.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 ./...

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

View file

@ -10,24 +10,25 @@ permissions:
on: on:
push: push:
branches: branches:
- main # or the name of your main branch - main
paths:
- '**.go'
- 'go.*'
- '.github/workflows/sonarqube.yml'
pull_request: pull_request:
branches: branches:
- main # or the name of your main branch - main
env: paths:
TEST_HOST: ${{ secrets.TEST_HOST }} - '**.go'
TEST_FROM: ${{ secrets.TEST_USER }} - 'go.*'
TEST_ALLOW_SEND: "1" - '.github/workflows/sonarqube.yml'
TEST_SMTPAUTH_USER: ${{ secrets.TEST_USER }}
TEST_SMTPAUTH_PASS: ${{ secrets.TEST_PASS }}
TEST_SMTPAUTH_TYPE: "LOGIN"
jobs: jobs:
build: build:
name: Build name: Build
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
@ -38,13 +39,13 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with: with:
go-version: '1.23.x' go-version: '1.23'
- name: Run unit Tests - name: Run unit Tests
run: | run: |
go test -v -race --coverprofile=./cov.out ./... go test -shuffle=on -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

@ -1,10 +0,0 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: go-mail
Upstream-Contact: Winni Neessen <winni@neessen.dev>
Source: https://github.com/wneessen/go-mail
# Sample paragraph, commented out:
#
# Files: src/*
# Copyright: $YEAR $NAME <$CONTACT>
# License: ...

View file

@ -18,40 +18,41 @@ SPDX-License-Identifier: CC0-1.0
<p align="center"><img src="./assets/gopher2.svg" width="250" alt="go-mail logo"/></p> <p align="center"><img src="./assets/gopher2.svg" width="250" alt="go-mail logo"/></p>
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. 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 go-mail follows idiomatic Go style and best practice. It has a small dependency footprint by mainly relying on the
of functionality from the standard library to give easy and convenient access to mail and SMTP related tasks. 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 In the early days, parts of this library (especially some parts of [msgwriter.go](msgwriter.go)) had been
[go-mail/mail](https://github.com/go-mail/mail) respectively [go-gomail/gomail](https://github.com/go-gomail/gomail) forked/ported from [go-mail/mail](https://github.com/go-mail/mail) and respectively [go-gomail/gomail](https://github.com/go-gomail/gomail). Today
which both seems to not be maintained anymore. 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 The `smtp` package of go-mail has been forked from the original Go stdlib's `net/smtp` package and has then been extended
team. by the go-mail team to fit the packages needs (more SMTP Auth methods, logging, concurrency-safety, etc.).
## Features ## 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] Modern, idiomatic Go
* [X] Sane and secure defaults * [X] Sane and secure defaults
* [X] Explicit SSL/TLS support * [X] Explicit SSL/TLS support
* [X] Implicit StartTLS support with different policies * [X] Implicit StartTLS support with different policies
* [X] Makes use of contexts for a better control flow and timeout/cancelation handling * [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] RFC5322 compliant mail address validation
* [X] Support for common mail header field generation (Message-ID, Date, Bulk-Precedence, Priority, etc.) * [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 attachments and inline embeds (from file system, `io.Reader` or `embed.FS`)
* [X] Support for different encodings * [X] Support for different encodings
* [X] Middleware support for 3rd-party libraries to alter mail messages * [X] Middleware support for 3rd-party libraries to alter mail messages
* [X] Support sending mails via a local sendmail command * [X] Support sending mails via a local sendmail command
* [X] Support for requestng MDNs (RFC 8098) and DSNs (RFC 1891) * [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] 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] 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] 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 * [X] Debug logging of SMTP traffic
@ -76,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. the user a timeframe of two years to update to the next or even the latest version of Go.
## Support ## 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 ## Middleware
The goal of go-mail is to keep it free from 3rd party dependencies and only focus on things a mail library should The goal of go-mail is to keep it free from 3rd party dependencies and only focus on things a mail library should
@ -99,15 +101,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. check out our [Getting started](https://go-mail.dev/getting-started/introduction/) guide.
## Authors/Contributors ## 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 <a href="https://github.com/wneessen/go-mail/graphs/contributors">
reviewing code, writing documenation or helping to translate the website): <img src="https://contrib.rocks/image?repo=wneessen/go-mail" />
* [Christian Vette](https://github.com/cvette) </a>
* [Dhia Gharsallaoui](https://github.com/dhia-gharsallaoui)
* [inliquid](https://github.com/inliquid) A huge thank you also goes to [Maria Letta](https://github.com/MariaLetta) for designing our super cool go-mail logo!
* [iwittkau](https://github.com/iwittkau)
* [James Elliott](https://github.com/james-d-elliott) ## Sponsors
* [Maria Letta](https://github.com/MariaLetta) (designed the go-mail logo) We sincerely thank our amazing sponsors for their generous support! Your contributions do not go unnoticed and helps
* [Nicola Murino](https://github.com/drakkan) keeping up the project!
* [sters](https://github.com/sters)
* [kolaente](https://github.com/kolaente)

9
REUSE.toml Normal file
View file

@ -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 <winni@neessen.dev>"
SPDX-PackageDownloadLocation = "https://github.com/wneessen/go-mail"
annotations = []

110
auth.go
View file

@ -6,41 +6,131 @@ package mail
import "errors" 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 type SMTPAuthType string
// Supported SMTP AUTH types
const ( 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/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
// 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" SMTPAuthCramMD5 SMTPAuthType = "CRAM-MD5"
// SMTPAuthLogin is the "LOGIN" SASL authentication mechanism // 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
// automatically matches the MS spec.
//
// 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" SMTPAuthLogin SMTPAuthType = "LOGIN"
// SMTPAuthNoAuth is equivalent to performing no authentication at all. It is a convenience // SMTPAuthNoAuth is equivalent to performing no authentication at all. It is a convenience
// option and should not be used. Instead, for mail servers that do no support/require // option and should not be used. Instead, for mail servers that do no support/require
// authentication, the Client should not be used with the WithSMTPAuth option // authentication, the Client should not be passed the WithSMTPAuth option at all.
SMTPAuthNoAuth SMTPAuthType = "" SMTPAuthNoAuth SMTPAuthType = ""
// SMTPAuthPlain is the "PLAIN" authentication mechanism as described in RFC 4616 // SMTPAuthPlain is the "PLAIN" authentication mechanism as described in RFC 4616.
//
// Since the "PLAIN" SASL authentication mechansim transmits the username and password in
// 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" SMTPAuthPlain SMTPAuthType = "PLAIN"
// SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism. // SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism.
// https://developers.google.com/gmail/imap/xoauth2-protocol // https://developers.google.com/gmail/imap/xoauth2-protocol
SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2" SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2"
// SMTPAuthSCRAMSHA1 is the "SCRAM-SHA-1" SASL authentication mechanism as described in RFC 5802.
//
// 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.
//
// 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.
//
// 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.
//
// 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"
) )
// SMTP Auth related static errors // SMTP Auth related static errors
var ( 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") 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") 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") 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") ErrXOauth2AuthNotSupported = errors.New("server does not support SMTP AUTH type: XOAUTH2")
// 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 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 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 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")
) )

View file

@ -9,21 +9,39 @@ import (
"io" "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" const ErrNoOutWriter = "no io.Writer set for Base64LineBreaker"
// Base64LineBreaker is a io.WriteCloser that writes Base64 encoded data streams // Base64LineBreaker handles base64 encoding with the insertion of new lines after a certain number
// with line breaks at a given line length // of characters.
//
// 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 { type Base64LineBreaker struct {
line [MaxBodyLength]byte line [MaxBodyLength]byte
used int used int
out io.Writer out io.Writer
} }
var newlineBytes = []byte(SingleNewLine) // Write writes data to the Base64LineBreaker, ensuring lines do not exceed MaxBodyLength.
//
// Write writes the data stream and inserts a SingleNewLine when the maximum // This method writes the provided data to the Base64LineBreaker. It ensures that the written
// line length is reached // 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) { func (l *Base64LineBreaker) Write(data []byte) (numBytes int, err error) {
if l.out == nil { if l.out == nil {
err = errors.New(ErrNoOutWriter) err = errors.New(ErrNoOutWriter)
@ -55,8 +73,14 @@ func (l *Base64LineBreaker) Write(data []byte) (numBytes int, err error) {
return l.Write(data[excess:]) return l.Write(data[excess:])
} }
// Close closes the Base64LineBreaker and writes any access data that is still // Close finalizes the Base64LineBreaker, writing any remaining buffered data and appending a newline.
// unwritten in memory //
// 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) { func (l *Base64LineBreaker) Close() (err error) {
if l.used > 0 { if l.used > 0 {
_, err = l.out.Write(l.line[0:l.used]) _, err = l.out.Write(l.line[0:l.used])

1082
client.go

File diff suppressed because it is too large Load diff

View file

@ -7,111 +7,39 @@
package mail package mail
import "strings" 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 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 { 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,101 +9,38 @@ package mail
import ( import (
"errors" "errors"
"strings"
) )
// 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 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) { func (c *Client) Send(messages ...*Msg) (returnErr error) {
if err := c.checkConn(); err != nil { if err := c.checkConn(); err != nil {
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)
} }
} }

File diff suppressed because it is too large Load diff

11
doc.go
View file

@ -2,8 +2,13 @@
// //
// SPDX-License-Identifier: MIT // 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 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
const VERSION = "0.4.4" // agent string.
const VERSION = "0.5.0"

205
eml.go
View file

@ -18,14 +18,35 @@ import (
"strings" "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) { func EMLToMsgFromString(emlString string) (*Msg, error) {
eb := bytes.NewBufferString(emlString) eb := bytes.NewBufferString(emlString)
return EMLToMsgFromReader(eb) return EMLToMsgFromReader(eb)
} }
// EMLToMsgFromReader will parse a reader that holds EML content and returns a pre-filled // EMLToMsgFromReader parses a reader that holds EML content and returns a pre-filled Msg pointer.
// 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) { func EMLToMsgFromReader(reader io.Reader) (*Msg, error) {
msg := &Msg{ msg := &Msg{
addrHeader: make(map[AddrHeader][]*netmail.Address), addrHeader: make(map[AddrHeader][]*netmail.Address),
@ -46,8 +67,19 @@ func EMLToMsgFromReader(reader io.Reader) (*Msg, error) {
return msg, nil return msg, nil
} }
// EMLToMsgFromFile will open and parse a .eml file at a provided file path and returns a // EMLToMsgFromFile opens and parses a .eml file at a provided file path and returns a
// pre-filled Msg pointer // 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) { func EMLToMsgFromFile(filePath string) (*Msg, error) {
msg := &Msg{ msg := &Msg{
addrHeader: make(map[AddrHeader][]*netmail.Address), addrHeader: make(map[AddrHeader][]*netmail.Address),
@ -68,7 +100,19 @@ func EMLToMsgFromFile(filePath string) (*Msg, error) {
return msg, nil 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.
//
// 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 { func parseEML(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
if err := parseEMLHeaders(&parsedMsg.Header, msg); err != nil { if err := parseEMLHeaders(&parsedMsg.Header, msg); err != nil {
return fmt.Errorf("failed to parse EML headers: %w", err) return fmt.Errorf("failed to parse EML headers: %w", err)
@ -79,7 +123,18 @@ func parseEML(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error
return nil 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.
//
// 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) { func readEML(filePath string) (*netmail.Message, *bytes.Buffer, error) {
fileHandle, err := os.Open(filePath) fileHandle, err := os.Open(filePath)
if err != nil { if err != nil {
@ -91,7 +146,19 @@ func readEML(filePath string) (*netmail.Message, *bytes.Buffer, error) {
return readEMLFromReader(fileHandle) 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.
//
// 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) { func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error) {
parsedMsg, err := netmail.ReadMessage(reader) parsedMsg, err := netmail.ReadMessage(reader)
if err != nil { if err != nil {
@ -106,8 +173,18 @@ func readEMLFromReader(reader io.Reader) (*netmail.Message, *bytes.Buffer, error
return parsedMsg, &buf, nil return parsedMsg, &buf, nil
} }
// parseEMLHeaders will check the EML headers for the most common headers and set the // parseEMLHeaders parses the EML's headers and populates the Msg with relevant information.
// according settings in the Msg //
// 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 { func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error {
commonHeaders := []Header{ commonHeaders := []Header{
HeaderContentType, HeaderImportance, HeaderInReplyTo, HeaderListUnsubscribe, HeaderContentType, HeaderImportance, HeaderInReplyTo, HeaderListUnsubscribe,
@ -175,7 +252,19 @@ func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error {
return nil 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 { func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
// Extract the transfer encoding of the body // Extract the transfer encoding of the body
mediatype, params, err := mime.ParseMediaType(parsedMsg.Header.Get(HeaderContentType.String())) mediatype, params, err := mime.ParseMediaType(parsedMsg.Header.Get(HeaderContentType.String()))
@ -212,10 +301,24 @@ func parseEMLBodyParts(parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *M
return nil 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 { func parseEMLBodyPlain(mediatype string, parsedMsg *netmail.Message, bodybuf *bytes.Buffer, msg *Msg) error {
contentTransferEnc := parsedMsg.Header.Get(HeaderContentTransferEnc.String()) 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()) { if contentTransferEnc == "" || strings.EqualFold(contentTransferEnc, EncodingUSASCII.String()) {
msg.SetEncoding(EncodingUSASCII) msg.SetEncoding(EncodingUSASCII)
msg.SetBodyString(ContentType(mediatype), bodybuf.String()) msg.SetBodyString(ContentType(mediatype), bodybuf.String())
@ -249,7 +352,20 @@ func parseEMLBodyPlain(mediatype string, parsedMsg *netmail.Message, bodybuf *by
return fmt.Errorf("unsupported Content-Transfer-Encoding") 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 { func parseEMLMultipart(params map[string]string, bodybuf *bytes.Buffer, msg *Msg) error {
boundary, ok := params["boundary"] boundary, ok := params["boundary"]
if !ok { if !ok {
@ -349,7 +465,15 @@ ReadNextPart:
return nil return nil
} }
// parseEMLEncoding parses and determines the encoding of the message // 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) { func parseEMLEncoding(mailHeader *netmail.Header, msg *Msg) {
if value := mailHeader.Get(HeaderContentTransferEnc.String()); value != "" { if value := mailHeader.Get(HeaderContentTransferEnc.String()); value != "" {
switch { switch {
@ -363,7 +487,15 @@ 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.
//
// 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) { func parseEMLContentTypeCharset(mailHeader *netmail.Header, msg *Msg) {
if value := mailHeader.Get(HeaderContentType.String()); value != "" { if value := mailHeader.Get(HeaderContentType.String()); value != "" {
contentType, optional := parseMultiPartHeader(value) contentType, optional := parseMultiPartHeader(value)
@ -377,7 +509,18 @@ 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.
//
// 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 { func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error {
part.SetEncoding(EncodingB64) part.SetEncoding(EncodingB64)
content, err := base64.StdEncoding.DecodeString(string(multiPartData)) content, err := base64.StdEncoding.DecodeString(string(multiPartData))
@ -388,8 +531,17 @@ func handleEMLMultiPartBase64Encoding(multiPartData []byte, part *Part) error {
return nil return nil
} }
// parseMultiPartHeader parses a multipart header and returns the value and optional parts as // parseMultiPartHeader parses a multipart header and returns the value and optional parts as a map.
// separate 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) { func parseMultiPartHeader(multiPartHeader string) (header string, optional map[string]string) {
optional = make(map[string]string) optional = make(map[string]string)
headerSplit := strings.SplitN(multiPartHeader, ";", 2) headerSplit := strings.SplitN(multiPartHeader, ";", 2)
@ -404,7 +556,20 @@ func parseMultiPartHeader(multiPartHeader string) (header string, optional map[s
return return
} }
// parseEMLAttachmentEmbed parses a multipart that is an attachment or embed // 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 { func parseEMLAttachmentEmbed(contentDisposition []string, multiPart *multipart.Part, msg *Msg) error {
cdType, optional := parseMultiPartHeader(contentDisposition[0]) cdType, optional := parseMultiPartHeader(contentDisposition[0])
filename := "generic.attachment" filename := "generic.attachment"

View file

@ -4,173 +4,226 @@
package mail 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 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 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 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 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 type MIMEType string
// List of supported encodings
const ( const (
// EncodingB64 represents the Base64 encoding as specified in RFC 2045. // EncodingB64 represents the Base64 encoding as specified in RFC 2045.
//
// https://datatracker.ietf.org/doc/html/rfc2045#section-6.8
EncodingB64 Encoding = "base64" EncodingB64 Encoding = "base64"
// EncodingQP represents the "quoted-printable" encoding as specified in RFC 2045. // 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" EncodingQP Encoding = "quoted-printable"
// EncodingUSASCII represents encoding with only US-ASCII characters (aka 7Bit) // EncodingUSASCII represents encoding with only US-ASCII characters (aka 7Bit)
//
// https://datatracker.ietf.org/doc/html/rfc2045#section-2.7
EncodingUSASCII Encoding = "7bit" 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" NoEncoding Encoding = "8bit"
) )
// List of common charsets
const ( const (
// CharsetUTF7 represents the "UTF-7" charset // CharsetUTF7 represents the "UTF-7" charset.
CharsetUTF7 Charset = "UTF-7" CharsetUTF7 Charset = "UTF-7"
// CharsetUTF8 represents the "UTF-8" charset // CharsetUTF8 represents the "UTF-8" charset.
CharsetUTF8 Charset = "UTF-8" CharsetUTF8 Charset = "UTF-8"
// CharsetASCII represents the "US-ASCII" charset // CharsetASCII represents the "US-ASCII" charset.
CharsetASCII Charset = "US-ASCII" CharsetASCII Charset = "US-ASCII"
// CharsetISO88591 represents the "ISO-8859-1" charset // CharsetISO88591 represents the "ISO-8859-1" charset.
CharsetISO88591 Charset = "ISO-8859-1" CharsetISO88591 Charset = "ISO-8859-1"
// CharsetISO88592 represents the "ISO-8859-2" charset // CharsetISO88592 represents the "ISO-8859-2" charset.
CharsetISO88592 Charset = "ISO-8859-2" CharsetISO88592 Charset = "ISO-8859-2"
// CharsetISO88593 represents the "ISO-8859-3" charset // CharsetISO88593 represents the "ISO-8859-3" charset.
CharsetISO88593 Charset = "ISO-8859-3" CharsetISO88593 Charset = "ISO-8859-3"
// CharsetISO88594 represents the "ISO-8859-4" charset // CharsetISO88594 represents the "ISO-8859-4" charset.
CharsetISO88594 Charset = "ISO-8859-4" CharsetISO88594 Charset = "ISO-8859-4"
// CharsetISO88595 represents the "ISO-8859-5" charset // CharsetISO88595 represents the "ISO-8859-5" charset.
CharsetISO88595 Charset = "ISO-8859-5" CharsetISO88595 Charset = "ISO-8859-5"
// CharsetISO88596 represents the "ISO-8859-6" charset // CharsetISO88596 represents the "ISO-8859-6" charset.
CharsetISO88596 Charset = "ISO-8859-6" CharsetISO88596 Charset = "ISO-8859-6"
// CharsetISO88597 represents the "ISO-8859-7" charset // CharsetISO88597 represents the "ISO-8859-7" charset.
CharsetISO88597 Charset = "ISO-8859-7" CharsetISO88597 Charset = "ISO-8859-7"
// CharsetISO88599 represents the "ISO-8859-9" charset // CharsetISO88599 represents the "ISO-8859-9" charset.
CharsetISO88599 Charset = "ISO-8859-9" CharsetISO88599 Charset = "ISO-8859-9"
// CharsetISO885913 represents the "ISO-8859-13" charset // CharsetISO885913 represents the "ISO-8859-13" charset.
CharsetISO885913 Charset = "ISO-8859-13" CharsetISO885913 Charset = "ISO-8859-13"
// CharsetISO885914 represents the "ISO-8859-14" charset // CharsetISO885914 represents the "ISO-8859-14" charset.
CharsetISO885914 Charset = "ISO-8859-14" CharsetISO885914 Charset = "ISO-8859-14"
// CharsetISO885915 represents the "ISO-8859-15" charset // CharsetISO885915 represents the "ISO-8859-15" charset.
CharsetISO885915 Charset = "ISO-8859-15" CharsetISO885915 Charset = "ISO-8859-15"
// CharsetISO885916 represents the "ISO-8859-16" charset // CharsetISO885916 represents the "ISO-8859-16" charset.
CharsetISO885916 Charset = "ISO-8859-16" CharsetISO885916 Charset = "ISO-8859-16"
// CharsetISO2022JP represents the "ISO-2022-JP" charset // CharsetISO2022JP represents the "ISO-2022-JP" charset.
CharsetISO2022JP Charset = "ISO-2022-JP" CharsetISO2022JP Charset = "ISO-2022-JP"
// CharsetISO2022KR represents the "ISO-2022-KR" charset // CharsetISO2022KR represents the "ISO-2022-KR" charset.
CharsetISO2022KR Charset = "ISO-2022-KR" CharsetISO2022KR Charset = "ISO-2022-KR"
// CharsetWindows1250 represents the "windows-1250" charset // CharsetWindows1250 represents the "windows-1250" charset.
CharsetWindows1250 Charset = "windows-1250" CharsetWindows1250 Charset = "windows-1250"
// CharsetWindows1251 represents the "windows-1251" charset // CharsetWindows1251 represents the "windows-1251" charset.
CharsetWindows1251 Charset = "windows-1251" CharsetWindows1251 Charset = "windows-1251"
// CharsetWindows1252 represents the "windows-1252" charset // CharsetWindows1252 represents the "windows-1252" charset.
CharsetWindows1252 Charset = "windows-1252" CharsetWindows1252 Charset = "windows-1252"
// CharsetWindows1255 represents the "windows-1255" charset // CharsetWindows1255 represents the "windows-1255" charset.
CharsetWindows1255 Charset = "windows-1255" CharsetWindows1255 Charset = "windows-1255"
// CharsetWindows1256 represents the "windows-1256" charset // CharsetWindows1256 represents the "windows-1256" charset.
CharsetWindows1256 Charset = "windows-1256" CharsetWindows1256 Charset = "windows-1256"
// CharsetKOI8R represents the "KOI8-R" charset // CharsetKOI8R represents the "KOI8-R" charset.
CharsetKOI8R Charset = "KOI8-R" CharsetKOI8R Charset = "KOI8-R"
// CharsetKOI8U represents the "KOI8-U" charset // CharsetKOI8U represents the "KOI8-U" charset.
CharsetKOI8U Charset = "KOI8-U" CharsetKOI8U Charset = "KOI8-U"
// CharsetBig5 represents the "Big5" charset // CharsetBig5 represents the "Big5" charset.
CharsetBig5 Charset = "Big5" CharsetBig5 Charset = "Big5"
// CharsetGB18030 represents the "GB18030" charset // CharsetGB18030 represents the "GB18030" charset.
CharsetGB18030 Charset = "GB18030" CharsetGB18030 Charset = "GB18030"
// CharsetGB2312 represents the "GB2312" charset // CharsetGB2312 represents the "GB2312" charset.
CharsetGB2312 Charset = "GB2312" CharsetGB2312 Charset = "GB2312"
// CharsetTIS620 represents the "TIS-620" charset // CharsetTIS620 represents the "TIS-620" charset.
CharsetTIS620 Charset = "TIS-620" CharsetTIS620 Charset = "TIS-620"
// CharsetEUCKR represents the "EUC-KR" charset // CharsetEUCKR represents the "EUC-KR" charset.
CharsetEUCKR Charset = "EUC-KR" CharsetEUCKR Charset = "EUC-KR"
// CharsetShiftJIS represents the "Shift_JIS" charset // CharsetShiftJIS represents the "Shift_JIS" charset.
CharsetShiftJIS Charset = "Shift_JIS" CharsetShiftJIS Charset = "Shift_JIS"
// CharsetUnknown represents the "Unknown" charset // CharsetUnknown represents the "Unknown" charset.
CharsetUnknown Charset = "Unknown" CharsetUnknown Charset = "Unknown"
// CharsetGBK represents the "GBK" charset // CharsetGBK represents the "GBK" charset.
CharsetGBK Charset = "GBK" CharsetGBK Charset = "GBK"
) )
// List of MIME versions // MIME10 represents the MIME version "1.0" used in email messages.
const ( const MIME10 MIMEVersion = "1.0"
// MIME10 is the MIME Version 1.0
MIME10 MIMEVersion = "1.0"
)
// List of common content types
const ( const (
// TypeAppOctetStream represents the MIME type for arbitrary binary data.
TypeAppOctetStream ContentType = "application/octet-stream" TypeAppOctetStream ContentType = "application/octet-stream"
// TypeMultipartAlternative represents the MIME type for a message body that can contain multiple alternative
// formats.
TypeMultipartAlternative ContentType = "multipart/alternative" TypeMultipartAlternative ContentType = "multipart/alternative"
// TypeMultipartMixed represents the MIME type for a multipart message containing different parts.
TypeMultipartMixed ContentType = "multipart/mixed" 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" TypeMultipartRelated ContentType = "multipart/related"
// TypePGPSignature represents the MIME type for PGP signed messages.
TypePGPSignature ContentType = "application/pgp-signature" TypePGPSignature ContentType = "application/pgp-signature"
// TypePGPEncrypted represents the MIME type for PGP encrypted messages.
TypePGPEncrypted ContentType = "application/pgp-encrypted" TypePGPEncrypted ContentType = "application/pgp-encrypted"
// TypeTextHTML represents the MIME type for HTML text content.
TypeTextHTML ContentType = "text/html" TypeTextHTML ContentType = "text/html"
// TypeTextPlain represents the MIME type for plain text content.
TypeTextPlain ContentType = "text/plain" TypeTextPlain ContentType = "text/plain"
// typeSMimeSigned represents the MIME type for S/MIME singed messages.
typeSMimeSigned ContentType = `application/pkcs7-signature; name="smime.p7s"` typeSMimeSigned ContentType = `application/pkcs7-signature; name="smime.p7s"`
) )
// List of MIMETypes
const ( const (
// MIMEAlternative MIMEType represents a MIME multipart/alternative type, used for emails with multiple versions.
MIMEAlternative MIMEType = "alternative" MIMEAlternative MIMEType = "alternative"
// MIMEMixed MIMEType represents a MIME multipart/mixed type used for emails containing different types of content.
MIMEMixed MIMEType = "mixed" MIMEMixed MIMEType = "mixed"
// MIMERelated MIMEType represents a MIME multipart/related type, used for emails with related content entities.
MIMERelated MIMEType = "related" MIMERelated MIMEType = "related"
MIMESMime MIMEType = `signed; protocol="application/pkcs7-signature"; micalg=sha256` // MIMESMime MIMEType represents a MIME multipart/signed type, used for siging emails with S/MIME.
MIMESMime MIMEType = `signed; protocol="application/pkcs7-signature"; micalg=sha-256`
) )
// 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.
//
// 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 { func (c Charset) String() string {
return string(c) 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.
//
// 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 { func (c ContentType) String() string {
return string(c) 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.
//
// 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 { func (e Encoding) String() string {
return string(e) return string(e)
} }
// String is a standard method to convert an MIMEType into a printable format
func (e MIMEType) String() string {
return string(e)
}

View file

@ -126,3 +126,24 @@ func TestCharset_String(t *testing.T) {
}) })
} }
} }
// TestContentType_String tests the mime type method of the MIMEType object
func TestMimeType_String(t *testing.T) {
tests := []struct {
mt MIMEType
want string
}{
{MIMEAlternative, "alternative"},
{MIMEMixed, "mixed"},
{MIMERelated, "related"},
{MIMESMime, `signed; protocol="application/pkcs7-signature"; micalg=sha-256`},
}
for _, tt := range tests {
t.Run(tt.mt.String(), func(t *testing.T) {
if tt.mt.String() != tt.want {
t.Errorf("wrong string for mime type returned. Expected: %s, got: %s",
tt.want, tt.mt.String())
}
})
}
}

97
file.go
View file

@ -9,10 +9,15 @@ import (
"net/textproto" "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) type FileOption func(*File)
// File is an attachment or embedded file of the 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 { type File struct {
ContentType ContentType ContentType ContentType
Desc string Desc string
@ -22,32 +27,68 @@ type File struct {
Writer func(w io.Writer) (int64, error) 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.
//
// 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 { func WithFileContentID(id string) FileOption {
return func(f *File) { return func(f *File) {
f.Header.Set(HeaderContentID.String(), id) f.Header.Set(HeaderContentID.String(), id)
} }
} }
// WithFileName sets the filename of the File // 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 { func WithFileName(name string) FileOption {
return func(f *File) { return func(f *File) {
f.Name = name f.Name = name
} }
} }
// WithFileDescription sets an optional file description of the File that will be // WithFileDescription sets an optional description for the File, which is used in the Content-Description
// added as Content-Description part // 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 { func WithFileDescription(description string) FileOption {
return func(f *File) { return func(f *File) {
f.Desc = description f.Desc = description
} }
} }
// WithFileEncoding sets the encoding of the File. By default we should always use // WithFileEncoding sets the encoding type for a File.
// 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 // This function allows the specification of an encoding type for the file, typically used for attachments
// is provided as argument, the function will automatically override back to Base64 // or embedded files. By default, Base64 encoding should be used, but this function can override the
// default if needed.
//
// 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 { func WithFileEncoding(encoding Encoding) FileOption {
return func(f *File) { return func(f *File) {
if encoding == EncodingQP { if encoding == EncodingQP {
@ -58,23 +99,45 @@ func WithFileEncoding(encoding Encoding) FileOption {
} }
// WithFileContentType sets the content type of the File. // 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 // By default, the content type is guessed based on the file type, and if no matching type is identified,
// could not be guessed. In some cases, however, it might be needed to force // the default "application/octet-stream" is used. This FileOption allows overriding the guessed content
// this to a specific type. For such situations this override method can // type with a specific one if required.
// be used //
// 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 { func WithFileContentType(contentType ContentType) FileOption {
return func(f *File) { return func(f *File) {
f.ContentType = contentType f.ContentType = contentType
} }
} }
// setHeader sets header fields to a 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) { func (f *File) setHeader(header Header, value string) {
f.Header.Set(string(header), value) f.Header.Set(string(header), value)
} }
// getHeader return header fields of a File // 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) { func (f *File) getHeader(header Header) (string, bool) {
v := f.Header.Get(string(header)) v := f.Header.Get(string(header))
return v, v != "" return v, v != ""

6
go.mod
View file

@ -6,4 +6,8 @@ module github.com/wneessen/go-mail
go 1.16 go 1.16
require go.mozilla.org/pkcs7 v0.9.0 require (
go.mozilla.org/pkcs7 v0.9.0
golang.org/x/crypto v0.28.0
golang.org/x/text v0.19.0
)

66
go.sum
View file

@ -1,2 +1,68 @@
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.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=
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=
go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI=
go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
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.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=
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.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=
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.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=
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=

3
go.sum.license Normal file
View file

@ -0,0 +1,3 @@
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
//
// SPDX-License-Identifier: MIT

133
header.go
View file

@ -4,129 +4,146 @@
package mail 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 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 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 type Importance int
// List of common generic header field names
const ( const (
// HeaderContentDescription is the "Content-Description" header // HeaderContentDescription is the "Content-Description" header.
HeaderContentDescription Header = "Content-Description" HeaderContentDescription Header = "Content-Description"
// HeaderContentDisposition is the "Content-Disposition" header // HeaderContentDisposition is the "Content-Disposition" header.
HeaderContentDisposition Header = "Content-Disposition" HeaderContentDisposition Header = "Content-Disposition"
// HeaderContentID is the "Content-ID" header // HeaderContentID is the "Content-ID" header.
HeaderContentID Header = "Content-ID" HeaderContentID Header = "Content-ID"
// HeaderContentLang is the "Content-Language" header // HeaderContentLang is the "Content-Language" header.
HeaderContentLang Header = "Content-Language" 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" HeaderContentLocation Header = "Content-Location"
// HeaderContentTransferEnc is the "Content-Transfer-Encoding" header // HeaderContentTransferEnc is the "Content-Transfer-Encoding" header.
HeaderContentTransferEnc Header = "Content-Transfer-Encoding" HeaderContentTransferEnc Header = "Content-Transfer-Encoding"
// HeaderContentType is the "Content-Type" header // HeaderContentType is the "Content-Type" header.
HeaderContentType Header = "Content-Type" HeaderContentType Header = "Content-Type"
// HeaderDate represents the "Date" field // HeaderDate represents the "Date" field.
// See: https://www.rfc-editor.org/rfc/rfc822#section-5.1 // https://datatracker.ietf.org/doc/html/rfc822#section-5.1
HeaderDate Header = "Date" HeaderDate Header = "Date"
// HeaderDispositionNotificationTo is the MDN header as described in RFC8098 // HeaderDispositionNotificationTo is the MDN header as described in RFC 8098.
// See: https://www.rfc-editor.org/rfc/rfc8098.html#section-2.1 // https://datatracker.ietf.org/doc/html/rfc8098#section-2.1
HeaderDispositionNotificationTo Header = "Disposition-Notification-To" HeaderDispositionNotificationTo Header = "Disposition-Notification-To"
// HeaderImportance represents the "Importance" field // HeaderImportance represents the "Importance" field.
HeaderImportance Header = "Importance" HeaderImportance Header = "Importance"
// HeaderInReplyTo represents the "In-Reply-To" field // HeaderInReplyTo represents the "In-Reply-To" field.
HeaderInReplyTo Header = "In-Reply-To" HeaderInReplyTo Header = "In-Reply-To"
// HeaderListUnsubscribe is the "List-Unsubscribe" header field // HeaderListUnsubscribe is the "List-Unsubscribe" header field.
HeaderListUnsubscribe Header = "List-Unsubscribe" 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" HeaderListUnsubscribePost Header = "List-Unsubscribe-Post"
// HeaderMessageID represents the "Message-ID" field for message identification // HeaderMessageID represents the "Message-ID" field for message identification.
// See: https://www.rfc-editor.org/rfc/rfc1036#section-2.1.5 // https://datatracker.ietf.org/doc/html/rfc1036#section-2.1.5
HeaderMessageID Header = "Message-ID" HeaderMessageID Header = "Message-ID"
// HeaderMIMEVersion represents the "MIME-Version" field as per RFC 2045 // HeaderMIMEVersion represents the "MIME-Version" field as per RFC 2045.
// See: https://datatracker.ietf.org/doc/html/rfc2045#section-4 // https://datatracker.ietf.org/doc/html/rfc2045#section-4
HeaderMIMEVersion Header = "MIME-Version" HeaderMIMEVersion Header = "MIME-Version"
// HeaderOrganization is the "Organization" header field // HeaderOrganization is the "Organization" header field.
HeaderOrganization Header = "Organization" HeaderOrganization Header = "Organization"
// HeaderPrecedence is the "Precedence" header field // HeaderPrecedence is the "Precedence" header field.
HeaderPrecedence Header = "Precedence" HeaderPrecedence Header = "Precedence"
// HeaderPriority represents the "Priority" field // HeaderPriority represents the "Priority" field.
HeaderPriority Header = "Priority" HeaderPriority Header = "Priority"
// HeaderReferences is the "References" header field // HeaderReferences is the "References" header field.
HeaderReferences Header = "References" HeaderReferences Header = "References"
// HeaderReplyTo is the "Reply-To" header field // HeaderReplyTo is the "Reply-To" header field.
HeaderReplyTo Header = "Reply-To" HeaderReplyTo Header = "Reply-To"
// HeaderSubject is the "Subject" header field // HeaderSubject is the "Subject" header field.
HeaderSubject Header = "Subject" HeaderSubject Header = "Subject"
// HeaderUserAgent is the "User-Agent" header field // HeaderUserAgent is the "User-Agent" header field.
HeaderUserAgent Header = "User-Agent" 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" HeaderXAutoResponseSuppress Header = "X-Auto-Response-Suppress"
// HeaderXMailer is the "X-Mailer" header field // HeaderXMailer is the "X-Mailer" header field.
HeaderXMailer Header = "X-Mailer" 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" HeaderXMSMailPriority Header = "X-MSMail-Priority"
// HeaderXPriority is the "X-Priority" header field // HeaderXPriority is the "X-Priority" header field.
HeaderXPriority Header = "X-Priority" HeaderXPriority Header = "X-Priority"
) )
// List of common address header field names
const ( const (
// HeaderBcc is the "Blind Carbon Copy" header field // HeaderBcc is the "Blind Carbon Copy" header field.
HeaderBcc AddrHeader = "Bcc" HeaderBcc AddrHeader = "Bcc"
// HeaderCc is the "Carbon Copy" header field // HeaderCc is the "Carbon Copy" header field.
HeaderCc AddrHeader = "Cc" HeaderCc AddrHeader = "Cc"
// HeaderEnvelopeFrom is the envelope FROM header field // HeaderEnvelopeFrom is the envelope FROM header field.
// It's not included in the mail body but only used by the Client for the envelope //
// 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" HeaderEnvelopeFrom AddrHeader = "EnvelopeFrom"
// HeaderFrom is the "From" header field // HeaderFrom is the "From" header field.
HeaderFrom AddrHeader = "From" HeaderFrom AddrHeader = "From"
// HeaderTo is the "Receipient" header field // HeaderTo is the "Receipient" header field.
HeaderTo AddrHeader = "To" HeaderTo AddrHeader = "To"
) )
// List of Importance values
const ( const (
// ImportanceLow indicates a low level of importance or priority in a Msg.
ImportanceLow Importance = iota ImportanceLow Importance = iota
// ImportanceNormal indicates a standard level of importance or priority for a Msg.
ImportanceNormal ImportanceNormal
// ImportanceHigh indicates a high level of importance or priority in a Msg.
ImportanceHigh ImportanceHigh
// ImportanceNonUrgent indicates a non-urgent level of importance or priority in a Msg.
ImportanceNonUrgent ImportanceNonUrgent
// ImportanceUrgent indicates an urgent level of importance or priority in a Msg.
ImportanceUrgent ImportanceUrgent
) )
// NumString returns the importance number string based on the Importance // 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 { func (i Importance) NumString() string {
switch i { switch i {
case ImportanceNonUrgent: case ImportanceNonUrgent:
@ -142,7 +159,14 @@ 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 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 { func (i Importance) XPrioString() string {
switch i { switch i {
case ImportanceNonUrgent: case ImportanceNonUrgent:
@ -158,7 +182,14 @@ 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.
//
// 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 { func (i Importance) String() string {
switch i { switch i {
case ImportanceNonUrgent: case ImportanceNonUrgent:
@ -174,12 +205,20 @@ 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.
//
// Returns:
// - A string representing the Header.
func (h Header) String() string { func (h Header) String() string {
return string(h) 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.
//
// Returns:
// - A string representing the AddrHeader.
func (a AddrHeader) String() string { func (a AddrHeader) String() string {
return string(a) return string(a)
} }

2094
msg.go

File diff suppressed because it is too large Load diff

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++ {
m := NewMsg() m := NewMsg()
for i := 0; i < 50_000; i++ {
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 {
@ -1911,6 +1924,22 @@ func TestMsg_hasAltWithSMime(t *testing.T) {
} }
} }
// TestMsg_hasSMime tests the hasSMime() method of the Msg
func TestMsg_hasSMime(t *testing.T) {
keyPair, err := getDummyCertificate()
if err != nil {
t.Errorf("failed to load dummy certificate. Cause: %v", err)
}
m := NewMsg()
if err := m.SignWithSMime(keyPair); err != nil {
t.Errorf("set of certificate was not successful")
}
m.SetBodyString(TypeTextPlain, "Plain")
if !m.hasSMime() {
t.Errorf("mail has smime configured but hasSMime() returned true")
}
}
// TestMsg_hasRelated tests the hasRelated() method of the Msg // TestMsg_hasRelated tests the hasRelated() method of the Msg
func TestMsg_hasRelated(t *testing.T) { func TestMsg_hasRelated(t *testing.T) {
m := NewMsg() m := NewMsg()
@ -1978,6 +2007,70 @@ func TestMsg_WriteToSkipMiddleware(t *testing.T) {
} }
} }
// TestMsg_WriteToWithSMIME tests the WriteTo() method of the Msg
func TestMsg_WriteToWithSMIME(t *testing.T) {
keyPair, err := getDummyCertificate()
if err != nil {
t.Errorf("failed to load dummy certificate. Cause: %v", err)
}
m := NewMsg()
m.Subject("This is a test")
m.SetBodyString(TypeTextPlain, "Plain")
if err := m.SignWithSMime(keyPair); err != nil {
t.Errorf("set of certificate was not successful")
}
wbuf := bytes.Buffer{}
if _, err = m.WriteTo(&wbuf); err != nil {
t.Errorf("WriteTo() failed: %s", err)
}
result := wbuf.String()
boundary := result[strings.LastIndex(result, "--")-60 : strings.LastIndex(result, "--")]
if strings.Count(result, boundary) != 4 {
t.Errorf("WriteTo() failed. False number of boundaries found")
}
parts := strings.Split(result, fmt.Sprintf("--%s", boundary))
if len(parts) != 4 {
t.Errorf("WriteTo() failed. False number of parts found")
}
preamble := parts[0]
if !strings.Contains(preamble, "MIME-Version: 1.0") {
t.Errorf("WriteTo() failed. Unable to find MIME-Version")
}
if !strings.Contains(preamble, "Subject: This is a test") {
t.Errorf("WriteTo() failed. Unable to find subject")
}
if !strings.Contains(preamble, fmt.Sprintf("Content-Type: multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=sha-256;\r\n boundary=%s", boundary)) {
t.Errorf("WriteTo() failed. Unable to find Content-Type")
}
signedData := parts[1]
if !strings.Contains(signedData, "Content-Transfer-Encoding: quoted-printable") {
t.Errorf("WriteTo() failed. Unable to find Content-Transfer-Encoding")
}
if !strings.Contains(signedData, "Content-Type: text/plain; charset=UTF-8") {
t.Errorf("WriteTo() failed. Unable to find Content-Type")
}
if !strings.Contains(signedData, "Plain") {
t.Errorf("WriteTo() failed. Unable to find Content")
}
signature := parts[2]
if !strings.Contains(signature, "Content-Transfer-Encoding: base64") {
t.Errorf("WriteTo() failed. Unable to find Content-Transfer-Encoding")
}
if !strings.Contains(signature, `application/pkcs7-signature; name="smime.p7s"`) {
t.Errorf("WriteTo() failed. Unable to find Content-Type")
}
if strings.Contains(signature, "Plain") {
t.Errorf("WriteTo() failed. Unable to find signature")
}
}
// TestMsg_WriteTo_fails tests the WriteTo() method of the Msg but with a failing body writer function // TestMsg_WriteTo_fails tests the WriteTo() method of the Msg but with a failing body writer function
func TestMsg_WriteTo_fails(t *testing.T) { func TestMsg_WriteTo_fails(t *testing.T) {
m := NewMsg() m := NewMsg()
@ -3266,9 +3359,6 @@ func TestSignWithSMime_ValidKeyPair(t *testing.T) {
if m.sMime.certificate == nil { if m.sMime.certificate == nil {
t.Errorf("WithSMimeSinging() - no certificate is given") t.Errorf("WithSMimeSinging() - no certificate is given")
} }
if len(m.sMime.parentCertificates) != len(keyPair.Certificate[:1]) {
t.Errorf("WithSMimeSinging() - no certificate is given")
}
} }
// TestSignWithSMime_InvalidKeyPair tests WithSMimeSinging with given invalid key pair // TestSignWithSMime_InvalidKeyPair tests WithSMimeSinging with given invalid key pair

View file

@ -12,8 +12,14 @@ import (
"os" "os"
) )
// WriteToTempFile will create a temporary file and output the Msg to this file // WriteToTempFile creates a temporary file and writes the Msg content to this file.
// The method will return the filename of the temporary 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) { func (m *Msg) WriteToTempFile() (string, error) {
f, err := os.CreateTemp("", "go-mail_*.eml") f, err := os.CreateTemp("", "go-mail_*.eml")
if err != nil { if err != nil {

View file

@ -12,8 +12,14 @@ import (
"io/ioutil" "io/ioutil"
) )
// WriteToTempFile will create a temporary file and output the Msg to this file // WriteToTempFile creates a temporary file and writes the Msg content to this file.
// The method will return the filename of the temporary 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) { func (m *Msg) WriteToTempFile() (string, error) {
f, err := ioutil.TempFile("", "go-mail_*.eml") f, err := ioutil.TempFile("", "go-mail_*.eml")
if err != nil { if err != nil {

View file

@ -18,22 +18,39 @@ import (
"strings" "strings"
) )
// MaxHeaderLength defines the maximum line length for a mail header const (
// RFC 2047 suggests 76 characters // MaxHeaderLength defines the maximum line length for a mail header.
const MaxHeaderLength = 76 //
// 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 // MaxBodyLength defines the maximum line length for the mail body.
// RFC 2047 suggests 76 characters //
const MaxBodyLength = 76 // 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 // SingleNewLine represents a single newline character sequence ("\r\n").
const SingleNewLine = "\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 // DoubleNewLine represents a double newline character sequence ("\r\n\r\n").
// indicate a new segement of the mail //
const DoubleNewLine = "\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 { type msgWriter struct {
bytesWritten int64 bytesWritten int64
charset Charset charset Charset
@ -45,7 +62,18 @@ type msgWriter struct {
writer io.Writer 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) { func (mw *msgWriter) Write(payload []byte) (int, error) {
if mw.err != nil { if mw.err != nil {
return 0, fmt.Errorf("failed to write due to previous error: %w", mw.err) 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 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) { func (mw *msgWriter) writeMsg(msg *Msg) {
msg.addDefaultHeader() msg.addDefaultHeader()
msg.checkUserAgent() msg.checkUserAgent()
@ -144,7 +184,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) { func (mw *msgWriter) writeGenHeader(msg *Msg) {
keys := make([]string, 0, len(msg.genHeader)) keys := make([]string, 0, len(msg.genHeader))
for key := range msg.genHeader { for key := range msg.genHeader {
@ -156,14 +202,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) { func (mw *msgWriter) writePreformattedGenHeader(msg *Msg) {
for key, val := range msg.preformHeader { for key, val := range msg.preformHeader {
mw.writeString(fmt.Sprintf("%s: %s%s", key, val, SingleNewLine)) 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) { func (mw *msgWriter) startMP(mimeType MIMEType, boundary string) {
multiPartWriter := multipart.NewWriter(mw) multiPartWriter := multipart.NewWriter(mw)
if boundary != "" { if boundary != "" {
@ -183,7 +247,10 @@ func (mw *msgWriter) startMP(mimeType MIMEType, boundary string) {
mw.depth++ 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() { func (mw *msgWriter) stopMP() {
if mw.depth > 0 { if mw.depth > 0 {
mw.err = mw.multiPartWriter[mw.depth-1].Close() mw.err = mw.multiPartWriter[mw.depth-1].Close()
@ -191,7 +258,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) { func (mw *msgWriter) addFiles(files []*File, isAttachment bool) {
for _, file := range files { for _, file := range files {
encoding := EncodingB64 encoding := EncodingB64
@ -250,18 +327,40 @@ 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) { func (mw *msgWriter) newPart(header map[string][]string) {
mw.partWriter, mw.err = mw.multiPartWriter[mw.depth-1].CreatePart(header) 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) { func (mw *msgWriter) writePart(part *Part, charset Charset) {
partCharset := part.charset partCharset := part.charset
if partCharset.String() == "" { if partCharset.String() == "" {
partCharset = charset partCharset = charset
} }
contentType := fmt.Sprintf("%s; charset=%s", part.contentType, partCharset)
contentType := part.contentType.String()
if !part.IsSMimeSigned() {
contentType = strings.Join([]string{contentType, "; charset=", partCharset.String()}, "")
}
contentTransferEnc := part.encoding.String() contentTransferEnc := part.encoding.String()
if mw.depth == 0 { if mw.depth == 0 {
mw.writeHeader(HeaderContentType, contentType) mw.writeHeader(HeaderContentType, contentType)
@ -280,7 +379,14 @@ func (mw *msgWriter) writePart(part *Part, charset Charset) {
mw.writeBody(part.writeFunc, part.encoding, part.smime) mw.writeBody(part.writeFunc, part.encoding, part.smime)
} }
// 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) { func (mw *msgWriter) writeString(s string) {
if mw.err != nil { if mw.err != nil {
return return
@ -290,7 +396,16 @@ func (mw *msgWriter) writeString(s string) {
mw.bytesWritten += int64(n) 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) { func (mw *msgWriter) writeHeader(key Header, values ...string) {
buffer := strings.Builder{} buffer := strings.Builder{}
charLength := MaxHeaderLength - 2 charLength := MaxHeaderLength - 2
@ -325,7 +440,18 @@ func (mw *msgWriter) writeHeader(key Header, values ...string) {
mw.writeString("\r\n") 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).
// - singingWithSMime: Whether the msg should be signed with S/MIME or not.
func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encoding Encoding, singingWithSMime bool) { func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encoding Encoding, singingWithSMime bool) {
var writer io.Writer var writer io.Writer
var encodedWriter io.WriteCloser var encodedWriter io.WriteCloser

View file

@ -161,6 +161,7 @@ func TestMsgWriter_writeMsg_SMime(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("failed to load dummy certificate. Cause: %v", err) t.Errorf("failed to load dummy certificate. Cause: %v", err)
} }
m := NewMsg() m := NewMsg()
if err := m.SignWithSMime(keyPair); err != nil { if err := m.SignWithSMime(keyPair); err != nil {
t.Errorf("set of certificate was not successful") t.Errorf("set of certificate was not successful")
@ -173,7 +174,22 @@ func TestMsgWriter_writeMsg_SMime(t *testing.T) {
mw := &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding} mw := &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding}
mw.writeMsg(m) mw.writeMsg(m)
ms := buf.String() ms := buf.String()
if !strings.Contains(ms, `multipart/signed; protocol="application/pkcs7-signature"; micalg=sha256;`) {
t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output") if !strings.Contains(ms, "MIME-Version: 1.0") {
t.Errorf("writeMsg failed. Unable to find MIME-Version")
}
if !strings.Contains(ms, "Subject: This is a subject") {
t.Errorf("writeMsg failed. Unable to find subject")
}
if !strings.Contains(ms, "From: \"Toni Tester\" <test@example.com>") {
t.Errorf("writeMsg failed. Unable to find transmitter")
}
if !strings.Contains(ms, "To: \"Toni Receiver\" <receiver@example.com>") {
t.Errorf("writeMsg failed. Unable to find receiver")
}
boundary := ms[strings.LastIndex(ms, "--")-60 : strings.LastIndex(ms, "--")]
if !strings.Contains(ms, fmt.Sprintf("Content-Type: multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=sha-256;\r\n boundary=%s", boundary)) {
t.Errorf("writeMsg failed. Unable to find Content-Type")
} }
} }

137
part.go
View file

@ -12,7 +12,11 @@ import (
// PartOption returns a function that can be used for grouping Part options // PartOption returns a function that can be used for grouping Part options
type PartOption func(*Part) 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 { type Part struct {
contentType ContentType contentType ContentType
charset Charset charset Charset
@ -23,7 +27,14 @@ type Part struct {
smime bool smime bool
} }
// 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) { func (p *Part) GetContent() ([]byte, error) {
var b bytes.Buffer var b bytes.Buffer
if _, err := p.writeFunc(&b); err != nil { if _, err := p.writeFunc(&b); err != nil {
@ -32,27 +43,54 @@ func (p *Part) GetContent() ([]byte, error) {
return b.Bytes(), nil 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 { func (p *Part) GetCharset() Charset {
return p.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 { func (p *Part) GetContentType() ContentType {
return p.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 { func (p *Part) GetEncoding() Encoding {
return p.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) { func (p *Part) GetWriteFunc() func(io.Writer) (int64, error) {
return p.writeFunc 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 { func (p *Part) GetDescription() string {
return p.description return p.description
} }
@ -62,63 +100,126 @@ func (p *Part) IsSMimeSigned() bool {
return p.smime return p.smime
} }
// 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) { func (p *Part) SetContent(content string) {
buffer := bytes.NewBufferString(content) buffer := bytes.NewBufferString(content)
p.writeFunc = writeFuncFromBuffer(buffer) 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) { func (p *Part) SetContentType(contentType ContentType) {
p.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) { func (p *Part) SetCharset(charset Charset) {
p.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) { func (p *Part) SetEncoding(encoding Encoding) {
p.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) { func (p *Part) SetDescription(description string) {
p.description = description p.description = description
} }
// SetIsSMimeSigned sets the flag for signing the Part with S/MIME // SetIsSMimeSigned sets the flag for signing the Part with S/MIME
func (p *Part) SetIsSMimeSigned(smime bool) { func (p *Part) SetIsSMimeSigned(smime bool) {
p.smime = smime p.smime = smime
} }
// 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)) { func (p *Part) SetWriteFunc(writeFunc func(io.Writer) (int64, error)) {
p.writeFunc = writeFunc p.writeFunc = writeFunc
} }
// Delete removes the current part from the parts list of the Msg by setting the // Delete removes the current part from the parts list of the Msg by setting the isDeleted flag to true.
// isDeleted flag to true. The msgWriter will skip it then //
// 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() { func (p *Part) Delete() {
p.isDeleted = true 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 { func WithPartCharset(charset Charset) PartOption {
return func(p *Part) { return func(p *Part) {
p.charset = charset 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 { func WithPartEncoding(encoding Encoding) PartOption {
return func(p *Part) { return func(p *Part) {
p.encoding = encoding 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 { func WithPartContentDescription(description string) PartOption {
return func(p *Part) { return func(p *Part) {
p.description = description p.description = description

View file

@ -7,23 +7,41 @@ package mail
import ( import (
"crypto/rand" "crypto/rand"
"encoding/binary" "encoding/binary"
"fmt"
"math/big"
"strings" "strings"
) )
// Range of characters for the secure string generation // 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) // 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 66 possible characters, and the constants help determine the
// number of bits and indices used in the process.
const ( const (
letterIdxBits = 7 // 7 bits to represent a letter index // letterIdxBits: Number of bits needed to represent a letter index. We have 64 possible characters
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits // which fit into 6 bits.
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits letterIdxBits = 6
// letterIdxMask: Bitmask to extract letter indices (all 1-bits for letterIdxBits).
letterIdxMask = 1<<letterIdxBits - 1
// letterIdxMax: The maximum number of letter indices that fit in 63 bits.
letterIdxMax = 63 / letterIdxBits
) )
// randomStringSecure returns a random, string of length characters. This method uses the // randomStringSecure returns a random string of the specified length.
// crypto/random package and therfore is cryptographically secure //
// This function generates a cryptographically secure random string of the given length using
// the crypto/rand package. It ensures that the randomness is secure and suitable for
// cryptographic purposes. The function reads random bytes, converts them to indices within
// a character range, and builds the string. If an error occurs while reading from the random
// pool, it returns the error.
//
// Parameters:
// - length: The length of the random string to be generated.
//
// Returns:
// - A randomly generated string.
// - An error if the random generation fails.
func randomStringSecure(length int) (string, error) { func randomStringSecure(length int) (string, error) {
randString := strings.Builder{} randString := strings.Builder{}
randString.Grow(length) randString.Grow(length)
@ -52,23 +70,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
}

View file

@ -38,33 +38,12 @@ func TestRandomStringSecure(t *testing.T) {
} }
} }
// TestRandomNum tests the randomNum method func BenchmarkGenerator_RandomStringSecure(b *testing.B) {
func TestRandomNum(t *testing.T) { b.ReportAllocs()
tt := []struct { for i := 0; i < b.N; i++ {
testName string _, err := randomStringSecure(22)
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, err := randNum(tc.max)
if err != nil { if err != nil {
t.Errorf("random number generation failed: %s", err) b.Errorf("RandomStringFromCharRange() failed: %s", err)
} }
if rn < 0 {
t.Errorf("random number generation failed: %d is smaller than zero", rn)
}
if rn > tc.max {
t.Errorf("random number generation failed: %d is bigger than given value %d", rn, tc.max)
}
})
} }
} }

View file

@ -8,19 +8,41 @@ import (
"io" "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 { type Reader struct {
buffer []byte // contents are the bytes buffer[offset : len(buffer)] buffer []byte // contents are the bytes buffer[offset : len(buffer)]
offset int // read at &buffer[offset], write at &buffer[len(buffer)] offset int // read at &buffer[offset], write at &buffer[len(buffer)]
err error // initialization error 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 { func (r *Reader) Error() error {
return r.err 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) { func (r *Reader) Read(payload []byte) (n int, err error) {
if r.err != nil { if r.err != nil {
return 0, r.err return 0, r.err
@ -37,12 +59,20 @@ func (r *Reader) Read(payload []byte) (n int, err error) {
return n, err return n, err
} }
// Reset resets the Reader buffer to be empty, but it retains the underlying storage // Reset resets the Reader buffer to be empty, but it retains the underlying storage for future use.
// for use by future writes. //
// 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() { func (r *Reader) Reset() {
r.buffer = r.buffer[:0] r.buffer = r.buffer[:0]
r.offset = 0 r.offset = 0
} }
// empty reports whether the unread portion of the Reader buffer is empty. // 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 } func (r *Reader) empty() bool { return len(r.buffer) <= r.offset }

View file

@ -54,18 +54,32 @@ const (
ErrAmbiguous 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 { 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
type SendErrReason int 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 { func (e *SendError) Error() string {
if e.Reason > 10 { if e.Reason > 10 {
return "unknown reason" return "unknown reason"
@ -92,10 +106,25 @@ 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()
} }
// 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 { func (e *SendError) Is(errType error) bool {
var t *SendError var t *SendError
if errors.As(errType, &t) && t != nil { if errors.As(errType, &t) && t != nil {
@ -104,7 +133,13 @@ func (e *SendError) Is(errType error) bool {
return false 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 { func (e *SendError) IsTemp() bool {
if e == nil { if e == nil {
return false return false
@ -112,7 +147,42 @@ func (e *SendError) IsTemp() bool {
return e.isTemp return e.isTemp
} }
// String implements the Stringer interface for the SendErrReason // 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 ""
}
return e.affectedMsg.GetMessageID()
}
// 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
}
return e.affectedMsg
}
// 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 { func (r SendErrReason) String() string {
switch r { switch r {
case ErrGetSender: case ErrGetSender:
@ -141,8 +211,16 @@ func (r SendErrReason) String() string {
return "unknown reason" return "unknown reason"
} }
// isTempError checks the given SMTP error and returns true if the given error is of temporary nature // isTempError checks if the given SMTP error is of a temporary nature and should be retried.
// 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 { func isTempError(err error) bool {
return err.Error()[0] == '4' return err.Error()[0] == '4'
} }

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

View file

@ -16,9 +16,6 @@ var (
// ErrInvalidKeyPair should be used if key pair is invalid // ErrInvalidKeyPair should be used if key pair is invalid
ErrInvalidKeyPair = errors.New("invalid key pair") ErrInvalidKeyPair = errors.New("invalid key pair")
// ErrInvalidCertificate should be used if a certificate is invalid
ErrInvalidCertificate = errors.New("invalid certificate")
// ErrCouldNotInitialize should be used if the signed data could not initialize // ErrCouldNotInitialize should be used if the signed data could not initialize
ErrCouldNotInitialize = errors.New("could not initialize signed data") ErrCouldNotInitialize = errors.New("could not initialize signed data")
@ -36,7 +33,6 @@ var (
type SMime struct { type SMime struct {
privateKey *rsa.PrivateKey privateKey *rsa.PrivateKey
certificate *x509.Certificate certificate *x509.Certificate
parentCertificates []*x509.Certificate
} }
// NewSMime construct a new instance of SMime with a provided *tls.Certificate // NewSMime construct a new instance of SMime with a provided *tls.Certificate
@ -45,53 +41,44 @@ func newSMime(keyPair *tls.Certificate) (*SMime, error) {
return nil, ErrInvalidKeyPair return nil, ErrInvalidKeyPair
} }
parentCertificates := make([]*x509.Certificate, 0)
for _, cert := range keyPair.Certificate[1:] {
c, err := x509.ParseCertificate(cert)
if err != nil {
return nil, ErrInvalidCertificate
}
parentCertificates = append(parentCertificates, c)
}
return &SMime{ return &SMime{
privateKey: keyPair.PrivateKey.(*rsa.PrivateKey), privateKey: keyPair.PrivateKey.(*rsa.PrivateKey),
certificate: keyPair.Leaf, certificate: keyPair.Leaf,
parentCertificates: parentCertificates,
}, nil }, nil
} }
// sign with the S/MIME method the message of the actual *Part // signMessage signs the message with S/MIME
func (sm *SMime) sign(signaturePart *Part, message string) error { func (sm *SMime) signMessage(message string) (*string, error) {
lines := parseLines([]byte(message)) lines := parseLines([]byte(message))
toBeSigned := lines.bytesFromLines([]byte("\r\n")) toBeSigned := lines.bytesFromLines([]byte("\r\n"))
tmp, err := pkcs7.NewSignedData(toBeSigned) signedData, err := pkcs7.NewSignedData(toBeSigned)
tmp.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256) signedData.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256)
if err != nil { if err != nil {
return ErrCouldNotInitialize return nil, ErrCouldNotInitialize
} }
if err = tmp.AddSignerChain(sm.certificate, sm.privateKey, sm.parentCertificates, pkcs7.SignerInfoConfig{}); err != nil { if err = signedData.AddSigner(sm.certificate, sm.privateKey, pkcs7.SignerInfoConfig{}); err != nil {
return ErrCouldNotAddSigner return nil, ErrCouldNotAddSigner
} }
signatureDER, err := tmp.Finish() signedData.Detach()
signatureDER, err := signedData.Finish()
if err != nil { if err != nil {
return ErrCouldNotFinishSigning return nil, ErrCouldNotFinishSigning
} }
pemMsg, err := encodeToPEM(signatureDER) pemMsg, err := encodeToPEM(signatureDER)
if err != nil { if err != nil {
return ErrCouldNoEncodeToPEM return nil, ErrCouldNoEncodeToPEM
} }
signaturePart.SetContent(*pemMsg)
return nil return pemMsg, nil
} }
// createMessage prepares the message that will be used for the sign method later // createMessage prepares the message that will be used for the sign method later
func (sm *SMime) createMessage(encoding Encoding, contentType ContentType, charset Charset, body []byte) string { func (sm *SMime) prepareMessage(encoding Encoding, contentType ContentType, charset Charset, body []byte) string {
return fmt.Sprintf("Content-Transfer-Encoding: %v\r\nContent-Type: %v; charset=%v\r\n\r\n%v", encoding, contentType, charset, string(body)) return fmt.Sprintf("Content-Transfer-Encoding: %v\r\nContent-Type: %v; charset=%v\r\n\r\n%v", encoding, contentType, charset, string(body))
} }

190
smime_test.go Normal file
View file

@ -0,0 +1,190 @@
package mail
import (
"bytes"
"encoding/base64"
"fmt"
"strings"
"testing"
)
// TestNewSMime tests the newSMime method
func TestNewSMime(t *testing.T) {
keyPair, err := getDummyCertificate()
if err != nil {
t.Errorf("Error getting dummy certificate: %s", err)
}
sMime, err := newSMime(keyPair)
if err != nil {
t.Errorf("Error creating new SMime from keyPair: %s", err)
}
if sMime.privateKey != keyPair.PrivateKey {
t.Errorf("NewSMime() did not return the same private key")
}
if sMime.certificate != keyPair.Leaf {
t.Errorf("NewSMime() did not return the same leaf certificate")
}
}
// TestSign tests the sign method
func TestSign(t *testing.T) {
keyPair, err := getDummyCertificate()
if err != nil {
t.Errorf("Error getting dummy certificate: %s", err)
}
sMime, err := newSMime(keyPair)
if err != nil {
t.Errorf("Error creating new SMime from keyPair: %s", err)
}
message := "This is a test message"
singedMessage, err := sMime.signMessage(message)
if err != nil {
t.Errorf("Error creating singed message: %s", err)
}
if *singedMessage == message {
t.Errorf("Sign() did not work")
}
}
// TestPrepareMessage tests the createMessage method
func TestPrepareMessage(t *testing.T) {
keyPair, err := getDummyCertificate()
if err != nil {
t.Errorf("Error getting dummy certificate: %s", err)
}
sMime, err := newSMime(keyPair)
if err != nil {
t.Errorf("Error creating new SMime from keyPair: %s", err)
}
encoding := EncodingB64
contentType := TypeTextPlain
charset := CharsetUTF8
body := []byte("This is the body!")
result := sMime.prepareMessage(encoding, contentType, charset, body)
if !strings.Contains(result, encoding.String()) {
t.Errorf("createMessage() did not return the correct encoding")
}
if !strings.Contains(result, contentType.String()) {
t.Errorf("createMessage() did not return the correct contentType")
}
if !strings.Contains(result, string(body)) {
t.Errorf("createMessage() did not return the correct body")
}
if result != fmt.Sprintf("Content-Transfer-Encoding: %s\r\nContent-Type: %s; charset=%s\r\n\r\n%s", encoding, contentType, charset, string(body)) {
t.Errorf("createMessage() did not sucessfully create the message")
}
}
// TestEncodeToPEM tests the encodeToPEM method
func TestEncodeToPEM(t *testing.T) {
message := []byte("This is a test message")
pemMessage, err := encodeToPEM(message)
if err != nil {
t.Errorf("Error encoding message: %s", err)
}
base64Encoded := base64.StdEncoding.EncodeToString(message)
if *pemMessage != base64Encoded {
t.Errorf("encodeToPEM() did not work")
}
}
// TestBytesFromLines tests the bytesFromLines method
func TestBytesFromLines(t *testing.T) {
ls := lines{
{line: []byte("Hello"), endOfLine: []byte("\n")},
{line: []byte("World"), endOfLine: []byte("\n")},
}
expected := []byte("Hello\nWorld\n")
result := ls.bytesFromLines([]byte("\n"))
if !bytes.Equal(result, expected) {
t.Errorf("Expected %s, but got %s", expected, result)
}
}
// FuzzBytesFromLines tests the bytesFromLines method with fuzzing
func FuzzBytesFromLines(f *testing.F) {
f.Add([]byte("Hello"), []byte("\n"))
f.Fuzz(func(t *testing.T, lineData, sep []byte) {
ls := lines{
{line: lineData, endOfLine: sep},
}
_ = ls.bytesFromLines(sep)
})
}
// TestParseLines tests the parseLines method
func TestParseLines(t *testing.T) {
input := []byte("Hello\r\nWorld\nHello\rWorld")
expected := lines{
{line: []byte("Hello"), endOfLine: []byte("\r\n")},
{line: []byte("World"), endOfLine: []byte("\n")},
{line: []byte("Hello"), endOfLine: []byte("\r")},
{line: []byte("World"), endOfLine: []byte("")},
}
result := parseLines(input)
if len(result) != len(expected) {
t.Errorf("Expected %d lines, but got %d", len(expected), len(result))
}
for i := range result {
if !bytes.Equal(result[i].line, expected[i].line) || !bytes.Equal(result[i].endOfLine, expected[i].endOfLine) {
t.Errorf("Line %d mismatch. Expected line: %s, endOfLine: %s, got line: %s, endOfLine: %s",
i, expected[i].line, expected[i].endOfLine, result[i].line, result[i].endOfLine)
}
}
}
// FuzzParseLines tests the parseLines method with fuzzing
func FuzzParseLines(f *testing.F) {
f.Add([]byte("Hello\nWorld\r\nAnother\rLine"))
f.Fuzz(func(t *testing.T, input []byte) {
_ = parseLines(input)
})
}
// TestSplitLine tests the splitLine method
func TestSplitLine(t *testing.T) {
ls := lines{
{line: []byte("Hello\r\nWorld\r\nAnotherLine"), endOfLine: []byte("")},
}
expected := lines{
{line: []byte("Hello"), endOfLine: []byte("\r\n")},
{line: []byte("World"), endOfLine: []byte("\r\n")},
{line: []byte("AnotherLine"), endOfLine: []byte("")},
}
result := ls.splitLine([]byte("\r\n"))
if len(result) != len(expected) {
t.Errorf("Expected %d lines, but got %d", len(expected), len(result))
}
for i := range result {
if !bytes.Equal(result[i].line, expected[i].line) || !bytes.Equal(result[i].endOfLine, expected[i].endOfLine) {
t.Errorf("Line %d mismatch. Expected line: %s, endOfLine: %s, got line: %s, endOfLine: %s",
i, expected[i].line, expected[i].endOfLine, result[i].line, result[i].endOfLine)
}
}
}
// FuzzSplitLine tests the parseLsplitLineines method with fuzzing
func FuzzSplitLine(f *testing.F) {
f.Add([]byte("Hello\r\nWorld"), []byte("\r\n"))
f.Fuzz(func(t *testing.T, input, sep []byte) {
ls := lines{
{line: input, endOfLine: []byte("")},
}
_ = ls.splitLine(sep)
})
}

View file

@ -13,6 +13,19 @@
package smtp 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. // Auth is implemented by an SMTP authentication mechanism.
type Auth interface { type Auth interface {
// Start begins an authentication with a server. // Start begins an authentication with a server.

View file

@ -1,11 +1,10 @@
// SPDX-FileCopyrightText: Copyright (c) 2022-2023 The go-mail Authors // SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package smtp package smtp
import ( import (
"errors"
"fmt" "fmt"
) )
@ -13,53 +12,35 @@ import (
type loginAuth struct { type loginAuth struct {
username, password string username, password string
host 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 // LoginAuth returns an [Auth] that implements the LOGIN authentication
// mechanism as it is used by MS Outlook. The Auth works similar to PLAIN // 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 // but instead of sending all in one response, the login is handled within
// 3 steps: // 3 steps:
// - Sending AUTH LOGIN (server responds with "Username:") // - Sending AUTH LOGIN (server might responds with "Username:")
// - Sending the username (server responds with "Password:") // - Sending the username (server might responds with "Password:")
// - Sending the password (server authenticates) // - 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 // LoginAuth will only send the credentials if the connection is using TLS
// or is connected to localhost. Otherwise authentication will fail with an // or is connected to localhost. Otherwise authentication will fail with an
// error, without sending the credentials. // error, without sending the credentials.
func LoginAuth(username, password, host string) Auth { func LoginAuth(username, password, host string) 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) { func (a *loginAuth) Start(server *ServerInfo) (string, []byte, error) {
// Must have TLS, or else localhost server. // Must have TLS, or else localhost server.
// Note: If TLS is not true, then we can't trust ANYTHING in ServerInfo. // Note: If TLS is not true, then we can't trust ANYTHING in ServerInfo.
@ -67,23 +48,28 @@ func (a *loginAuth) Start(server *ServerInfo) (string, []byte, error) {
// That might just be the attacker saying // That might just be the attacker saying
// "it's ok, you can trust me with your password." // "it's ok, you can trust me with your password."
if !server.TLS && !isLocalhost(server.Name) { if !server.TLS && !isLocalhost(server.Name) {
return "", nil, errors.New("unencrypted connection") return "", nil, ErrUnencrypted
} }
if server.Name != a.host { if server.Name != a.host {
return "", nil, errors.New("wrong host name") return "", nil, ErrWrongHostname
} }
a.respStep = 0
return "LOGIN", nil, nil 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) { func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if more { if more {
switch string(fromServer) { switch a.respStep {
case LoginXUsernameChallenge, LoginXUsernameLowerChallenge, LoginXDraftUsernameChallenge: case 0:
a.respStep++
return []byte(a.username), nil return []byte(a.username), nil
case LoginXPasswordChallenge, LoginXPasswordLowerChallenge, LoginXDraftPasswordChallenge: case 1:
a.respStep++
return []byte(a.password), nil return []byte(a.password), nil
default: default:
return nil, fmt.Errorf("unexpected server response: %s", string(fromServer)) return nil, fmt.Errorf("%w: %s", ErrUnexpectedServerResponse, string(fromServer))
} }
} }
return nil, nil return nil, nil

View file

@ -13,10 +13,6 @@
package smtp package smtp
import (
"errors"
)
// plainAuth is the type that satisfies the Auth interface for the "SMTP PLAIN" auth // plainAuth is the type that satisfies the Auth interface for the "SMTP PLAIN" auth
type plainAuth struct { type plainAuth struct {
identity, username, password string identity, username, password string
@ -42,10 +38,10 @@ func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
// That might just be the attacker saying // That might just be the attacker saying
// "it's ok, you can trust me with your password." // "it's ok, you can trust me with your password."
if !server.TLS && !isLocalhost(server.Name) { if !server.TLS && !isLocalhost(server.Name) {
return "", nil, errors.New("unencrypted connection") return "", nil, ErrUnencrypted
} }
if server.Name != a.host { 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) resp := []byte(a.identity + "\x00" + a.username + "\x00" + a.password)
return "PLAIN", resp, nil 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) { func (a *plainAuth) Next(_ []byte, more bool) ([]byte, error) {
if more { if more {
// We've already sent everything. // We've already sent everything.
return nil, errors.New("unexpected server challenge") return nil, ErrUnexpectedServerChallange
} }
return nil, nil return nil, nil
} }

317
smtp/auth_scram.go Normal file
View file

@ -0,0 +1,317 @@
// SPDX-FileCopyrightText: Copyright (c) 2024 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"
)
// 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
iterations int
h func() hash.Hash
isPlus bool
tlsConnState *tls.ConnectionState
bindData []byte
}
// 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,
password: password,
algorithm: "SCRAM-SHA-1",
h: sha1.New,
}
}
// 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,
password: password,
algorithm: "SCRAM-SHA-1-PLUS",
h: sha1.New,
isPlus: true,
tlsConnState: tlsConnState,
}
}
// 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) {
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 {
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, fmt.Errorf("%w: %s", ErrUnexpectedServerResponse, string(fromServer))
}
}
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
a.saltedPwd = nil
a.authMessage = nil
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 {
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))
// 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
// 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 {
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
}
// 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 {
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))
// 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))
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()
if !hmac.Equal(serverSignature, computedServerSignature) {
return nil, errors.New("invalid server signature")
}
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)
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
}
// 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)
buf := make([]byte, base64.StdEncoding.EncodedLen(len(serverSignature)))
base64.StdEncoding.Encode(buf, serverSignature)
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.
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
}
// 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 "", fmt.Errorf("failled to normalize string: %w", err)
}
if s == "" {
return "", errors.New("normalized string is empty")
}
return s, nil
}

View file

@ -30,34 +30,70 @@ import (
"net/textproto" "net/textproto"
"os" "os"
"strings" "strings"
"sync"
"time"
"github.com/wneessen/go-mail/log" "github.com/wneessen/go-mail/log"
) )
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. // A Client represents a client connection to an SMTP server.
type Client struct { type Client struct {
// Text is the textproto.Conn used by the Client. It is exported to allow for // Text is the textproto.Conn used by the Client. It is exported to allow for clients to add extensions.
// clients to add extensions.
Text *textproto.Conn Text *textproto.Conn
// keep a reference to the connection so it can be used to create a TLS
// connection later // auth supported auth mechanisms
conn net.Conn
// whether the Client is using TLS
tls bool
serverName string
// map of supported extensions
ext map[string]string
// supported auth mechanisms
auth []string auth []string
// keep a reference to the connection so it can be used to create a TLS connection later
conn net.Conn
// 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
// 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 localName string // the name to use in HELO/EHLO
didHello bool // whether we've said HELO/EHLO
helloError error // the error from the hello // logger will be used for debug logging
// debug logging logger log.Logger
debug bool // debug logging is enabled
logger log.Logger // logger will be used for debug logging // mutex is used to synchronize access to shared resources, ensuring that only one goroutine can access
// DSN support // the resource at a time.
dsnmrtype string // dsnmrtype defines the mail return option in case DSN is enabled mutex sync.RWMutex
dsnrntype string // dsnrntype defines the recipient notify option in case DSN is enabled
// 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. // Dial returns a new [Client] connected to an SMTP server at addr.
@ -88,13 +124,18 @@ func NewClient(conn net.Conn, host string) (*Client, error) {
} }
c := &Client{Text: text, conn: conn, serverName: host, localName: "localhost"} c := &Client{Text: text, conn: conn, serverName: host, localName: "localhost"}
_, c.tls = conn.(*tls.Conn) _, c.tls = conn.(*tls.Conn)
c.isConnected = true
return c, nil return c, nil
} }
// Close closes the connection. // Close closes the connection.
func (c *Client) Close() error { func (c *Client) Close() error {
return c.Text.Close() c.mutex.Lock()
err := c.Text.Close()
c.isConnected = false
c.mutex.Unlock()
return err
} }
// hello runs a hello exchange if needed. // hello runs a hello exchange if needed.
@ -121,28 +162,39 @@ func (c *Client) Hello(localName string) error {
if c.didHello { if c.didHello {
return errors.New("smtp: Hello called after other methods") return errors.New("smtp: Hello called after other methods")
} }
c.mutex.Lock()
c.localName = localName c.localName = localName
c.mutex.Unlock()
return c.hello() return c.hello()
} }
// cmd is a convenience function that sends a command and returns the response // 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) { func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
c.mutex.Lock()
c.debugLog(log.DirClientToServer, format, args...) c.debugLog(log.DirClientToServer, format, args...)
id, err := c.Text.Cmd(format, args...) id, err := c.Text.Cmd(format, args...)
if err != nil { if err != nil {
c.mutex.Unlock()
return 0, "", err return 0, "", err
} }
c.Text.StartResponse(id) c.Text.StartResponse(id)
defer c.Text.EndResponse(id)
code, msg, err := c.Text.ReadResponse(expectCode) code, msg, err := c.Text.ReadResponse(expectCode)
c.debugLog(log.DirServerToClient, "%d %s", code, msg) c.debugLog(log.DirServerToClient, "%d %s", code, msg)
c.Text.EndResponse(id)
c.mutex.Unlock()
return code, msg, err return code, msg, err
} }
// helo sends the HELO greeting to the server. It should be used only when the // helo sends the HELO greeting to the server. It should be used only when the
// server does not support ehlo. // server does not support ehlo.
func (c *Client) helo() error { func (c *Client) helo() error {
c.mutex.Lock()
c.ext = nil c.ext = nil
c.mutex.Unlock()
_, _, err := c.cmd(250, "HELO %s", c.localName) _, _, err := c.cmd(250, "HELO %s", c.localName)
return err return err
} }
@ -157,9 +209,13 @@ func (c *Client) StartTLS(config *tls.Config) error {
if err != nil { if err != nil {
return err return err
} }
c.mutex.Lock()
c.conn = tls.Client(c.conn, config) c.conn = tls.Client(c.conn, config)
c.Text = textproto.NewConn(c.conn) c.Text = textproto.NewConn(c.conn)
c.tls = true c.tls = true
c.mutex.Unlock()
return c.ehlo() return c.ehlo()
} }
@ -167,11 +223,15 @@ func (c *Client) StartTLS(config *tls.Config) error {
// The return values are their zero values if [Client.StartTLS] did // The return values are their zero values if [Client.StartTLS] did
// not succeed. // not succeed.
func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) { func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) {
c.mutex.RLock()
defer c.mutex.RUnlock()
tc, ok := c.conn.(*tls.Conn) tc, ok := c.conn.(*tls.Conn)
if !ok { if !ok {
return return
} }
return tc.ConnectionState(), true state, ok = tc.ConnectionState(), true
return
} }
// Verify checks the validity of an email address on the server. // Verify checks the validity of an email address on the server.
@ -257,6 +317,8 @@ func (c *Client) Mail(from string) error {
return err return err
} }
cmdStr := "MAIL FROM:<%s>" cmdStr := "MAIL FROM:<%s>"
c.mutex.RLock()
if c.ext != nil { if c.ext != nil {
if _, ok := c.ext["8BITMIME"]; ok { if _, ok := c.ext["8BITMIME"]; ok {
cmdStr += " BODY=8BITMIME" cmdStr += " BODY=8BITMIME"
@ -269,6 +331,8 @@ func (c *Client) Mail(from string) error {
cmdStr += fmt.Sprintf(" RET=%s", c.dsnmrtype) cmdStr += fmt.Sprintf(" RET=%s", c.dsnmrtype)
} }
} }
c.mutex.RUnlock()
_, _, err := c.cmd(250, cmdStr, from) _, _, err := c.cmd(250, cmdStr, from)
return err return err
} }
@ -280,7 +344,11 @@ func (c *Client) Rcpt(to string) error {
if err := validateLine(to); err != nil { if err := validateLine(to); err != nil {
return err return err
} }
c.mutex.RLock()
_, ok := c.ext["DSN"] _, ok := c.ext["DSN"]
c.mutex.RUnlock()
if ok && c.dsnrntype != "" { if ok && c.dsnrntype != "" {
_, _, err := c.cmd(25, "RCPT TO:<%s> NOTIFY=%s", to, c.dsnrntype) _, _, err := c.cmd(25, "RCPT TO:<%s> NOTIFY=%s", to, c.dsnrntype)
return err return err
@ -294,12 +362,23 @@ type dataCloser struct {
io.WriteCloser io.WriteCloser
} }
// Close releases the lock, closes the WriteCloser, waits for a response, and then returns any error encountered.
func (d *dataCloser) Close() error { func (d *dataCloser) Close() error {
d.c.mutex.Lock()
_ = d.WriteCloser.Close() _ = d.WriteCloser.Close()
_, _, err := d.c.Text.ReadResponse(250) _, _, err := d.c.Text.ReadResponse(250)
d.c.mutex.Unlock()
return err 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 // 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 // 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 // close the writer before calling any more methods on c. A call to
@ -309,7 +388,14 @@ func (c *Client) Data() (io.WriteCloser, error) {
if err != nil { if err != nil {
return nil, err 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 var testHookStartTLS func(*tls.Config) // nil, except for tests
@ -405,7 +491,10 @@ func (c *Client) Extension(ext string) (bool, string) {
return false, "" return false, ""
} }
ext = strings.ToUpper(ext) ext = strings.ToUpper(ext)
c.mutex.RLock()
param, ok := c.ext[ext] param, ok := c.ext[ext]
c.mutex.RUnlock()
return ok, param return ok, param
} }
@ -438,7 +527,12 @@ func (c *Client) Quit() error {
if err != nil { if err != nil {
return err return err
} }
return c.Text.Close() c.mutex.Lock()
err = c.Text.Close()
c.isConnected = false
c.mutex.Unlock()
return err
} }
// SetDebugLog enables the debug logging for incoming and outgoing SMTP messages // SetDebugLog enables the debug logging for incoming and outgoing SMTP messages
@ -472,6 +566,44 @@ func (c *Client) SetDSNRcptNotifyOption(d string) {
c.dsnrntype = d 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 {
c.mutex.RLock()
isConn := c.isConnected
c.mutex.RUnlock()
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()
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)
}
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.isConnected {
return nil, ErrNoConnection
}
if !c.tls {
return nil, ErrNonTLSConnection
}
if conn, ok := c.conn.(*tls.Conn); ok {
cstate := conn.ConnectionState()
return &cstate, nil
}
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 // debugLog checks if the debug flag is set and if so logs the provided message to
// the log.Logger interface // the log.Logger interface
func (c *Client) debugLog(d log.Direction, f string, a ...interface{}) { func (c *Client) debugLog(d log.Direction, f string, a ...interface{}) {

View file

@ -25,6 +25,9 @@ func (c *Client) ehlo() error {
if err != nil { if err != nil {
return err return err
} }
c.mutex.Lock()
defer c.mutex.Unlock()
ext := make(map[string]string) ext := make(map[string]string)
extList := strings.Split(msg, "\n") extList := strings.Split(msg, "\n")
if len(extList) > 1 { if len(extList) > 1 {

View file

@ -22,12 +22,15 @@ import "strings"
// should be the preferred greeting for servers that support it. // should be the preferred greeting for servers that support it.
// //
// Backport of: https://github.com/golang/go/commit/4d8db00641cc9ff4f44de7df9b8c4f4a4f9416ee#diff-4f6f6bdb9891d4dd271f9f31430420a2e44018fe4ee539576faf458bebb3cee4 // 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 { func (c *Client) ehlo() error {
_, msg, err := c.cmd(250, "EHLO %s", c.localName) _, msg, err := c.cmd(250, "EHLO %s", c.localName)
if err != nil { if err != nil {
return err return err
} }
c.mutex.Lock()
defer c.mutex.Unlock()
ext := make(map[string]string) ext := make(map[string]string)
extList := strings.Split(msg, "\n") extList := strings.Split(msg, "\n")
if len(extList) > 1 { if len(extList) > 1 {

File diff suppressed because it is too large Load diff

17
tls.go
View file

@ -4,25 +4,32 @@
package mail 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 type TLSPolicy int
const ( const (
// TLSMandatory requires that the connection to the server is // TLSMandatory requires that the connection to the server is
// encrypting using STARTTLS. If the server does not support STARTTLS // 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 TLSMandatory TLSPolicy = iota
// TLSOpportunistic tries to establish an encrypted connection via the // TLSOpportunistic tries to establish an encrypted connection via the
// STARTTLS protocol. If the server does not support this, it will fall // 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 TLSOpportunistic
// NoTLS forces the transaction to be not encrypted // NoTLS forces the transaction to be not encrypted.
NoTLS 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 { func (p TLSPolicy) String() string {
switch p { switch p {
case TLSMandatory: case TLSMandatory:

View file

@ -10,6 +10,7 @@ const (
keyFilePath = "dummy-child-key.pem" keyFilePath = "dummy-child-key.pem"
) )
// getDummyCertificate loads a certificate and a private key form local disk for testing purposes
func getDummyCertificate() (*tls.Certificate, error) { func getDummyCertificate() (*tls.Certificate, error) {
keyPair, err := tls.LoadX509KeyPair(certFilePath, keyFilePath) keyPair, err := tls.LoadX509KeyPair(certFilePath, keyFilePath)
if err != nil { if err != nil {