Compare commits

...

193 commits

Author SHA1 Message Date
edc5300b54
Merge pull request #349 from wneessen/fix-dependency-review-bug
Add base-ref and head-ref to Dependency Review action
2024-10-28 22:59:42 +01:00
8bc9b8b7fd
Add base-ref and head-ref to Dependency Review action
This update ensures that the Dependency Review action has the correct references for base and head in pull request events. It helps in accurately reviewing dependency changes between the branches. Reference: https://github.com/actions/dependency-review-action/issues/456#issuecomment-1537840047
2024-10-28 22:59:16 +01:00
c8478fb6c0
Merge pull request #348 from wneessen/feature/overhaul-tests
Overhaul test suite and GH CI workflow
2024-10-28 22:52:01 +01:00
c9f8a2acdd
Fix newline character in test attachment string for Windows
Updated the base64-encoded string for attachments in the tests to reflect the correct newline character for Windows. This ensures that the test cases correctly validate the presence of attachments in the mail messages.
2024-10-28 22:28:23 +01:00
b63a3dab9a
Adjust tests to handle platform-specific differences.
Added platform-specific checks for header and attachment content in `msgwriter_test.go`. This ensures compatibility and correct behavior on different operating systems, such as Windows and FreeBSD.
2024-10-28 22:20:09 +01:00
3a046d728e
Add test attachment and its license file
Introduced a new test attachment file for testing purposes. Also added a corresponding license file indicating copyright and licensing information for transparency and compliance.
2024-10-28 22:08:35 +01:00
a30215ebce
Add tests for writeHeader and writeBody functions
Introduce comprehensive test cases for the writeHeader and writeBody functions in msgWriter. These tests cover various scenarios including handling of multiple header values, edge cases, and error conditions in body writing operations. This concludes all tests for msgwriter.go.
2024-10-28 22:02:43 +01:00
b7a87fb15b
Add tests for MsgWriter_writeString function
Implemented tests to validate the MsgWriter writeString method for successful writes, failure scenarios, and handling pre-existing errors in the writer. This improves coverage and ensures robustness of the MsgWriter component.
2024-10-28 21:30:50 +01:00
dedb0e36c8
Add tests for MsgWriter writePart functionality
Introduce tests to validate MsgWriter's handling of message parts. This includes ensuring charset defaulting behavior and proper inclusion of part descriptions.
2024-10-28 21:25:22 +01:00
8e11fabbaf
Remove unused TestRcpt constant from client_test.go
The TestRcpt constant was defined but never used within the test file, leading to unnecessary clutter. Removing it enhances code readability and maintainability.
2024-10-28 21:14:05 +01:00
5c8d07407f
Add tests for file attachments in msgWriter
This commit introduces unit tests covering various scenarios for attaching files in email messages using msgWriter. These tests check for proper encoding, custom content types, different transfer encodings, custom descriptions, and handling attachments without body parts.
2024-10-28 21:13:36 +01:00
61244a541e
Refactor msgWriter tests and add new test cases
Reorganize existing tests for msgWriter and writeMsg, adding subtests for various encoding, charset, and failure scenarios. Enhanced tests to cover multipart/mixed, multipart/related, multipart/alternative, application/pgp-encrypted, application/pgp-signature messages, and handling of preformatted headers.
2024-10-28 20:52:38 +01:00
7bbcee7d48
Add tests for consecutive attachment and embedding
Added tests for `AttachReader`, `AttachReadSeeker`, `EmbedReader`, and `EmbedReadSeeker` methods with consecutive `WriteTo` calls to ensure attachments are not lost. This addresses issue #110 on GitHub. Also, added tests for `SetBodyWriter` with a nil option and improved existing tests for `hasAlt` and `hasMixed` methods. This commit concludes the tests for msg.go. We have achieved the highest possible coverage.
2024-10-28 19:47:00 +01:00
d0280ea9ad
Add test coverage for WriteToTempFile method
Implemented unit tests for WriteToTempFile, including successful writing to a temporary file and failure cases due to an invalid TMPDIR. These tests ensure the method behaves correctly under different conditions and enhances code robustness.
2024-10-28 18:52:02 +01:00
c28bd7e331
Prevent Test Execution Without Env Variable
Added a check for the "PERFORM_UNIX_OPEN_WRITE_TESTS" environment variable to conditionally skip tests. This ensures tests are only executed when the variable is set to "true". Additionally, commented out the TestMsg_WriteToTempFileFailed function.
2024-10-28 17:42:18 +01:00
1c6b9faf15
Add tests for Msg.HasSendError method.
Introduce several unit tests for the Msg.HasSendError function to ensure its correct behavior under different conditions like unsent messages, failed deliveries, and temporary errors. This enhances the validation of message error states within the system.
2024-10-28 17:34:35 +01:00
7d670a1f24
Add temporary failure condition in client test
Introduce a new `FailTemp` property to simulate a temporary failure during the DATA transmission phase in tests. This helps ensure better coverage and handling of specific error conditions.
2024-10-28 17:31:11 +01:00
2f3809f33e
Add tests for Msg.UpdateReader method
Implemented new tests to validate the Msg.UpdateReader method, ensuring its functionality for both success and failure scenarios. Removed obsolete tests for Msg.NewReader to maintain code clarity and relevance.
2024-10-28 17:20:40 +01:00
4ecc6f2b0c
Refactor Msg tests and remove unused code
Reorganized message writing tests to create more concise and specific scenarios, including new context tests and WriteToFile tests. Removed outdated and redundant test cases to streamline the test suite.
2024-10-28 17:14:06 +01:00
a59173fae0
Refactor buffer initialization in NewReader.
Replaced bytes.Buffer{} with bytes.NewBuffer(nil) for buffer initialization in NewReader method of Msg. This change enhances clarity and consistency in buffer handling.
2024-10-28 17:04:44 +01:00
d39953c837
Update environment variables in CI workflow
Standardize variables by using expressions for consistency. This change ensures that all environment variables for Unix and Sendmail tests are sourced from GitHub variables.
2024-10-28 16:32:24 +01:00
6cbb6745bb
Add new test cases for message attachments and long headers
Introduce tests for "AttachFile" with a normal file and "WriteTo" with long headers. Additionally, remove outdated and redundant test functions.
2024-10-28 15:58:14 +01:00
4506472319
Add missing newline after test function
A newline was added after an anonymous function block to improve code readability. This change adheres to proper formatting practices and enhances the maintainability of the code.
2024-10-28 15:43:32 +01:00
759452f346
Refactor tests to remove redundant unix-specific cases
Unified the test cases for AttachFile, AttachReader, AttachReadSeeker, EmbedFile, EmbedReader, and EmbedReadSeeker by incorporating the previously separate UNIX-specific tests into the general test files. This change enhances maintainability by reducing code duplication and ensuring more consistent test coverage across platforms.
2024-10-28 15:42:20 +01:00
4eb9d8a1fa
Add tests for Msg.Write and WriteTo error handling
Implemented tests for the Msg.Write method and enhanced WriteTo error handling. Also added a failReadWriteSeekCloser type to explicitly simulate IO failures in tests, ensuring comprehensive reliability of error handling mechanisms.
2024-10-28 15:14:43 +01:00
afa65585a0
Add test for WriteToSkipMiddleware function in Msg structure
Implement a new test for the WriteToSkipMiddleware function to validate its behavior with normal mail parts. Ensure that the function properly skips the specified middleware and writes the message to a buffer correctly.
2024-10-28 14:58:55 +01:00
4a519a3b1f
Add tests for middleware application on messages
Introduced comprehensive test cases for applying middlewares (uppercase and encode) to messages. Also added tests for writing messages to memory buffers and parsing them back. Corrected messages in helper functions for consistency.
2024-10-28 14:54:42 +01:00
ae44d37d03
Add test for Msg_Reset function
This test ensures that the Reset method correctly clears all fields in the Msg struct. It verifies that the From, To, Subject, Body, Attachments, and Embeds fields are empty after calling Reset.
2024-10-28 12:30:13 +01:00
80bf7240b4
Remove redundant attachment and embed tests
Eliminated multiple test functions for file attachments and embeds, including TestMsg_AttachFile, TestMsg_AttachFromEmbedFS, and TestMsg_EmbedFile. These tests either duplicated functionality or were no longer relevant to the current code base. Simplifying the test suite enhances maintainability and reduces unnecessary checks.
2024-10-28 12:21:58 +01:00
f48ff6e150
Add tests for embedding functionalities in Msg
Introduce unit tests for EmbedReadSeeker, EmbedHTMLTemplate, EmbedTextTemplate, and EmbedFromEmbedFS methods in the Msg package. These tests cover various use cases and error scenarios, ensuring the correct handling of embedded content.
2024-10-28 12:17:20 +01:00
c395c83a06
Add tests for embedding files and readers
Introduced unit tests to verify the embedding of files and readers in messages, including tests specific to Unix file handling scenarios. These tests ensure correct handling of embed errors and proper cleanup of temporary files.
2024-10-28 12:01:31 +01:00
ae7b6a68c5
Add tests for the EmbedFile functionality in msg_test.go
This commit introduces three new test cases to validate the EmbedFile function in the msg_test.go file. The tests cover embedding an existing file, handling a non-existent file, and referencing pre-tested options.
2024-10-28 11:37:04 +01:00
d02f469658
Add test cases for AttachFromEmbedFS errors
Introduce two new test cases for the AttachFromEmbedFS method. These tests verify that the method correctly handles an invalid file path and a nil embed FS, ensuring error scenarios are properly managed.
2024-10-28 11:35:05 +01:00
e779777c9b
Add tests for message attachment methods using templates
Included unit tests to verify the functionality of the AttachHTMLTemplate and AttachTextTemplate methods, ensuring valid, invalid, and nil templates are handled correctly. Also added tests for AttachFromEmbedFS to validate attachments from embedded files.
2024-10-28 11:30:34 +01:00
f576b92ce2
Increase code coverage targets to 90% and tighten thresholds.
Updated project and patch coverage targets to 90% from 85% and 80%, respectively. Tightened thresholds from 5% to 2% to enforce stricter quality controls.
2024-10-28 11:30:01 +01:00
466c2892bf
Add AttachReadSeeker tests for Unix and general cases
Introduce tests for the AttachReadSeeker method, detailing scenarios for general use and Unix-specific paths. Ensure file handling and error conditions are properly validated to maintain robustness.
2024-10-28 09:34:05 +01:00
e7e0fe03bb
Remove redundant OS version from CI matrix
This commit eliminates the duplication of the '14.1' OS version in the CI workflow matrix configuration. Simplifying the matrix helps in avoiding redundant test runs and ensures a clearer CI process.
2024-10-27 21:00:22 +01:00
c0c4049964
Enable Unix write tests and fix OS version matrix.
Added a test environment variable to control Unix write tests and included their setup in the test function. Also corrected the OS version matrix by uncommenting '14.0' and '13.4'.
2024-10-27 20:59:24 +01:00
6a43cf4aaf
Rename and update tests for UNIX-specific functionality
Renamed `msg_nowin_test.go` to `msg_unix_test.go` to better reflect its purpose. Updated the file attachment test to use `/dev/mem` for Unix environments instead of a temporary file with restricted permissions, ensuring compatibility with continuous integration environments.
2024-10-27 20:46:16 +01:00
e74adb8b90
Update CI configuration for FreeBSD
Commented out older OS versions and added no copyback option. Adjusted the go test run step to include workspace directory change.
2024-10-27 18:22:22 +01:00
9682755e25
Delete unused .cirrus.yml configuration file
The .cirrus.yml file was removed as it is no longer needed for our CI/CD processes. This helps clean up the repository and avoids confusion over unused configurations.
2024-10-27 18:03:09 +01:00
8fb05a33ff
Update tempfile path in unit tests
Changed tempfile creation path to "testdata/tmp" for better test organization and to ensure temporary files are stored within the testdata directory. This change affects the TestMsg_AttachFile_unixOnly tests.
2024-10-27 18:02:37 +01:00
7b9df7de47
Remove harden-runner step from CI workflow
The harden-runner step has been removed from the FreeBSD testing matrix in the CI workflow. This change simplifies the workflow and removes an additional security auditing step.
2024-10-27 17:55:12 +01:00
bdffa22ad8
Remove duplicate Go test step from CI workflow
Eliminated a redundant "Run go test" step from the GitHub Actions CI configuration. This helps streamline the workflow and avoid unnecessary repetitions in the CI process.
2024-10-27 17:50:36 +01:00
89f29b241e
Add FreeBSD testing to CI workflow
Included FreeBSD versions 13.4, 14.0, and 14.1 in the CI pipeline. Configured the workflow to harden the runner, checkout code, and run tests on the FreeBSD virtual machine.
2024-10-27 17:49:04 +01:00
eed3dec7d6
Clean up redundant error checks in msg_nowin_test.go
Removed unnecessary assertions for specific error types in test cases. This streamlines the tests, maintaining focus on the primary failure conditions without checking the error type.
2024-10-27 16:19:16 +01:00
3be41b1aea
Fix chmod notation to use octal literal syntax
Updated the chmod call in msg_nowin_test.go to use the consistent and proper octal literal syntax (0o000) instead of 0000. This change improves code readability and aligns with Go convention.
2024-10-27 16:10:46 +01:00
f7cfe5289a
Remove obsolete tests and add error handling test
Removed commented-out tests to clean up the codebase. Added a new test to verify behavior when the file is closed early during attachment, ensuring robustness.
2024-10-27 16:08:50 +01:00
946ad294ce
Add tests for Msg attachment handling
Introduce new unit tests for verifying the `AttachReader` method and enhancing error handling in attachment functions. These tests cover scenarios for success, file permissions, and early file closure errors.
2024-10-27 15:54:39 +01:00
0cf636ee9b
Add test for AttachFile method and change Skip to Log
Replace t.Skip with t.Log to avoid skipping tests and ensure they log their status. Introduce a comprehensive test for the AttachFile method, covering cases with existing, non-existent files, and using options.
2024-10-27 15:28:42 +01:00
84f9d0583d
Refactor buffer initialization to use bytes.NewBuffer
Replaced direct bytes.Buffer{} initialization with bytes.NewBuffer(nil) for better consistency and potential performance improvements. This refactor affects both the HTML and plain text alternative writers.
2024-10-27 15:17:30 +01:00
254dc81706
Add tests for AddAlternativeTextTemplate method
Include various scenarios like default, with existing body string, nil template, and invalid template. These tests help in validating the addition of alternative text templates to messages.
2024-10-27 15:16:49 +01:00
e00ddda3a3
Fix test expectation and add HTML template tests
Corrected the expected number of parts in an existing test from 1 to 2. Additionally, introduced tests for adding HTML templates to message bodies, including cases with a valid template, a body string, a nil template, and an invalid template.
2024-10-27 15:13:57 +01:00
602f8a6e29
Add comprehensive tests for message alternatives
Introduced tests for AddAlternativeString and AddAlternativeWriter methods, including variations for charset, encoding, content description, and simultaneous usages. Ensure accurate handling of content types, encodings, and body strings. This enhances coverage and reliability of message formatting features.
2024-10-27 15:10:01 +01:00
4db66696a6
Add tests for SetBodyTextTemplate method in msg_test.go
Implemented thorough unit tests to validate the SetBodyTextTemplate method in msg_test.go. These tests cover default behavior, handling of nil templates, and invalid templates, ensuring robustness and reliability.
2024-10-27 14:56:34 +01:00
f8caa5599b
Refactor buffer creation in msg.go
Replace `bytes.Buffer{}` with `bytes.NewBuffer(nil)` for buffer instantiation to improve clarity and consistency. This change simplifies the buffer creation process and aligns it with common Go practices.
2024-10-27 14:55:51 +01:00
35ffc95102
Add tests for SetBodyWriter and SetBodyHTMLTemplate
Introduced new tests to validate the functionality of `SetBodyWriter` with `WithPartEncoding` and `WithPartContentDescription`. Also added comprehensive tests for `SetBodyHTMLTemplate`, covering default behavior and error cases.
2024-10-27 14:50:14 +01:00
432e21f162
Refactor buffer initialization in message template execution
Change buffer initialization from an empty `bytes.Buffer` to `bytes.NewBuffer(nil)` to streamline buffer allocation. This adjustment ensures better flexibility and aligns with best practices in handling buffer slices.
2024-10-27 14:33:00 +01:00
0aa81d724b
Reorganize and cleanup msg_test.go
Removed redundant and commented-out test functions to improve code readability and maintainability. Moved the essential helper functions (checkAddrHeader and checkGenHeader) to the end to keep the structure organized.
2024-10-27 14:20:44 +01:00
78ee1a2a81
Fix timeout message in client_test.go
Updated the skip message to accurately reflect the timeout reason when closing the test server connection. This improves the clarity of the error reporting in tests.
2024-10-27 12:11:48 +01:00
b510d2654c
Handle timeout errors in test client cleanup
Added a check in the client cleanup function to skip the test if the client connection fails due to a timeout. This ensures that network-related timeouts do not cause test failures, providing more reliable test outcomes.
2024-10-27 12:10:51 +01:00
056ec60734
Remove newline characters in test data strings
Trim newline characters from test attachment and embed strings. Updated assertions to compare strings without trailing newlines for consistency.
2024-10-27 12:05:18 +01:00
cb5ac8b0e2
Suppress inspection for deprecated method usage in test
Added GoLand directive to suppress deprecation warnings for SetAttachements method in msg_test.go. This method is already fully tested by SetAttachments and the test is marked to be skipped.
2024-10-27 11:32:15 +01:00
6376f29190
Add ContentType tests and simplify error messages
Introduced new test cases for ContentType values to ensure proper handling. Additionally, simplified multiple error messages in existing tests to enhance readability and maintain consistency.
2024-10-27 11:27:38 +01:00
9a01629c47
Add unit tests for msg embed setters and unsetters
Introduce tests for `SetEmbeds` with single, multiple, and no files. Add `UnsetAllEmbeds` and `UnsetAllParts` tests to ensure embeds and attachments are removed correctly.
2024-10-27 10:49:02 +01:00
2dad9d36b2
Add embed and attachment tests
Introduced tests for embedding files and managing attachments in messages. Validated behavior for setting and unsetting attachments and retrieving embedded files with various scenarios.
2024-10-27 10:42:32 +01:00
c8c7d18ba9
Add test attachment with license information
Introduced a new test attachment file to the testdata directory. Included a corresponding license file to specify copyright and licensing details.
2024-10-27 10:30:20 +01:00
472a5a6454
Add tests for SetAttachments in msg_test.go
Implemented comprehensive tests for SetAttachments including scenarios with single, multiple, and no files. Additionally, deprecated the SetAttachements function and noted it as fully tested by SetAttachments.
2024-10-27 10:25:53 +01:00
f2619737e8
Add tests for Msg GetBoundary method
Introduce unit tests for the GetBoundary method in the Msg type. Ensure proper functionality for messages with and without a boundary set.
2024-10-27 10:13:14 +01:00
b7ca41af81
Remove redundant TestMsg_SetAttachments
Eliminated the TestMsg_SetAttachments function from the test suite as it duplicates functionality covered by other tests. Removing this redundant code improves maintainability and clarity.
2024-10-27 10:09:56 +01:00
66e25d82d3
Add test for GetAttachments method with no attachment
Introduced a new test case to check `GetAttachments` when no attachments are present in the message object. Removed the redundant `TestMsg_GetAttachments` function to simplify the test suite.
2024-10-27 10:09:25 +01:00
babf7b9780
Add tests for GetAttachments method in Msg
This commit introduces unit tests for the GetAttachments method in the Msg struct to ensure it correctly handles both single and multiple attachments. The tests verify the name and content of the attachments to validate the expected behavior.
2024-10-27 10:07:05 +01:00
43f9ffa3af
Add tests for GetGenHeader and GetParts methods in Msg
Introduce comprehensive tests for the GetGenHeader and GetParts methods. These tests cover single and multiple values for headers, body parts retrieval, and edge cases such as nil values, ensuring robustness and correctness.
2024-10-27 09:58:22 +01:00
e808e0b972
Add comprehensive unit tests for email address getters
Introduced unit tests for GetFrom, GetFromString, GetTo, GetToString, GetCc, GetCcString, GetBcc, and GetBccString methods. These tests cover scenarios with single, multiple, and no addresses, ensuring correct functionality of email address retrieval methods.
2024-10-26 23:20:23 +02:00
22f56a0143
Add tests for GetAddrHeaderString in msg_test.go
Add unit tests for the GetAddrHeaderString function to verify correct behavior for various header types and address counts. These tests ensure the function correctly handles single and multiple addresses, as well as scenarios with no addresses.
2024-10-26 22:57:47 +02:00
d16ae61f64
Add test for GetAddrHeader with multiple valid addresses
Introduce a new test to validate the behavior of GetAddrHeader for handling multiple addresses (to, cc, bcc). This test ensures that the function correctly processes and returns the expected addresses in various headers.
2024-10-26 22:53:30 +02:00
5e5bcef696
Add tests for Msg_GetAddrHeader
Introduce unit tests for the GetAddrHeader function in the Msg struct. These tests cover various scenarios, including valid addresses and handling of headers like "from," "to," "cc," and "bcc."
2024-10-26 22:50:31 +02:00
7b600534ea
Add tests for Msg.GetRecipients method
Implemented comprehensive tests for the Msg.GetRecipients method, covering various scenarios such as recipients in "to", "cc", "bcc" fields and combinations of these. Ensured correct handling of each case and validation against expected recipient counts and specific addresses.
2024-10-26 22:31:13 +02:00
842d4373f2
Add missing newline at EOF
Ensured the file ends with a newline to comply with best practices. This change improves code readability and consistency with other files in the repository.
2024-10-26 22:14:56 +02:00
5c2831c331
Add tests for MDN address formatting and GetSender logic
Implemented tests for `RequestMDNToFormat`, `RequestMDNAddTo`, and `RequestMDNAddToFormat` methods to ensure proper address handling. Added comprehensive `GetSender` method tests to verify sender retrieval in various scenarios. This enhances coverage and robustness of email message handling functionality.
2024-10-26 22:13:51 +02:00
4fe9022815
Refactor RequestMDNAddTo
Simplify the address handling for the "Disposition-Notification-To" header in msg.go. The code now directly reassigns the previously fetched addresses and ensures appending the new address effectively. This improves code readability and correctness in updating the header.
2024-10-26 22:13:37 +02:00
4f97cd8261
Initialize genHeader in RequestMDNTo method
Add checks to initialize `genHeader` if it is nil in the RequestMDNTo method to prevent potential nil map assignment errors. The newly implemented tests showed that this method was never actually working and the old test was inefficient to identify this.
2024-10-26 21:33:43 +02:00
9e51dba82a
Add new tests for Msg methods
Introduce tests for SetImportance, SetOrganization, SetUserAgent, and IsDelivered methods in the Msg type. These additions ensure the correct behavior and robustness of the email handling functionality.
2024-10-26 21:07:48 +02:00
cf117d320b
Add nil check in testMessage helper function
Added a conditional check to ensure 'NewMsg()' does not return nil in the 'testMessage' helper function. This update will prevent potential nil pointer dereferences and improve the robustness of the test.
2024-10-26 21:07:04 +02:00
42c63791ef
Move delivery status update after writer close check
Relocated the `isDelivered` flag update to occur after the writer's close method validation. This ensures that the message delivery status is only marked as true if no errors arise during the writer close process. This showed up in the new test cases that covered errors on closing the DATA channel. This can already happen before the first byte is sent to the server. Therefore isDelivered should be set after a successful close.
2024-10-26 21:06:21 +02:00
f5279cd584
Refactor character encoding test cases formatting
Reformatted the test cases for European umlaut characters for consistency and readability. This change does not affect the execution or output of the tests.
2024-10-26 20:22:04 +02:00
ef3f103c30
Add additional tests for setting message headers
Introduce tests for SetMessageIDWithValue, SetDate, and SetDateWithValue functions. Enhance coverage and validation for these methods by comparing generated headers and parsed dates.
2024-10-26 20:08:16 +02:00
ae15a12ce5
Refactor SetDate to use SetDateWithValue
Replaces direct time formatting in SetDate with a call to SetDateWithValue, improving code reusability and readability. The new approach centralizes date formatting logic in one method.
2024-10-26 19:57:02 +02:00
ea5b02bfdd
Add tests for SetMessageID and GetMessageID methods
Introduce unit tests to ensure SetMessageID generates unique IDs and GetMessageID correctly formats various input IDs. Additionally, verify GetMessageID returns an empty string when no ID is set.
2024-10-26 19:30:48 +02:00
591425bb99
Add test for European umlaut characters and remove others
Added a test case for European umlaut characters in msg_test.go. Removed redundant test cases related to address handlers (To, Cc, Bcc) and other helper methods to streamline the code.
2024-10-26 19:10:25 +02:00
007286fc5e
Add unit tests for Msg.Subject() method
Introduced unit tests to verify the encoding of different character sets in the Msg.Subject() method. Tests include cases for Latin characters, Japanese, Simplified Chinese, Cyrillic, and Emoji to ensure proper encoding handling.
2024-10-26 19:03:31 +02:00
5b602be818
Add tests for ReplyToFormat method
Implement new tests for the Msg.ReplyToFormat method to ensure it correctly handles both valid and invalid email addresses. These tests validate the ReplyTo header formatting and error handling.
2024-10-26 18:46:46 +02:00
96d45c26bc
Add tests for Msg.ReplyTo function
Introduce unit tests for the `ReplyTo` method in the `Msg` struct to validate both correct handling of valid email addresses and proper failure responses for invalid ones. Further, refactor and add helper functions to check generated headers in messages.
2024-10-26 18:40:53 +02:00
953a4b4df1
Add Bcc-related unit tests to msg_test.go
Introduce comprehensive unit tests for Bcc functionalities, including validation of single and multiple addresses, various invalid input scenarios, and RFC5322 compliance checks. Additionally, implement tests for AddBcc, AddBccFormat, BccIgnoreInvalid, and BccFromString methods to ensure robust handling of different Bcc cases.
2024-10-26 18:29:51 +02:00
f079ea09eb
Improve BccFromString to handle spaces and empty addresses
Trim spaces from email addresses and skip empty addresses in BccFromString method. This ensures that the BCC list only includes valid, non-empty email addresses, enhancing email sending reliability.
2024-10-26 18:29:26 +02:00
03cb09c3bd
Add tests for Cc, AddCc, AddCcFormat, CcIgnoreInvalid, and CcFromString
This commit introduces comprehensive tests for various "Cc" related methods in the `Msg` struct. It includes test cases for valid and invalid email addresses for methods: `Cc`, `AddCc`, `AddCcFormat`, `CcIgnoreInvalid`, and `CcFromString`, ensuring robust handling of different scenarios and edge cases.
2024-10-26 18:22:01 +02:00
855d7f0867
Refine CcFromString to handle spaces and empty addresses.
This change ensures that email addresses with leading or trailing spaces are trimmed, and empty addresses resulting from multiple commas are ignored. This helps prevent potential errors with malformed email addresses in the "Cc" field.
2024-10-26 18:21:50 +02:00
d7b32480fd
Handle test server connection timeouts
Add logic to skip tests if there's a timeout error while connecting to the test server. This ensures that transient network issues do not cause test failures.
2024-10-26 17:33:52 +02:00
23399ed84c
Change DefaultHost to loopback address
Updated DefaultHost from "localhost" to "127.0.0.1" in client_test.go. This change ensures consistent and direct communication with the local machine, avoiding potential issues with DNS resolution of "localhost".
2024-10-26 17:17:58 +02:00
90e3162a22
Update CI to support older Go versions
Added Go 1.19 and 1.20 to the CI matrix to extend compatibility testing. This ensures that our project continues to work with these older versions of Go.
2024-10-26 17:11:21 +02:00
273a26ca53
Add tests for Go 1.21+ compatibility
Moved some tests that use JSONlog (which requires Go 1.21+) to a separate file that is skipped on older Go versions.
2024-10-26 17:10:50 +02:00
a815c58571
Update CI workflow to install nullmailer instead of ssmtp
Replacing ssmtp with nullmailer ensures better compatibility with the updated email delivery requirements. The DEBIAN_FRONTEND=noninteractive parameter was also added to avoid interactive prompts during installation.
2024-10-26 16:45:07 +02:00
c33900ca29
Add sudo to apt-get commands in CI workflow
Previously, the apt-get commands lacked the necessary sudo prefix, which could lead to permission issues during the CI process. This change ensures that updates, upgrades, and installations are executed with the appropriate permissions.
2024-10-26 16:26:49 +02:00
4b8bf0507d
Update CI workflow and sendmail test condition
Add the TEST_SENDMAIL environment variable for better control over sendmail tests. Optimize sendmail installation in CI by updating and installing ssmtp. Modify tests to check if TEST_SENDMAIL is set to "true" before running.
2024-10-26 16:25:42 +02:00
9072aef355
Remove support for Go 1.19 and 1.20 in CI workflow
This commit updates the CI configuration to no longer test against Go versions 1.19 and 1.20. The supported Go versions are now 1.21, 1.22, and 1.23, ensuring the CI pipeline aligns with our current support policy.
2024-10-26 15:58:13 +02:00
3aef85e324
Add SPDX license headers to CI workflow file
This change adds SPDX license headers to the .github/workflows/ci.yml file to ensure proper attribution and compliance with the MIT license. The added headers include copyright information and the applicable license type.
2024-10-26 15:57:27 +02:00
f82ac0c5ae
Update concurrency group names in GitHub Actions
This change modifies the concurrency group names to include OS and Go version for better differentiation. This prevents conflicts and ensures that concurrent jobs are properly managed based on their specific matrices.
2024-10-26 15:52:27 +02:00
eeccee0d94
Add checkout step to CI workflow
Ensure CI workflow has access to the latest code by adding a checkout step. This change is necessary for the golangci-lint action to function correctly with the most recent codebase.
2024-10-26 15:50:14 +02:00
9c57ba56cf
Add 'runs-on' directive to lint and govulncheck steps
This ensures the lint and vulnerability check steps run on the 'ubuntu-latest' environment. Establishing a clear execution environment helps maintain consistency across CI runs.
2024-10-26 15:48:10 +02:00
4d4aa1e1df
Add runs-on parameter to dependency review job
This change specifies that the dependency review job should run on the latest version of Ubuntu. It ensures consistency and clarity in the workflow configuration. This modification helps avoid potential issues related to unspecified runner environments.
2024-10-26 15:47:04 +02:00
960c015a93
Remove IPv6 and IPv4 test cases from msg_test.go
Removed test cases for IPv6 and IPv4 addresses from `msg_test.go` as they are no longer required. Also made a minor formatting adjustment to the `checkAddrHeader` function signature for better readability.
2024-10-26 15:45:01 +02:00
12e9a0cb5d
Simplify CI workflow branch checks
Removed file path filters on branch triggers in the CI workflow configuration. This allows the CI to run for any changes made in the main branch, ensuring broader test coverage and catching issues early.
2024-10-26 15:37:07 +02:00
9e6c1f0417
Consolidate CI workflows into a single file
Merged separate workflows for Codecov, dependency-review, golangci-lint, govulncheck, offline-tests, reuse compliance, and SonarQube into a unified CI workflow file in `.github/workflows/ci.yml`. This restructuring simplifies our CI setup and ensures more consistent and efficient pipeline management.
2024-10-26 15:33:05 +02:00
0e9646e0e4
Merge branch 'main' into feature/overhaul-tests 2024-10-26 14:58:41 +02:00
3a3eaed348
Refactor address header tests to use checkAddrHeader
Consolidate repeated header validation logic into the `checkAddrHeader` helper function. This refactoring improves code readability and maintainability by reducing redundancy and potential for errors.
2024-10-26 02:07:44 +02:00
e08d36d0b8
Refactor address header validation in tests
Replace repeated address header validation code with a helper function `checkAddrHeader` to reduce redundancy and improve readability. Also, add new test cases for `ToFromString` to handle valid addresses with and without empty fields.
2024-10-25 20:21:17 +02:00
c99b6c3f14
Fix ToFromString to handle and trim empty addresses
Previously, the ToFromString function split email addresses by commas but did not handle empty addresses or trim whitespace. Now, it trims each address and ignores any empty entries to ensure only valid addresses are processed. This prevents potential errors stemming from malformed input.
2024-10-25 19:54:41 +02:00
03da20fc39
Add unit tests for Msg_ToIgnoreInvalid behavior
Introduces new test cases to verify the Msg_ToIgnoreInvalid function handles various scenarios correctly. These tests check for behavior with valid addresses, invalid addresses, and a mix of both.
2024-10-25 19:43:05 +02:00
8b6a7927ef
Add dummy test function in msg_test.go
Introduce a new, empty test function `TestMsg_ToIgnoreInvalid` in `msg_test.go` to accommodate future test cases. This commit also aligns a comment for better readability.
2024-10-25 17:43:59 +02:00
1ea7b173c6
Add tests for Msg.AddToFormat
Introduced new test cases to validate Msg.AddToFormat functionality with both valid and invalid email addresses. Ensured proper error handling and address formatting in the message headers.
2024-10-25 17:16:46 +02:00
a7f81baa4b
Fix address references in tests and add AddTo functionality
Corrected incorrect address references in the `msg_test.go` file. Added new tests to validate the AddTo functionality, ensuring multiple addresses can be added and validated properly.
2024-10-25 17:13:22 +02:00
cb85a136c3
Add Goland noinspection comments to suppress deprecation warnings
This change adds `//goland:noinspection GoDeprecation` comments in the `msg_test.go` file. These comments suppress deprecation warnings for the `SetHeader` and `SetHeaderPreformatted` methods during test execution.
2024-10-25 17:05:22 +02:00
aa46b408ad
Add additional tests for From and To address handling
Refactor "From" related tests to improve error messages and add tests for the new "FromFormat" function. Introduce new tests to validate the "To" address handling, covering various valid and invalid address scenarios.
2024-10-25 17:03:57 +02:00
5d85be068d
Add comprehensive tests for email "From" field validation
Implemented extensive test cases for the "From" field in email messages, covering various valid and invalid email formats according to RFC5322. Verified correct handling for each scenario to ensure robustness of email address validation.
2024-10-25 16:36:34 +02:00
1caa2cfb92
Add tests for EnvelopeFrom and EnvelopeFromFormat methods
Implemented comprehensive tests for the EnvelopeFrom and EnvelopeFromFormat methods to ensure proper handling of valid and invalid email addresses. This includes validation of both address and format for the "EnvelopeFrom" header.
2024-10-25 15:14:06 +02:00
c8dbc9a735
Update tests for SetAddrHeader and add new test cases
Renamed existing test cases for clarity and added new test cases for SetAddrHeaderIgnoreInvalid function. The new tests cover multiple scenarios including valid/invalid addresses, and edge cases such as nil addrHeader map.
2024-10-25 15:00:53 +02:00
08fe44c051
Initialize address header map and enforce single 'From' address.
This commit ensures that the address header map is properly initialized before assigning addresses to it. Additionally, it enforces the rule that only a single 'From' address is allowed, preventing multiple addresses from being set for the 'From' header. These misses were found while working on the enhanced testing suite
2024-10-25 14:57:36 +02:00
7d352bc58e
Add comprehensive tests for header and address methods
Implemented extensive unit tests for setting various headers and address fields in the `Msg` struct, including setting general headers, preformatted headers, and address headers with various scenarios to ensure correctness and robustness.
2024-10-25 12:18:26 +02:00
9505f94e3d
Refactor header test structure and improve readability
Simplify test structure by renaming fields for clarity and brevity. AddressHeader tests and importance tests now use more concise and consistent naming conventions. This enhances readability and maintainability of the test code.
2024-10-25 12:18:12 +02:00
143e3b5b4f
Fix context in tests and improve error handling
Updated test cases to use a predefined context instead of creating new backgrounds. Additionally, improved error handling by checking for client creation failures and adding appropriate fatal log messages. These changes enhance test reliability and debugging clarity.
2024-10-25 11:46:48 +02:00
a2e9dbae11
Add unit tests for charset and header setting in messages
This commit introduces two new unit tests: `TestMsg_Charset` and `TestMsg_SetHeader`. These tests cover the functionality of setting and retrieving the character set and headers for messages, ensuring correctness and robustness.
2024-10-25 11:36:26 +02:00
69c5f43cbf
Refactor test cases in header_test.go
Consolidate repetitive test case definitions into separate slices for readability and maintainability. This change simplifies the addition of new test cases and reduces redundancy.
2024-10-25 11:36:17 +02:00
425a190eb1
fumpt'ed formatting 2024-10-25 11:36:05 +02:00
64aeb683ba
Format test cases for consistency
Reformat test cases in `client_test.go` for better readability and consistency by converting single-line struct definitions into multi-line blocks. This change improves code maintainability and aligns the formatting across similar tests.
2024-10-25 11:35:25 +02:00
1dba76948f
Simplify test descriptions in msg_test.go
Shortened the test case descriptions for better clarity and readability. Added a new test function 'TestMsg_Encoding' to verify encoding outputs.
2024-10-25 11:24:35 +02:00
120c2efd08
Refactor test cases with shared test data
Consolidated repetitive test data into shared variables to improve code maintainability and readability. This modification also helps in reducing redundancy and streamlining future updates to test data.
2024-10-25 11:18:09 +02:00
c4946af3ab
Refactor msg_test.go to streamline tests and reduce redundancy
Reorganized and condensed test cases for the NewMsg method by merging individual tests and removing obsolete functions. This improves maintainability and reduces duplication in the test suite.
2024-10-25 11:08:11 +02:00
64cfbf9e46
Rename test functions and add new header cases.
Renamed test functions from `TestHeader_String` to `TestHeader_Stringer` and `TestAddrHeader_String` to `TestAddrHeader_Stringer` for consistency. Additionally, added new header cases such as "Content-Description" and "X-Auto-Response-Suppress" to improve test coverage.
2024-10-25 09:53:49 +02:00
c58d52e49a
Refactor TestImportance_StringFuncs in header_test.go
Separated test cases for String, NumString, and XPrioString methods of the Importance object into distinct sub-tests. Improved readability and maintainability by grouping similar assertions together.
2024-10-25 09:48:00 +02:00
eebbaa2513
Refactor and reintegrate content type tests in file_test.go
Reintegrates previously commented out tests for file content types, ensuring they are part of a structured t.Run block. Enhances readability and maintains functionality by checking the presence and correctness of attachments in a more concise manner.
2024-10-25 09:40:15 +02:00
8353b4b255
Follow upstream for HELO during Quit bug
I reported the bug I fixed in 74fa3f6f62 to Go upstream. They fixed simpler by just ignoring the error (See: https://go.dev/cl/622476). We follow this patch accordingly. The upstream test has been adopted as well.
2024-10-25 09:33:45 +02:00
9834c6508d
Refactor file_test.go to use subtests
Consolidate multiple test functions into a single TestFile function using subtests. This improves test organization and enhances readability. Added a new test case for testing file name attachment.
2024-10-24 17:09:55 +02:00
75e035c783
Add test cases for various EML parsing scenarios
Introduced a series of test cases for validating EML parsing against different edge cases, including invalid headers, content types, and transfer encodings. Ensured both valid and invalid EML strings are covered to thoroughly test the robustness of the parser.
2024-10-24 16:42:11 +02:00
769783f037
Refactor error handling in eml parser
Removed redundant error checking in address parsing as netmail.ParseAddressList already performs necessary checks. Added a default error return for unsupported content disposition types to improve robustness.
2024-10-24 16:42:00 +02:00
9f1e1976fe
Fix assignment in error handling for EML parsing function
Correct the variable assignment in the `if err` statement to ensure proper error handling. This change eliminates a potential bug where the wrong variable might be used.
2024-10-24 14:49:24 +02:00
887e3cd768
Add EML parsing with new tests and examples
Introduce new EML files with valid and invalid examples. Implement tests for EML parsing from readers and files, checking for both successful parsing and expected failures on invalid inputs.
2024-10-24 13:36:03 +02:00
127cfdf2bc
Fix error variable declaration in eml.go
The error variable declaration has been corrected from "if err := parseEML(...)" to "if err = parseEML(...)". This change ensures consistency with the rest of the error handling code in the file.
2024-10-24 13:20:09 +02:00
7ed23bf01b
Remove outdated client test cases
Removed obsolete and redundant client test cases that were no longer relevant. This cleanup improves code maintainability and readability by eliminating excessive, unused test methods.
2024-10-24 12:53:37 +02:00
0310527eb5
Completed client.go tests
We've now covered 96% of all code. Everything else is not testable for us at this point.
2024-10-24 12:25:13 +02:00
1399a3331a
Refactor and extend client email tests
Refactor existing email sending tests by organizing multiple edge cases and adding robust test coverage. This includes adding checks for invalid sender/recipient addresses, handling DSN support, and ensuring proper client server interactions during failures like DATA init, DATA close, and MAIL FROM.
2024-10-24 12:03:56 +02:00
45ebcb95b3
Remove redundant connection check in send function.
The connection check is performed in the c.Reset call just before the c.checkconn call, making this redundant. Removing it simplifies the code and eliminates unnecessary error handling for connection status. This change helps improve code maintainability.
2024-10-24 12:02:57 +02:00
1519522e5d
Reduce sleep duration in client tests
Decreased sleep time from 300ms to 30ms across multiple tests to improve test execution speed. Added a new test `TestClient_sendSingleMsg` to connect and send an email message, ensuring the robustness of the sending functionality.
2024-10-24 10:50:17 +02:00
3bf1992cab
Improve test conciseness and concurrency handling
Simplified repeated message initialization by introducing a helper function `testMessage(t)`. Enhanced existing tests by adding robust concurrency tests and refined the structure of email sending scenarios.
2024-10-24 10:45:05 +02:00
4a8ac76636
Add nil check for smtpClient in checkConn function
Previously, if smtpClient was nil, the checkConn function would not handle that case. This update ensures that an appropriate error is returned when smtpClient is nil, enhancing the robustness of the client connection checks.
2024-10-24 10:44:40 +02:00
5e3ebcc1a6
Remove redundant connection check in auth function
The checkConn call in the auth function was redundant because the connection is already managed appropriately elsewhere. Removing this unnecessary check simplifies the code and avoids potential duplication of error handling.
2024-10-24 10:12:57 +02:00
040289cea4
Remove hardcoded test credentials and add new auth tests.
Replaced hardcoded SMTP credentials with generic placeholders for improved security. Added new test cases to handle unsupported authentication methods and connections without TLS.
2024-10-24 10:12:43 +02:00
2a2176d700
Add tests for various SMTP authentication methods
Implemented new test cases for different SMTP authentication methods including PLAIN, LOGIN, XOAUTH2, and various SCRAM mechanisms. These tests ensure that the client can correctly handle both successful and failing authentication scenarios, as well as unsupported authentication types.
2024-10-24 09:59:31 +02:00
e442419c18
Remove redundant connection check in tls function
The `tls` function in `client.go` no longer checks for an active connection before executing. This simplifies the code since the connection check is either redundant or already handled elsewhere in the flow.
2024-10-24 09:19:08 +02:00
28dc629674
Refactor and expand client dial-and-send tests
Renamed TestClient_DialAndSend to TestClient_DialAndSendWithContext and reorganized message setup. Added multiple test cases to cover different failure points in the DialAndSendWithContext method.
2024-10-24 00:50:16 +02:00
84ca70083a
Add test for DialAndSend functionality
Introduce a new test `TestClient_DialAndSend` to validate the process of dialing and sending an email using the SMTP client. This includes setting up a mock SMTP server and verifying the process from message creation to sending.
2024-10-24 00:30:57 +02:00
06f6fd3692
Add client reset functionality tests
Introduce tests for the client reset functionality, including scenarios to test successful reset, reset on a disconnected client, and reset with server failure. This ensures robustness and reliability of the client reset feature under various conditions.
2024-10-24 00:22:33 +02:00
2710250baa
Add *testing.T to simpleSMTPServer and logging improvements
Pass `*testing.T` to `simpleSMTPServer` for enhanced test logging and helper methods. This allows better integration with the testing framework, converting standard log outputs to `t.Logf` for improved test diagnostics and error reporting.
2024-10-24 00:11:58 +02:00
7f3cd8dc38
Merge branch 'main' into feature/overhaul-tests 2024-10-23 23:52:13 +02:00
cf1246d9ea
Remove redundant DialWithContext test cases
Deleted two test functions for DialWithContext that tested invalid HELO and authentication scenarios. These tests were deemed redundant as the error handling for these cases is covered elsewhere.
2024-10-23 23:34:13 +02:00
c63b8b124e
Add STARTTLS and SSL test cases for SMTP client
Extended test cases to include scenarios for STARTTLS and SSL connections. This includes handling TLS configurations, testing certificate handling, and tests for various authentication methods under TLS.
2024-10-23 23:33:33 +02:00
563ccbab4a
Fix typo in comment within Quit function
Corrected grammar in a comment to enhance code readability and maintain consistency. This change does not affect the functionality of the `Quit` method.
2024-10-23 22:26:08 +02:00
ea57644a8e
Add debug logging to client creation in tests
Introduce `WithDebugLog()` in client creation for enhanced logging during tests. Correct handling of certain SMTP command errors by replacing `return` with `break` in switch cases for proper loop continuation.
2024-10-23 22:20:32 +02:00
74fa3f6f62
Fix Quit handling when initial HELO fails
Ensure QUIT command can be sent even if initial HELO fails. Added a check to skip retrying HELO if it already failed, allowing for proper closing of the connection. This prevents potential hangs or errors during connection termination.
2024-10-23 21:54:06 +02:00
572751ac10
Add test for invalid HELO handling in SMTP client
Introduce a new test case to ensure the SMTP client fails gracefully when an invalid HELO command is used. This includes validating error handling and maintaining the client connection integrity. Also, optimize EHLO/HELO command handling by enhancing syntax checking and error response generation.
2024-10-23 18:20:52 +02:00
d281f838d4
Add integration tests for invalid host and HELO failure
Add test cases to validate client behavior on connecting with an invalid host and encountering a HELO failure. This helps ensure the client handles these error scenarios correctly, maintaining robustness and reliability.
2024-10-23 18:08:30 +02:00
0db1383940
Refactor buffer initialization in client tests
Replaced &bytes.Buffer{} with bytes.NewBuffer(nil) for buffer initialization in various client tests. This change ensures more idiomatic and consistent creation of byte buffers throughout the test cases.
2024-10-23 17:58:08 +02:00
21184e60b9
Switch to using bytes.NewBuffer(nil) in tests
Refactored the test code to initialize buffers using bytes.NewBuffer(nil) instead of &bytes.Buffer{}. This change ensures a consistent and idiomatic initialization of byte buffers throughout the test cases.
2024-10-23 17:57:09 +02:00
12695385e8
Refactor SMTP server test setup to use serverProps struct
Consolidate server configuration properties into a new serverProps struct for better code clarity and future extensibility. This change involves updating simpleSMTPServer and test cases to use the new struct, allowing more control over server behavior during tests.
2024-10-23 17:55:31 +02:00
8a6cd2b448
Add and update client tests
Reintroduces and enhances several client tests including `SetLogAuthData`, `Close`, `DialWithContext`, and others. Deprecated and non-functional tests are removed, and a new log parsing method is added for enhanced logging validation.
2024-10-23 17:34:23 +02:00
ae7160ddba
Refactor SMTP Auth unit test with table-driven approach
Replaced the single test case for setting custom SMTP authentication with a table-driven approach. This refactor improves test coverage by including multiple authentication methods such as CRAM-MD5, LOGIN, PLAIN, SCRAM, and XOAUTH2.
2024-10-23 16:46:18 +02:00
d4dc212dd3
Refactor client tests and add SetSMTPAuthCustom test
Removed outdated tests for TLSConfig, Username, Password, and SMTPAuth that were commented out. Added a new test for the SetSMTPAuthCustom method, specifically for PLAIN authentication, ensuring proper behavior and type checks.
2024-10-23 16:14:05 +02:00
17cb590a45
Add test for Client.SetSMTPAuth function
Introduce a new test to validate the functionality of the Client.SetSMTPAuth method. This test covers various SMTP authentication types and ensures that the method correctly sets the expected authentication type while also verifying the override behavior for different custom and default authentications.
2024-10-23 15:44:21 +02:00
c946f74ad2
Add unit tests for Client TLS, Username, and Password methods
Introduced unit tests for Client's SetTLSConfig, SetUsername, and SetPassword methods. The tests cover various scenarios, including setting valid configurations, handling nil inputs, and overriding previous settings. This improves our test coverage and ensures the reliability of these methods.
2024-10-23 15:31:00 +02:00
68bc5dde72
Add comprehensive tests for Client TLS and debugging features
Implemented detailed test cases for various TLS policies and settings, port configurations, and SSL options. Also added tests for debugging functionalities of the Client, enabling robust validation of different scenarios.
2024-10-23 15:15:59 +02:00
3efd2b529f
Set fallbackPort to 0 in SetTLSPortPolicy
The default fallback port should be reset to 0 when setting the TLS port policy. This ensures the fallbackPort does not retain an incorrect value when switching policies.
2024-10-23 14:39:28 +02:00
c5b57543c1
Update client tests to add context support and new checks
Introduced `context` package for new tests. Added checks for `WithDialContextFunc`, `WithLogAuthData`, and TLS policy assertions. Improved test failure handling and removed obsolete tests.
2024-10-23 14:08:19 +02:00
ab8fc3e4fc
Add DSN and dial context func options to client tests
Expanded the client test cases to include DSN mail return types and multiple DSN recipient notify types. Also added tests for setting dial context functions using both net.Dialer and tls.Dialer, including error handling for invalid options.
2024-10-23 13:51:48 +02:00
35f92f2ddc
Add error handling for nil DialContextFunc
Introduce ErrDialContextFuncIsNil to handle cases where the dial context function is not provided. Update WithDialContextFunc to return this error when a nil function is passed.
2024-10-23 13:51:31 +02:00
3251d74c36
Refactor client test to enhance coverage and clarity
Reorganized and refactored tests for `NewClient` by adding individual test cases for different client creation scenarios, including various options and validation. Improved clarity and coverage by isolating cases, enhancing error messages, and removing redundancy.
2024-10-23 13:23:01 +02:00
1c8b2904f5
Add error check for nil SMTP authentication method
Ensure that the SMTP authentication method is not nil by adding a corresponding error check in the WithSMTPAuthCustom function. Introduced a new error, ErrSMTPAuthMethodIsNil, to handle this validation.
2024-10-23 13:10:13 +02:00
ab835b7870
Uncomment seed data in FuzzBase64LineBreaker
The previously commented-out empty byte array is now uncommented to include an empty string as part of the test cases. This change ensures that the FuzzBase64LineBreaker function properly handles an empty input scenario.
2024-10-23 11:36:06 +02:00
0bac51746d
Add error handling for base64 decoding in test
This commit adds a check for errors when decoding base64 strings in the b64linebreaker_test.go file. If an error occurs, it logs a meaningful error message, improving the test's robustness and clarity.
2024-10-23 11:29:04 +02:00
b31c7cf3a7
Add licenses for logo SVG files
These files specify the copyright and licensing information for the logo assets. They ensure that our usage of these images complies with the CC-BY-ND-4.0 license.
2024-10-23 11:26:42 +02:00
854361090a
Add coverage files to .gitignore
This change ensures that coverage data files are not tracked by Git. The addition helps keep the repository clean from auto-generated coverage reports.
2024-10-23 11:21:03 +02:00
421451f179
Add base64-encoded logo.svg to testdata
This commit introduces a new file, logo.svg.base64, into the testdata directory. This encoded SVG logo will be useful for testing purposes.
2024-10-23 11:20:24 +02:00
bdfe13dc93
Overhauled Base64LineBreaker test suite
Improved the test suite for the Base64LineBreaker. Tests are now written more consistently. Maintainability and readability have been improved as well.
2024-10-23 11:19:58 +02:00
946d9888d6
Refactor error handling in Base64LineBreaker
Switched ErrNoOutWriter from a string constant to an error object for improved error handling consistency. This change enhances clarity and reduces the risk of error handling inconsistencies in the future.
2024-10-23 11:16:09 +02:00
2088796049
Remove testdata directory and extra newline from .gitignore
The testdata directory entry was incorrect and has been removed. Additionally, an extra newline at the end of the file was removed for consistency.
2024-10-23 11:15:32 +02:00
42 changed files with 12651 additions and 6964 deletions

View file

@ -1,23 +0,0 @@
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
#
# SPDX-License-Identifier: MIT
freebsd_task:
name: FreeBSD
matrix:
- name: FreeBSD 13.3
freebsd_instance:
image_family: freebsd-13-3
- name: FreeBSD 14.0
freebsd_instance:
image_family: freebsd-14-0
env:
TEST_SKIP_SENDMAIL: 1
pkginstall_script:
- pkg install -y go
test_script:
- go test -race -cover -shuffle=on ./...

221
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,221 @@
# SPDX-FileCopyrightText: 2024 The go-mail Authors
#
# SPDX-License-Identifier: MIT
name: CI
permissions:
contents: read
on:
push:
branches:
- main
pull_request:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
jobs:
codecov:
name: Test with Codecov coverage (${{ matrix.os }} / ${{ matrix.go }})
runs-on: ${{ matrix.os }}
concurrency:
group: ci-codecov-${{ matrix.os }}-${{ matrix.go }}
cancel-in-progress: true
strategy:
matrix:
os: [ubuntu-latest]
go: ['1.23']
env:
PERFORM_ONLINE_TEST: ${{ vars.PERFORM_ONLINE_TEST }}
PERFORM_UNIX_OPEN_WRITE_TESTS: ${{ vars.PERFORM_UNIX_OPEN_WRITE_TESTS }}
PERFORM_SENDMAIL_TESTS: ${{ vars.PERFORM_SENDMAIL_TESTS }}
TEST_HOST: ${{ secrets.TEST_HOST }}
TEST_USER: ${{ secrets.TEST_USER }}
TEST_PASS: ${{ secrets.TEST_PASS }}
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@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version: ${{ matrix.go }}
check-latest: true
- name: Install sendmail
run: |
sudo apt-get -y update >/dev/null && sudo apt-get -y upgrade >/dev/null && sudo DEBIAN_FRONTEND=noninteractive apt-get -y install nullmailer >/dev/null && which sendmail
- name: Run go test
if: success()
run: |
go test -race -shuffle=on --coverprofile=coverage.coverprofile --covermode=atomic ./...
- name: Upload coverage to Codecov
if: success()
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
lint:
name: golangci-lint (${{ matrix.go }})
runs-on: ubuntu-latest
concurrency:
group: ci-lint-${{ matrix.go }}
cancel-in-progress: true
strategy:
matrix:
go: ['1.23']
steps:
- name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
egress-policy: audit
- name: Setup go
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version: ${{ matrix.go }}
check-latest: true
- name: Checkout Code
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
- name: golangci-lint
uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1
with:
version: latest
dependency-review:
name: Dependency review
runs-on: ubuntu-latest
concurrency:
group: ci-dependency-review
cancel-in-progress: true
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: 'Dependency Review'
uses: actions/dependency-review-action@a6993e2c61fd5dc440b409aa1d6904921c5e1894 # v4.3.5
with:
base-ref: ${{ github.event.pull_request.base.sha || 'main' }}
head-ref: ${{ github.event.pull_request.head.sha || github.ref }}
govulncheck:
name: Go vulnerabilities check
runs-on: ubuntu-latest
concurrency:
group: ci-govulncheck
cancel-in-progress: true
steps:
- name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
egress-policy: audit
- name: Run govulncheck
uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4
test:
name: Test (${{ matrix.os }} / ${{ matrix.go }})
runs-on: ${{ matrix.os }}
concurrency:
group: ci-test-${{ matrix.os }}-${{ matrix.go }}
cancel-in-progress: true
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@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version: ${{ matrix.go }}
- name: Run go test
run: |
go test -race -shuffle=on ./...
test-fbsd:
name: Test on FreeBSD ${{ matrix.osver }}
runs-on: ubuntu-latest
concurrency:
group: ci-test-freebsd-${{ matrix.osver }}
cancel-in-progress: true
strategy:
matrix:
osver: ['14.1', '14.0', 13.4']
steps:
- name: Checkout Code
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master
- name: Run go test on FreeBSD
uses: vmactions/freebsd-vm@v1
with:
usesh: true
copyback: false
prepare: |
pkg install -y go
run: |
cd $GITHUB_WORKSPACE;
go test -race -shuffle=on ./...
reuse:
name: REUSE Compliance Check
runs-on: ubuntu-latest
concurrency:
group: ci-reuse
cancel-in-progress: true
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: REUSE Compliance Check
uses: fsfe/reuse-action@3ae3c6bdf1257ab19397fab11fd3312144692083 # v4.0.0
sonarqube:
name: Test with SonarQube review (${{ matrix.os }} / ${{ matrix.go }})
runs-on: ${{ matrix.os }}
concurrency:
group: ci-codecov-${{ matrix.go }}
cancel-in-progress: true
strategy:
matrix:
os: [ubuntu-latest]
go: ['1.23']
env:
PERFORM_ONLINE_TEST: ${{ vars.PERFORM_ONLINE_TEST }}
TEST_HOST: ${{ secrets.TEST_HOST }}
TEST_USER: ${{ secrets.TEST_USER }}
TEST_PASS: ${{ secrets.TEST_PASS }}
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@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version: ${{ matrix.go }}
check-latest: true
- name: Run go test
run: |
go test -shuffle=on -race --coverprofile=./cov.out ./...
- name: SonarQube scan
uses: sonarsource/sonarqube-scan-action@884b79409bbd464b2a59edc326a4b77dc56b2195 # master
if: success()
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
- name: SonarQube quality gate
uses: sonarsource/sonarqube-quality-gate-action@dc2f7b0dd95544cd550de3028f89193576e958b9 # master
timeout-minutes: 5
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

View file

@ -1,67 +0,0 @@
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
#
# SPDX-License-Identifier: CC0-1.0
name: Codecov workflow
on:
push:
branches:
- main
paths:
- '**.go'
- 'go.*'
- '.github/workflows/codecov.yml'
- 'codecov.yml'
pull_request:
branches:
- main
paths:
- '**.go'
- 'go.*'
- '.github/workflows/codecov.yml'
- 'codecov.yml'
env:
TEST_HOST: ${{ secrets.TEST_HOST }}
TEST_FROM: ${{ secrets.TEST_USER }}
TEST_ALLOW_SEND: "1"
TEST_SMTPAUTH_USER: ${{ secrets.TEST_USER }}
TEST_SMTPAUTH_PASS: ${{ secrets.TEST_PASS }}
TEST_SMTPAUTH_TYPE: "LOGIN"
TEST_ONLINE_SCRAM: "1"
TEST_HOST_SCRAM: ${{ secrets.TEST_HOST_SCRAM }}
TEST_USER_SCRAM: ${{ secrets.TEST_USER_SCRAM }}
TEST_PASS_SCRAM: ${{ secrets.TEST_PASS_SCRAM }}
permissions:
contents: read
jobs:
run:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
go: ['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@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version: ${{ matrix.go }}
- name: Install sendmail
if: matrix.go == '1.23' && matrix.os == 'ubuntu-latest'
run: |
sudo apt-get -y install sendmail; which sendmail
- name: Run Tests
run: |
go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
- name: Upload coverage to Codecov
if: success() && matrix.go == '1.23' && matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos

View file

@ -1,31 +0,0 @@
# SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
#
# SPDX-License-Identifier: CC0-1.0
# Dependency Review Action
#
# This Action will scan dependency manifest files that change as part of a Pull Request,
# surfacing known-vulnerable versions of the packages declared or updated in the PR.
# Once installed, if the workflow run is marked as required,
# PRs introducing known-vulnerable packages will be blocked from merging.
#
# Source repository: https://github.com/actions/dependency-review-action
name: 'Dependency Review'
on: [pull_request]
permissions:
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
egress-policy: audit
- name: 'Checkout Repository'
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- name: 'Dependency Review'
uses: actions/dependency-review-action@a6993e2c61fd5dc440b409aa1d6904921c5e1894 # v4.3.5

View file

@ -1,54 +0,0 @@
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
#
# SPDX-License-Identifier: CC0-1.0
name: golangci-lint
on:
push:
tags:
- v*
branches:
- main
pull_request:
permissions:
contents: read
# Optional: allow read access to pull request. Use with `only-new-issues` option.
# pull-requests: read
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
egress-policy: audit
- uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version: '1.23'
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- name: golangci-lint
uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1
with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: latest
# Optional: working directory, useful for monorepos
# working-directory: somedir
# Optional: golangci-lint command line arguments.
# args: --issues-exit-code=0
# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true
# Optional: if set to true then the all caching functionality will be complete disabled,
# takes precedence over all other caching options.
# skip-cache: true
# Optional: if set to true then the action don't cache or restore ~/go/pkg.
# skip-pkg-cache: true
# Optional: if set to true then the action don't cache or restore ~/.cache/go-build.
# skip-build-cache: true

View file

@ -1,21 +0,0 @@
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
#
# SPDX-License-Identifier: CC0-1.0
name: Govulncheck Security Scan
on: [push, pull_request]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
egress-policy: audit
- name: Run govulncheck
uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4

View file

@ -1,45 +0,0 @@
# 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@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version: ${{ matrix.go }}
- name: Run Tests
run: |
go test -race -shuffle=on ./...

View file

@ -1,23 +0,0 @@
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
#
# SPDX-License-Identifier: CC0-1.0
name: REUSE Compliance Check
on: [push, pull_request]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
egress-policy: audit
- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
- name: REUSE Compliance Check
uses: fsfe/reuse-action@3ae3c6bdf1257ab19397fab11fd3312144692083 # v4.0.0

View file

@ -1,56 +0,0 @@
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
#
# SPDX-License-Identifier: CC0-1.0
name: SonarQube
permissions:
contents: read
on:
push:
branches:
- main
paths:
- '**.go'
- 'go.*'
- '.github/workflows/sonarqube.yml'
pull_request:
branches:
- main
paths:
- '**.go'
- 'go.*'
- '.github/workflows/sonarqube.yml'
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
egress-policy: audit
- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version: '1.23'
- name: Run unit Tests
run: |
go test -shuffle=on -race --coverprofile=./cov.out ./...
- uses: sonarsource/sonarqube-scan-action@884b79409bbd464b2a59edc326a4b77dc56b2195 # master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
- uses: sonarsource/sonarqube-quality-gate-action@dc2f7b0dd95544cd550de3028f89193576e958b9 # master
timeout-minutes: 5
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

4
.gitignore vendored
View file

@ -56,4 +56,6 @@ crashlytics.properties
crashlytics-build.properties
fabric.properties
testdata
## Coverage data
coverage.coverprofile
coverage.html

View file

@ -13,8 +13,8 @@ import (
// 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"
// ErrNoOutWriter is the error returned when no io.Writer is set for Base64LineBreaker.
var ErrNoOutWriter = errors.New("no io.Writer set for Base64LineBreaker")
// Base64LineBreaker handles base64 encoding with the insertion of new lines after a certain number
// of characters.
@ -44,7 +44,7 @@ type Base64LineBreaker struct {
// - err: An error if one occurred during the write operation.
func (l *Base64LineBreaker) Write(data []byte) (numBytes int, err error) {
if l.out == nil {
err = errors.New(ErrNoOutWriter)
err = ErrNoOutWriter
return
}
if l.used+len(data) < MaxBodyLength {

View file

@ -5,487 +5,165 @@
package mail
import (
"bufio"
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"os"
"testing"
)
const logoB64 = `PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PCFE
T0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53
My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHdpZHRoPSIxMDAlIiBo
ZWlnaHQ9IjEwMCUiIHZpZXdCb3g9IjAgMCA2MDAgNjAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJo
dHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3Jn
LzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiIHhtbG5zOnNlcmlmPSJodHRwOi8vd3d3
LnNlcmlmLmNvbS8iIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZDtjbGlwLXJ1bGU6ZXZlbm9kZDtz
dHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGlt
aXQ6MS41OyI+PHJlY3QgaWQ9ItCc0L7QvdGC0LDQttC90LDRjy3QvtCx0LvQsNGB0YLRjDEiIHNl
cmlmOmlkPSLQnNC+0L3RgtCw0LbQvdCw0Y8g0L7QsdC70LDRgdGC0YwxIiB4PSIwIiB5PSIwIiB3
aWR0aD0iNjAwIiBoZWlnaHQ9IjYwMCIgc3R5bGU9ImZpbGw6bm9uZTsiLz48Zz48cGF0aCBkPSJN
NTg0LjcyNiw5OC4zOTVjLTAsLTkuNzM4IC03LjkwNywtMTcuNjQ1IC0xNy42NDUsLTE3LjY0NWwt
NTEwLjAyNywwYy05LjczOSwwIC0xNy42NDUsNy45MDcgLTE3LjY0NSwxNy42NDVsLTAsMzE5Ljc5
NGMtMCw5LjczOSA3LjkwNiwxNy42NDYgMTcuNjQ1LDE3LjY0Nmw1MTAuMDI3LC0wYzkuNzM4LC0w
IDE3LjY0NSwtNy45MDcgMTcuNjQ1LC0xNy42NDZsLTAsLTMxOS43OTRaIiBzdHlsZT0iZmlsbDoj
MmMyYzJjO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDoyLjFweDsiLz48cGF0aCBkPSJNNTg1LjEy
Niw5OC4zOTVjLTAsLTkuNzM4IC03LjkwNywtMTcuNjQ1IC0xNy42NDUsLTE3LjY0NWwtNDk0Ljgz
OSwwYy05LjczOCwwIC0xNy42NDUsNy45MDcgLTE3LjY0NSwxNy42NDVsMCwzMTkuNzk0YzAsOS43
MzkgNy45MDcsMTcuNjQ2IDE3LjY0NSwxNy42NDZsNDk0LjgzOSwtMGM5LjczOCwtMCAxNy42NDUs
LTcuOTA3IDE3LjY0NSwtMTcuNjQ2bC0wLC0zMTkuNzk0WiIgc3R5bGU9ImZpbGw6IzM3MzczNztz
dHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Mi4wN3B4OyIvPjxwYXRoIGQ9Ik01NjcuMjU5LDExNC40
NzJjLTAsLTguNzYgLTcuMTEyLC0xNS44NzIgLTE1Ljg3MiwtMTUuODcybC00NjIuNjUxLDBjLTgu
NzYsMCAtMTUuODcyLDcuMTEyIC0xNS44NzIsMTUuODcybDAsMjg3LjY0MWMwLDguNzYgNy4xMTIs
MTUuODcxIDE1Ljg3MiwxNS44NzFsNDYyLjY1MSwwYzguNzYsMCAxNS44NzIsLTcuMTExIDE1Ljg3
MiwtMTUuODcxbC0wLC0yODcuNjQxWiIgc3R5bGU9ImZpbGw6I2ZmZjJlYjtzdHJva2U6IzAwMDtz
dHJva2Utd2lkdGg6MS45cHg7Ii8+PHBhdGggZD0iTTIzNi4xNDUsNDM1Ljg3OGwtMjMuNDIsOTEu
MDA4bDEzMC43NjUsLTBsMjIuMzY1LC05MC43NTZsLTEyOS43MSwtMC4yNTJaIiBzdHlsZT0iZmls
bDojMmMyYzJjO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDoyLjQ3cHg7Ii8+PHBhdGggZD0iTTI0
NS4zOTcsNDM1Ljg3OGwtMjMuNDIxLDkxLjAwOGwxMzAuNzY2LC0wbDIyLjM2NSwtOTAuNzU2bC0x
MjkuNzEsLTAuMjUyWiIgc3R5bGU9ImZpbGw6IzM3MzczNztzdHJva2U6IzAwMDtzdHJva2Utd2lk
dGg6Mi40N3B4OyIvPjxwYXRoIGQ9Ik00NzIuNTg0LDUzMS4wOTNjLTAsLTIuODI3IC0yLjI5NSwt
NS4xMjEgLTUuMTIxLC01LjEyMWwtMjkyLjU0MSwtMGMtMi44MjYsLTAgLTUuMTIxLDIuMjk0IC01
LjEyMSw1LjEyMWwwLDEwLjI0M2MwLDIuODI2IDIuMjk1LDUuMTIxIDUuMTIxLDUuMTIxbDI5Mi41
NDEsLTBjMi44MjYsLTAgNS4xMjEsLTIuMjk1IDUuMTIxLC01LjEyMWwtMCwtMTAuMjQzWiIgc3R5
bGU9ImZpbGw6IzJjMmMyYztzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Mi40N3B4OyIvPjxnPjxw
YXRoIGQ9Ik0xOTAuMzc5LDQzMS40NTVjLTAsLTEuOTE2IC0xLjU1NiwtMy40NzIgLTMuNDcyLC0z
LjQ3MmwtNTQuNTYzLDBjLTEuOTE3LDAgLTMuNDcyLDEuNTU2IC0zLjQ3MiwzLjQ3MmwtMCw0Ni4z
MDljLTAsMS45MTYgMS41NTUsMy40NzIgMy40NzIsMy40NzJsNTQuNTYzLC0wYzEuOTE2LC0wIDMu
NDcyLC0xLjU1NiAzLjQ3MiwtMy40NzJsLTAsLTQ2LjMwOVoiIHN0eWxlPSJmaWxsOiNlZWFjMDE7
c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjMuMXB4OyIvPjxnPjxwYXRoIGQ9Ik0xMzkuOTk2LDQ0
Mi44NTNjLTAsMC40MiAtMC4wNzEsMC45MjIgMC4wNjMsMS4zMjNjMC41MywxLjU5MiAyLjc1LDEu
Mzg2IDQuMDk1LDEuMzg2YzIuMzk3LC0wIDQuNjc0LC0wLjEwOCA2LjQ4OSwtMS44MjdjMC40NSwt
MC40MjYgMC45NjYsLTAuOTQgMS4wNzIsLTEuNTc1YzAuMDYxLC0wLjM2NyAwLjEwMiwtMS4yMDgg
LTAuMTI2LC0xLjUxMmMtMC44MDMsLTEuMDcxIC0yLjg0NCwtMC4zNzMgLTMuNjU1LDAuMjUyYy0w
LjcxNCwwLjU1IC0wLjgyMiwxLjgyNiAtMC45NDUsMi42NDZjLTAuNDA2LDIuNzA1IDAuMzg3LDUu
MDMgMy4xNSw1Ljg1OWMzLjQwNSwxLjAyMSA2Ljc4NCwwLjA2IDEwLjA4NiwtMC44MDljMS41OTcs
LTAuNDIxIDMuNjYsLTAuNzcgNS4wMTcsLTEuNzJjMC40NTIsLTAuMzE2IDAuNDQ0LC0wLjI4NCAw
LjkwMSwtMC41NThjMS4yMDgsLTAuNzI1IDMuODY5LC0xLjkyNyAyLjQ1NywtMy41OTFjLTAuMTks
LTAuMjI0IC0xLjEzOSwtMC4yMzcgLTEuMjYsMC4xMjZjLTAuNDIsMS4yNTcgLTEuMzI2LDMuMzAy
IDAuNTY3LDMuNzhjMS4zOTgsMC4zNTMgMi45MTksMC4xODkgNC4zNDcsMC4xODljMC4xOTMsLTAg
MS40ODUsLTAuMjc5IDEuNzAxLC0wLjA2M2MwLjU1NSwwLjU1NSAxLjU0MywwLjgxMSAyLjMzMSwx
LjAwOGMwLjkzMiwwLjIzMyAxLjk0NCwwLjYwNiAyLjg5OSwwLjY5M2MwLjg2OSwwLjA3OSAxLjc5
LC0wLjA0NSAyLjY0NiwwLjEyNiIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Ut
d2lkdGg6MS45NnB4OyIvPjxwYXRoIGQ9Ik0xNDIuNzA1LDQ1Mi42ODFjMC4xMTksMC41NTEgMC44
OTMsMC42NjkgMS4zMTgsMC44NDZjMS41OTgsMC42NjYgMy41MTQsMC44NzQgNS4yMzQsMC45ODJj
Mi4zNzgsMC4xNDggNC43MDQsLTAuMTU5IDcuMDU3LC0wLjMxNWMxLjg2MSwtMC4xMjUgMy43NDYs
MC4xMTYgNS42MDcsLTBjMi4wNTEsLTAuMTI5IDQuMzIxLC0wLjM3NyA2LjM2NCwtMC4wNjNjMS4w
NzIsMC4xNjQgMi4wODksMC40OCAzLjE1LDAuNjkzIiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZToj
MDAwO3N0cm9rZS13aWR0aDoxLjk2cHg7Ii8+PHBhdGggZD0iTTE3Ni4zNDksNDU1LjA3NmMxLjEx
MywtMC4wMjEgMi4yMjYsLTAuMDQyIDMuMzQsLTAuMDYzIiBzdHlsZT0iZmlsbDpub25lO3N0cm9r
ZTojMDAwO3N0cm9rZS13aWR0aDoxLjk2cHg7Ii8+PHBhdGggZD0iTTE0NS4xNjIsNDU5Ljg2NGwy
MS42NzQsLTAiIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjEuOTZw
eDsiLz48L2c+PC9nPjxnPjxnPjxwYXRoIGQ9Ik03Ni4yNzMsMTIxLjQ5NGw0ODcuNTU3LC0wLjAx
NSIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6I2Y0ZGRkMDtzdHJva2Utd2lkdGg6NC45M3B4O3N0
cm9rZS1saW5lY2FwOnNxdWFyZTsiLz48cGF0aCBkPSJNMjMwLjk1MSwyMjQuMTc1Yy0wLjgwMiwt
My4zMjcgLTguMDU5LC03Ljk4NCAtMTEuMDA1LC04Ljk0NWMtMzUuODQ2LC0xMS42OTkgLTIwLjc4
MywyNy4zNzIgLTAuNjE4LDI0LjM0MiIgc3R5bGU9ImZpbGw6IzYxZTBlZTsiLz48cGF0aCBkPSJN
MjM0Ljk0NCwyMjMuMjEzYy0wLjM3OSwtMS41NzQgLTEuNTIyLC0zLjQzNiAtMy4yNzYsLTUuMTc0
Yy0zLjA2MSwtMy4wMzMgLTguMDE0LC01LjkyIC0xMC40NDgsLTYuNzE0Yy0xMy4xMzUsLTQuMjg3
IC0yMC42MTgsLTEuOTk0IC0yNC4wNzEsMS44MTljLTMuNjM4LDQuMDE3IC0zLjgzNiwxMC40OTIg
LTAuODI1LDE2LjY1MWMzLjk4MSw4LjE0MyAxMy4zMjEsMTUuMzg2IDIzLjYxNSwxMy44MzljMi4y
NDIsLTAuMzM3IDMuNzg4LC0yLjQzIDMuNDUxLC00LjY3MmMtMC4zMzcsLTIuMjQyIC0yLjQzLC0z
Ljc4OSAtNC42NzIsLTMuNDUyYy02LjY0NywwLjk5OSAtMTIuNDQ0LC00LjA2NSAtMTUuMDE0LC05
LjMyM2MtMC44MDEsLTEuNjM3IC0xLjI5MywtMy4zMDggLTEuMzAyLC00Ljg0N2MtMC4wMDUsLTEu
MDIgMC4xOTUsLTEuOTc0IDAuODM2LC0yLjY4MmMwLjgzMiwtMC45MTggMi4yMjUsLTEuMzU0IDQu
MTMzLC0xLjQ4MWMyLjg1MiwtMC4xOTEgNi41NjcsMC40MTIgMTEuMywxLjk1N2MxLjQ1NiwwLjQ3
NSA0LjE2LDIuMDk2IDYuMjU3LDMuODY5YzAuNjM2LDAuNTM3IDEuMjE1LDEuMDg4IDEuNjU4LDEu
NjM2YzAuMTUyLDAuMTg4IDAuMzMxLDAuMzI3IDAuMzcyLDAuNDk4YzAuNTMxLDIuMjA0IDIuNzUx
LDMuNTYyIDQuOTU1LDMuMDMxYzIuMjA0LC0wLjUzMSAzLjU2MiwtMi43NTIgMy4wMzEsLTQuOTU1
WiIvPjxwYXRoIGQ9Ik0zNDkuMzI2LDEyMy41MjdjMCwwIC0xNS4zNSwtMTMuODYyIC0xOC4xMTQs
MjIuODM0IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojNjFlMGVlO3N0cm9rZS13aWR0aDo2LjE2
cHg7Ii8+PHBhdGggZD0iTTI5OS42NjIsMTIzLjQ4NWMtMCwtMCAxMi42NzEsLTE1LjI2NSAyMC4x
MTIsMjEuODk4IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojNjFlMGVlO3N0cm9rZS13aWR0aDo2
LjE2cHg7Ii8+PHBhdGggZD0iTTMzNi4xMTEsMTA4Ljc3M2MtMCwwIC0xNi44MDgsLTkuMDk5IC0x
MC44ODIsMzIuMTI0IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojNjFlMGVlO3N0cm9rZS13aWR0
aDo2LjE2cHg7Ii8+PHBhdGggZD0iTTQxNy43OTYsMTg0LjQ1M2MzLjI4MiwwLjk3IDYuNTgzLC0x
LjI5IDguODg5LC0zLjM2MWMyOC4wNTUsLTI1LjE5NCAtMTMuMTk4LC0zMS41MzggLTIwLjY1Niwt
MTIuNTU5IiBzdHlsZT0iZmlsbDojNjFlMGVlOyIvPjxwYXRoIGQ9Ik00MTYuNjMyLDE4OC4zOTNj
NC42NTcsMS4zNzUgOS41MjYsLTEuMzA3IDEyLjc5NywtNC4yNDVjMTAuMjc5LC05LjIzIDEyLjA0
NiwtMTYuODQzIDEwLjQ4OCwtMjEuNzE2Yy0xLjY0NSwtNS4xNSAtNy4xNDIsLTguNTQ0IC0xMy45
NzgsLTguOTljLTkuMDExLC0wLjU4NyAtMTkuOTI3LDMuOTA0IC0yMy43MzMsMTMuNTg5Yy0wLjgz
LDIuMTEgMC4yMSw0LjQ5NiAyLjMyLDUuMzI1YzIuMTEsMC44MjkgNC40OTYsLTAuMjExIDUuMzI1
LC0yLjMyMWMyLjQ2LC02LjI1OSA5LjczLC04Ljc3NSAxNS41NTMsLTguMzk1YzEuODA3LDAuMTE3
IDMuNDg5LDAuNTE3IDQuODE3LDEuMjY5YzAuODcxLDAuNDk0IDEuNTg2LDEuMTMgMS44NzEsMi4w
MjNjMC4zNzUsMS4xNzIgMC4wNDksMi41ODggLTAuNzk1LDQuMjk4Yy0xLjI2NiwyLjU2MiAtMy42
NDksNS40NzcgLTcuMzU2LDguODA2Yy0wLjg2OCwwLjc4IC0xLjkwNywxLjYxOCAtMy4wNTQsMi4x
NDdjLTAuNjE2LDAuMjg0IC0xLjI2NSwwLjUyNyAtMS45MjgsMC4zMzFjLTIuMTc0LC0wLjY0MiAt
NC40NjEsMC42MDIgLTUuMTAzLDIuNzc2Yy0wLjY0MiwyLjE3NCAwLjYwMiw0LjQ2MSAyLjc3Niw1
LjEwM1oiLz48cGF0aCBkPSJNNDM2LjAxNCw0MTkuNjQ1YzMuNjAxLC0xMC45MzQgOS4wMDMsLTMx
LjYwOSA1LjY4MiwtNDkuNTYzYy0xNC45NTMsLTgwLjg0NyA3Ljg5NSwtOTguNTQ5IC01LjAzNSwt
MTQ3LjU5M2MtNC40OSwtMTcuMDI5IC0xMi4wNDksLTM2LjI1IC0yMy41NTQsLTQ5Ljc5MmMtMjUu
Mjg1LC0yOS43NjIgLTcwLjczNiwtNDIuOTEzIC0xMDguNzMsLTMyLjc4NWMtMTUuMjEzLDQuMDU1
IC0yOC42NjIsMTEuNzI3IC00MS45MjMsMjAuMDM0Yy00Ni4xMzYsMjguOSAtNTIuMjU5LDgzLjM3
NyAtNDIuNzkxLDEyOC4yNDhjNC4xOTEsMTkuODY0IDIzLjY3Myw0OS44OTcgMjIuNTg2LDcwLjE5
NGMtMS4zODcsMjUuOTEgLTAuNzY1LDQ0LjQyNiAzLjM3Niw2MC44ODQiIHN0eWxlPSJmaWxsOiM2
MWUwZWU7Ii8+PGNsaXBQYXRoIGlkPSJfY2xpcDEiPjxwYXRoIGQ9Ik00MzYuMDE0LDQxOS42NDVj
My42MDEsLTEwLjkzNCA5LjAwMywtMzEuNjA5IDUuNjgyLC00OS41NjNjLTE0Ljk1MywtODAuODQ3
IDcuODk1LC05OC41NDkgLTUuMDM1LC0xNDcuNTkzYy00LjQ5LC0xNy4wMjkgLTEyLjA0OSwtMzYu
MjUgLTIzLjU1NCwtNDkuNzkyYy0yNS4yODUsLTI5Ljc2MiAtNzAuNzM2LC00Mi45MTMgLTEwOC43
MywtMzIuNzg1Yy0xNS4yMTMsNC4wNTUgLTI4LjY2MiwxMS43MjcgLTQxLjkyMywyMC4wMzRjLTQ2
LjEzNiwyOC45IC01Mi4yNTksODMuMzc3IC00Mi43OTEsMTI4LjI0OGM0LjE5MSwxOS44NjQgMjMu
NjczLDQ5Ljg5NyAyMi41ODYsNzAuMTk0Yy0xLjM4NywyNS45MSAtMC43NjUsNDQuNDI2IDMuMzc2
LDYwLjg4NCIvPjwvY2xpcFBhdGg+PGcgY2xpcC1wYXRoPSJ1cmwoI19jbGlwMSkiPjxwYXRoIGQ9
Ik0zMTEuNzc2LDIzNi44MzVjMCwwIC0zLjA2OCwyNC4xOTkgLTMxLjU5NywyNC44MzhjLTI4LjUz
LDAuNjM5IC0zMi41OTEsLTI4LjI0NiAtMzIuNTkxLC0yOC4yNDZsNjQuMTg4LDMuNDA4WiIgc3R5
bGU9ImZpbGw6IzRmY2JlMDsiLz48cGF0aCBkPSJNNDMyLjUwOSwyMTYuMDIzYzAsLTAgLTMuMDY3
LDI0LjE5OCAtMzEuNTk3LDI0LjgzN2MtMjguNTMsMC42MzkgLTMyLjU5MSwtMjguMjQ2IC0zMi41
OTEsLTI4LjI0Nmw2NC4xODgsMy40MDlaIiBzdHlsZT0iZmlsbDojNGZjYmUwOyIvPjxwYXRoIGQ9
Ik0yODYuODQ1LDQxOC45NjFsMTI3Ljg4NiwtMGMwLjM4NiwtMS4yNjcgMC42NjYsLTIuNDMyIDAu
ODcsLTMuNDcxYzMuODY3LC0xOS43NDMgMy43MjksLTU3LjQ2OCAtMi40MTUsLTc3LjA5NWMtMi4x
MzMsLTYuODE2IC01Ljc5MywtMTQuNDkgLTExLjQ3NiwtMTkuODUyYy0xMi40OSwtMTEuNzg1IC02
NC4zOTgsLTkuMTgxIC04My41NjEsLTQuODM1Yy03LjY3MywxLjc0MSAtMTQuNDg3LDQuOTIyIC0y
MS4yMSw4LjM1OGMtMTQuOTExLDcuNjIgLTIwLjI1Myw1OC43MTIgLTEwLjA5NCw5Ni44OTVaIiBz
dHlsZT0iZmlsbDojYjdmOGZmOyIvPjxwYXRoIGQ9Ik0yNDYuNzExLDQxOC45NjFjLTUuMTA4LC0y
MC42MTMgLTYuNDksLTQyLjk3NSAtNS41NjIsLTYwLjg1NWMyLjEzNiwtNDEuMTMzIC0zMS4zMTQs
LTUwLjQ1MyAtMjUuNDYxLC0xMjEuNTEzYzE0LjUzNCwtNDIuNDA5IDE1LjA4OSwtNDAuMTA3IDI3
LjY1MywtNTUuNDQ1YzAsMCAtMTMuMTQ3LDM5Ljk2OCAtMS4xOTMsNzAuNzI4YzExLjk1NCwzMC43
NTkgMzIuMTc0LDczLjQxOCAzMi41OSwxMTcuNDkxYzAuMTg2LDE5Ljc3MSAxLjYzNCwzNi4zMSA0
LjEzLDQ5LjU5NGwtMzIuMTU3LC0wWiIgc3R5bGU9ImZpbGw6IzRmY2JlMDsiLz48L2c+PHBhdGgg
ZD0iTTQzOS45MTUsNDIwLjkzYzMuNzQ4LC0xMS4zOCA5LjI3NiwtMzIuOTA4IDUuODIsLTUxLjU5
NWMtOC44NjUsLTQ3LjkzMyAtNC4yNTksLTczLjQwNSAtMS45MzUsLTk2LjU4YzEuNjEzLC0xNi4w
OTMgMi4xNTYsLTMxLjEyMiAtMy4xNjcsLTUxLjMxM2MtNC42MzgsLTE3LjU5MSAtMTIuNTExLC0z
Ny40MTYgLTI0LjM5NiwtNTEuNDA1Yy0yNi4yNjEsLTMwLjkxMSAtNzMuNDU2LC00NC42MTMgLTEx
Mi45MTcsLTM0LjA5NGMtMTUuNjE0LDQuMTYxIC0yOS40MzcsMTEuOTk2IC00My4wNDcsMjAuNTIy
Yy00Ny43MzUsMjkuOTAyIC01NC40MjYsODYuMTUgLTQ0LjYyOSwxMzIuNTc3YzIuMTM5LDEwLjEz
OCA4LjEzNiwyMi44ODggMTMuNTA0LDM1LjY3NmM0Ljk5NCwxMS45IDkuNTE1LDIzLjgxIDguOTk5
LDMzLjQ1Yy0xLjQxNSwyNi40MzIgLTAuNzMsNDUuMzE3IDMuNDk1LDYyLjEwN2wxLjAwMiwzLjk4
M2w3Ljk2NywtMi4wMDVsLTEuMDAyLC0zLjk4M2MtNC4wNTgsLTE2LjEyNyAtNC42MTgsLTM0LjI3
NCAtMy4yNTgsLTU5LjY2M2MwLjQ2LC04LjYwOSAtMi40NiwtMTguODk1IC02LjU0NSwtMjkuNDU3
Yy01LjY5NCwtMTQuNzI0IC0xMy42NDYsLTMwLjA1OSAtMTYuMTI0LC00MS44MDRjLTkuMTQsLTQz
LjMxNSAtMy41ODQsLTk2LjAyMiA0MC45NTIsLTEyMy45MTljMTIuOTEyLC04LjA4OSAyNS45ODks
LTE1LjU5OCA0MC44MDEsLTE5LjU0N2MzNi41MjYsLTkuNzM1IDgwLjIzNCwyLjg2NSAxMDQuNTQx
LDMxLjQ3NmMxMS4xMjYsMTMuMDk2IDE4LjM3MiwzMS43MTMgMjIuNzEzLDQ4LjE4YzYuMzEsMjMu
OTMyIDMuODIsNDAuMjE5IDEuNjQ3LDYwLjM4M2MtMi4yNTcsMjAuOTQ5IC00LjI1OSw0NS45Mjcg
My4zMjEsODYuOTFjMy4xODYsMTcuMjIyIC0yLjA5LDM3LjA0MyAtNS41NDQsNDcuNTMxbC0xLjI4
NSwzLjkwMmw3LjgwMywyLjU2OWwxLjI4NCwtMy45MDFaIi8+PHBhdGggZD0iTTQyNC41MzksMjQy
LjI0N2MtMS44NjgsMC4xMzQgLTMuMjc2LDEuNzU4IC0zLjE0MiwzLjYyNmMwLjEzMywxLjg2NyAx
Ljc1NywzLjI3NSAzLjYyNSwzLjE0MWMxLjg2NywtMC4xMzMgMy4yNzUsLTEuNzU3IDMuMTQyLC0z
LjYyNWMtMC4xMzQsLTEuODY3IC0xLjc1OCwtMy4yNzUgLTMuNjI1LC0zLjE0MloiIHN0eWxlPSJm
aWxsOiNiN2Y4ZmY7Ii8+PHBhdGggZD0iTTI1Ni45OTcsMjYwLjM5MWMtMS44NjgsMC4xMzMgLTMu
Mjc1LDEuNzU4IC0zLjE0MiwzLjYyNWMwLjEzMywxLjg2OCAxLjc1OCwzLjI3NSAzLjYyNSwzLjE0
MmMxLjg2OCwtMC4xMzMgMy4yNzUsLTEuNzU4IDMuMTQyLC0zLjYyNWMtMC4xMzMsLTEuODY4IC0x
Ljc1OCwtMy4yNzUgLTMuNjI1LC0zLjE0MloiIHN0eWxlPSJmaWxsOiNiN2Y4ZmY7Ii8+PGc+PHBh
dGggZD0iTTM2NC4wOSwyNjkuMjI5Yy0xLjE4Niw0Ljk2NyAtMS40NDMsMTQuMzA2IC00LjMxNSwx
OC40Yy0xLjM5MywxLjk4NSAtNC40OTUsMi4wODYgLTYuNTE2LDIuMTUzYy0xMS42MTIsMC4zODMg
LTExLjQ0NiwtMTEuNTEzIC0xMy4xODEsLTIxLjg1NSIgc3R5bGU9ImZpbGw6I2VkZWRlZDsiLz48
cGF0aCBkPSJNMzYxLjA5NCwyNjguNTEzYy0wLjcxOSwzLjAwOSAtMS4xMDYsNy42MDcgLTEuOTIy
LDExLjY5OGMtMC40NDUsMi4yMjggLTAuOTcxLDQuMjk4IC0xLjkxOSw1LjY0OWMtMC4yNjMsMC4z
NzYgLTAuNzQ5LDAuNDU5IC0xLjIwMywwLjU2NmMtMC45OTUsMC4yMzMgLTIuMDU2LDAuMjUgLTIu
ODkzLDAuMjc3Yy0yLjA1NSwwLjA2OCAtMy42MSwtMC4zMjYgLTQuNzc0LC0xLjE5MmMtMS4xOTMs
LTAuODg3IC0xLjk1NCwtMi4yMTIgLTIuNTQ0LC0zLjc0M2MtMS41NjEsLTQuMDQ3IC0xLjg5LC05
LjM5IC0yLjcyMywtMTQuMzUxYy0wLjI4MSwtMS42NzcgLTEuODcxLC0yLjgxIC0zLjU0OCwtMi41
MjhjLTEuNjc3LDAuMjgxIC0yLjgwOSwxLjg3MSAtMi41MjgsMy41NDhjMC45MDMsNS4zODEgMS4z
NTgsMTEuMTU4IDMuMDUxLDE1LjU0OGMxLjAzNiwyLjY4OCAyLjUyLDQuOTEyIDQuNjE1LDYuNDdj
Mi4xMjUsMS41ODEgNC45MDMsMi41MyA4LjY1NCwyLjQwNmMxLjQ2OCwtMC4wNDggMy40LC0wLjE1
MiA1LjA3OSwtMC43MTRjMS41NiwtMC41MjIgMi45MTksLTEuNDEgMy44NTgsLTIuNzQ5YzEuMzUs
LTEuOTI0IDIuMjg0LC00LjgwOCAyLjkxNywtNy45ODFjMC44LC00LjAxMSAxLjE2OCwtOC41MjIg
MS44NzMsLTExLjQ3M2MwLjM5NSwtMS42NTMgLTAuNjI3LC0zLjMxNyAtMi4yODEsLTMuNzEyYy0x
LjY1NCwtMC4zOTUgLTMuMzE3LDAuNjI3IC0zLjcxMiwyLjI4MVoiLz48cGF0aCBkPSJNMzQ5LjAw
MSwyMzQuMDVjLTkuMzMyLDEuMjQ2IC0xNi4yNjUsNy4wNzUgLTE1LjQ3MywxMy4wMDhjMC43OTIs
NS45MzMgOS4wMTIsOS43MzggMTguMzQ0LDguNDkyYzkuMzMzLC0xLjI0NiAxNi4yNjYsLTcuMDc1
IDE1LjQ3NCwtMTMuMDA4Yy0wLjc5MiwtNS45MzMgLTkuMDEyLC05LjczOCAtMTguMzQ1LC04LjQ5
MloiLz48cGF0aCBkPSJNMzQ4LjkwMSwyNTIuNTVjMC40Niw1LjIwNCAxLjY5NCwxMC4xNDIgMi40
NTMsMTUuMjc3YzAuMjQ4LDEuNjgyIDEuODE1LDIuODQ2IDMuNDk3LDIuNTk4YzEuNjgyLC0wLjI0
OCAyLjg0NiwtMS44MTYgMi41OTgsLTMuNDk4Yy0wLjc0MSwtNS4wMTUgLTEuOTYxLC05LjgzNyAt
Mi40MTEsLTE0LjkyYy0wLjE1LC0xLjY5NCAtMS42NDcsLTIuOTQ3IC0zLjM0LC0yLjc5N2MtMS42
OTQsMC4xNSAtMi45NDcsMS42NDcgLTIuNzk3LDMuMzRaIi8+PHBhdGggZD0iTTM3NC4yNiwyNTku
ODE0Yy04LjAyMiw1LjM1IC0xNS45MDEsNy44MjUgLTIzLjU5NCw3LjI3NGMtNy42OTQsLTAuNTUx
IC0xNS4xNDgsLTQuMTI0IC0yMi4zNDIsLTEwLjU4OWMtMS4yNjUsLTEuMTM2IC0zLjIxNCwtMS4w
MzIgLTQuMzUxLDAuMjMzYy0xLjEzNiwxLjI2NCAtMS4wMzIsMy4yMTQgMC4yMzMsNC4zNWM4LjM1
Niw3LjUwOCAxNy4wODQsMTEuNTExIDI2LjAyLDEyLjE1MWM4LjkzNiwwLjY0IDE4LjEzNSwtMi4w
NzggMjcuNDUzLC04LjI5NGMxLjQxNCwtMC45NDMgMS43OTcsLTIuODU4IDAuODUzLC00LjI3MmMt
MC45NDMsLTEuNDE1IC0yLjg1OCwtMS43OTcgLTQuMjcyLC0wLjg1M1oiLz48cGF0aCBkPSJNMzc4
LjYwNSwxNTIuMTU2Yy0zLjY0NywtMC44MjYgLTYuOTU2LDAuMDM4IC03LjM4NSwxLjkyOWMtMC40
MjgsMS44OTEgMi4xODUsNC4wOTcgNS44MzIsNC45MjRjMy42NDcsMC44MjcgNi45NTYsLTAuMDM3
IDcuMzg1LC0xLjkyOGMwLjQyOCwtMS44OTEgLTIuMTg1LC00LjA5OCAtNS44MzIsLTQuOTI1WiIg
c3R5bGU9ImZpbGw6I2I3ZjhmZjsiLz48cGF0aCBkPSJNMjQ5LjUsMTc3LjI3Yy0yLjE5NSwzLjAy
OCAtMi43MDIsNi40MSAtMS4xMzIsNy41NDhjMS41NywxLjEzOCA0LjYyNiwtMC4zOTYgNi44MjEs
LTMuNDI0YzIuMTk1LC0zLjAyOCAyLjcwMiwtNi40MSAxLjEzMiwtNy41NDhjLTEuNTcsLTEuMTM4
IC00LjYyNiwwLjM5NiAtNi44MjEsMy40MjRaIiBzdHlsZT0iZmlsbDojYjdmOGZmOyIvPjxwYXRo
IGQ9Ik00MDMuNjQ4LDE2Mi44MjVjLTE5LjIxMywtMS4zNSAtMzUuOTA3LDEzLjE1MSAtMzcuMjU3
LDMyLjM2NGMtMS4zNSwxOS4yMTIgMTMuMTUyLDM1LjkwNiAzMi4zNjQsMzcuMjU2YzE5LjIxMywx
LjM1MSAzNS45MDcsLTEzLjE1MSAzNy4yNTcsLTMyLjM2NGMxLjM1LC0xOS4yMTIgLTEzLjE1Miwt
MzUuOTA2IC0zMi4zNjQsLTM3LjI1NloiIHN0eWxlPSJmaWxsOiNmZmY7c3Ryb2tlOiMwMDA7c3Ry
b2tlLXdpZHRoOjYuMTZweDsiLz48cGF0aCBkPSJNMjgxLjY5NiwxODEuNzhjLTE5LjIxMiwtMS4z
NSAtMzUuOTA2LDEzLjE1MiAtMzcuMjU3LDMyLjM2NGMtMS4zNSwxOS4yMTIgMTMuMTUyLDM1Ljkw
NyAzMi4zNjUsMzcuMjU3YzE5LjIxMiwxLjM1IDM1LjkwNiwtMTMuMTUyIDM3LjI1NiwtMzIuMzY0
YzEuMzUsLTE5LjIxMyAtMTMuMTUyLC0zNS45MDcgLTMyLjM2NCwtMzcuMjU3WiIgc3R5bGU9ImZp
bGw6I2ZmZjtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Ni4xNnB4OyIvPjxwYXRoIGQ9Ik0zOTgu
NDk5LDE5Mi44ODFjLTYuODI0LC0wLjQ3OSAtMTIuNzU0LDQuNjcyIC0xMy4yMzMsMTEuNDk2Yy0w
LjQ4LDYuODI0IDQuNjcxLDEyLjc1MyAxMS40OTUsMTMuMjMzYzYuODI0LDAuNDc5IDEyLjc1NCwt
NC42NzIgMTMuMjMzLC0xMS40OTZjMC40OCwtNi44MjQgLTQuNjcxLC0xMi43NTMgLTExLjQ5NSwt
MTMuMjMzWiIvPjxwYXRoIGQ9Ik0yNzguMTQyLDIxMC4yMjdjLTYuODI0LC0wLjQ3OSAtMTIuNzU0
LDQuNjcxIC0xMy4yMzMsMTEuNDk2Yy0wLjQ4LDYuODI0IDQuNjcxLDEyLjc1MyAxMS40OTUsMTMu
MjMzYzYuODI0LDAuNDc5IDEyLjc1NCwtNC42NzIgMTMuMjMzLC0xMS40OTZjMC40OCwtNi44MjQg
LTQuNjcxLC0xMi43NTMgLTExLjQ5NSwtMTMuMjMzWiIvPjxwYXRoIGQ9Ik0zOTkuODkyLDE5Ny45
NjFjLTEuOTE2LC0wLjEzNSAtMy41ODEsMS4zMTEgLTMuNzE2LDMuMjI3Yy0wLjEzNCwxLjkxNiAx
LjMxMiwzLjU4MSAzLjIyOCwzLjcxNmMxLjkxNiwwLjEzNCAzLjU4MSwtMS4zMTIgMy43MTUsLTMu
MjI4YzAuMTM1LC0xLjkxNiAtMS4zMTEsLTMuNTgxIC0zLjIyNywtMy43MTVaIiBzdHlsZT0iZmls
bDojZmZmOyIvPjxwYXRoIGQ9Ik0yNzkuNTM1LDIxNS4zMDZjLTEuOTE2LC0wLjEzNCAtMy41ODEs
MS4zMTIgLTMuNzE1LDMuMjI4Yy0wLjEzNSwxLjkxNiAxLjMxMSwzLjU4MSAzLjIyNywzLjcxNmMx
LjkxNiwwLjEzNCAzLjU4MSwtMS4zMTIgMy43MTYsLTMuMjI4YzAuMTM0LC0xLjkxNiAtMS4zMTIs
LTMuNTgxIC0zLjIyOCwtMy43MTZaIiBzdHlsZT0iZmlsbDojZmZmOyIvPjwvZz48L2c+PHBhdGgg
ZD0iTTUxMi4yOTEsMzMzLjcyNmMwLC0wIC0yNy40OTYsMTMuNDAyIC00My4yNDUsMy43OTJjLTE1
Ljc1LC05LjYxIC0xMC44LC0yNC43ODggMy4xMDIsLTIyLjUyYzEzLjkwMywyLjI2OCAyNy4xNjgs
My4xOTUgMzUuOTQzLC0zLjkzN2M4Ljc3NiwtNy4xMzIgMjEuNjMzLDE0LjM3NiA0LjIsMjIuNjY1
WiIgc3R5bGU9ImZpbGw6I2Q2YzI5NTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Ni4xNnB4OyIv
PjwvZz48Zz48cGF0aCBkPSJNNDQxLjI2OCwyOTEuMzQ5Yy0wLDAgMjguMDgsNDcuNDg4IC0xMjIu
NTkxLDExNC4wNGMtMTEuNjI5LC0yLjAxNyAtMTQuMzMsLTguNTE1IC0xNC4zMywtOC41MTVjMCwt
MCA0My42MTcsLTEyLjU4NiA4My44MTIsLTQ0Ljg3NmM0My41OTcsLTM1LjAyNCA1My4xMDksLTYw
LjY0OSA1My4xMDksLTYwLjY0OVoiIHN0eWxlPSJmaWxsOiM3YjQ3MjI7c3Ryb2tlOiMwMDA7c3Ry
b2tlLXdpZHRoOjYuNjhweDsiLz48cGF0aCBkPSJNMjMzLjk1NywzNzIuMTM4YzMuNTYsLTUuNTA2
IDEwLjcyOSwtNy4zOTIgMTYuNTM4LC00LjM1MWMxNy43NjcsOS4yMzUgNTMuNjE0LDI4LjAzMiA2
Ny4xNTQsMzYuMzc2YzUuOTcxLDMuNjggLTYuNDc3LDE2LjkxMyAtMTkuMjgzLDM3LjkyN2MtMy4z
NjgsNS41NjcgLTEwLjQzNiw3LjYyOCAtMTYuMjY5LDQuNzQ2Yy0yMS42NTYsLTEwLjcxNiAtNDEu
MDQ3LC0yMC45NTggLTU0LjIzNCwtMjguMDczYy00LjU5MiwtMi41MDEgLTcuOTM0LC02LjgwMiAt
OS4yMjMsLTExLjg2OWMtMS4yODgsLTUuMDY4IC0wLjQwNywtMTAuNDQzIDIuNDMzLC0xNC44MzNj
NC4zMzUsLTYuNzA4IDkuMTYyLC0xNC4xNyAxMi44ODQsLTE5LjkyM1oiIHN0eWxlPSJmaWxsOiM3
YjQ3MjI7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48cGF0aCBkPSJNMjM5Ljg1
LDM2MS4xNzFsLTI1Ljg0OCwyMy4yOTZsNzkuMTcsNDUuNjA1bDI1LjE5NiwtMjQuMjk1bC03OC41
MTgsLTQ0LjYwNloiIHN0eWxlPSJmaWxsOiNhZDZjNGE7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRo
OjQuOTVweDsiLz48cGF0aCBkPSJNMjM4LjA4MSwzNzcuNzA0YzIuNTI2LDAuNTMgNC4xNDcsMy4w
MTEgMy42MTgsNS41MzdjLTAuNTMsMi41MjYgLTMuMDExLDQuMTQ3IC01LjUzOCwzLjYxOGMtMi41
MjYsLTAuNTMgLTQuMTQ3LC0zLjAxMSAtMy42MTcsLTUuNTM4YzAuNTMsLTIuNTI2IDMuMDExLC00
LjE0NyA1LjUzNywtMy42MTdaIiBzdHlsZT0iZmlsbDojZmZiMjVjO3N0cm9rZTojMDAwO3N0cm9r
ZS13aWR0aDo0Ljk1cHg7Ii8+PHBhdGggZD0iTTI5MC4zMyw0MDcuOWMyLjUyNywwLjUzIDQuMTQ3
LDMuMDExIDMuNjE4LDUuNTM3Yy0wLjUzLDIuNTI3IC0zLjAxMSw0LjE0NyAtNS41MzcsMy42MThj
LTIuNTI3LC0wLjUzIC00LjE0OCwtMy4wMTEgLTMuNjE4LC01LjUzN2MwLjUzLC0yLjUyNyAzLjAx
MSwtNC4xNDggNS41MzcsLTMuNjE4WiIgc3R5bGU9ImZpbGw6I2ZmYjI1YztzdHJva2U6IzAwMDtz
dHJva2Utd2lkdGg6NC45NXB4OyIvPjwvZz48Zz48cGF0aCBkPSJNMzA1LjMyMywzMzYuMjM1YzAs
MCAyNC4yMDQsMTEyLjU3OCAzNS4zNTcsMTE0LjM1NWMxMS4xNTMsMS43NzYgMTgyLjk5OCwtNTcu
MTYyIDE4Mi45OTgsLTU3LjE2MmMwLC0wIC0yNC4yNywtMTA1Ljc3MyAtMzQuNDU2LC0xMTQuNjAy
Yy0xMC4xODYsLTguODMgLTE4My44OTksNTcuNDA5IC0xODMuODk5LDU3LjQwOVoiIHN0eWxlPSJm
aWxsOiNmZmYyZWI7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuNjhweDsiLz48cGF0aCBkPSJN
NDEzLjU2MywzNjcuNjU2bC0xMDguMjQsLTMxLjQyMWwxMDguMjQsMzEuNDIxWiIgc3R5bGU9ImZp
bGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6NS40NnB4OyIvPjxwYXRoIGQ9Ik00ODgu
NTY2LDI3OS4xNTZsLTc1LjAwMyw4OC41IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwO3N0
cm9rZS13aWR0aDo1LjQ2cHg7Ii8+PHBhdGggZD0iTTMzOS4xNjcsNDQ5LjEyN2w2Mi40MDEsLTgz
LjUwNSIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6NS40NnB4OyIv
PjxwYXRoIGQ9Ik00MjMuMjcsMzU1LjgzNWw5OS4xOTQsMzcuMDAxIiBzdHlsZT0iZmlsbDpub25l
O3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo1LjQ2cHg7Ii8+PC9nPjxwYXRoIGQ9Ik0zMDQuNDE2
LDM3NC43ODVjLTAsMCAtMzAuNDYsLTIuOCAtMzguOTQzLC0xOS4xODVjLTguNDgyLC0xNi4zODQg
My42MjIsLTI2Ljc5NCAxNC4zMzIsLTE3LjY0NmMxMC43MTEsOS4xNDkgMjEuNTcyLDE2LjgyMSAz
Mi43NzQsMTUuMjc0YzExLjIwMSwtMS41NDcgMTEuMDQsMjMuNTExIC04LjE2MywyMS41NTdaIiBz
dHlsZT0iZmlsbDojZDZjMjk1O3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo2LjE2cHg7Ii8+PHBh
dGggZD0iTTEwOS45NTEsMzExLjcxMmw2OC4wNzYsLTAiIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tl
OiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48cGF0aCBkPSJNMTM5LjMwNSwzNTEuNjgzbDY4
LjA3NiwwIiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo0Ljk1cHg7
Ii8+PHBhdGggZD0iTTEzOS42MTcsMzMxLjY5OGwzOC4wOTgsLTAiIHN0eWxlPSJmaWxsOm5vbmU7
c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48cGF0aCBkPSJNODUuMjAxLDIxMS4y
NTNjLTAsLTIuMjEzIC0xLjc5NywtNC4wMSAtNC4wMSwtNC4wMWwtNTMuNDg3LC0wYy0yLjIxNCwt
MCAtNC4wMTEsMS43OTcgLTQuMDExLDQuMDFsMCw1OC44OTRjMCwyLjIxMyAxLjc5Nyw0LjAxIDQu
MDExLDQuMDFsNTMuNDg3LDBjMi4yMTMsMCA0LjAxLC0xLjc5NyA0LjAxLC00LjAxbC0wLC01OC44
OTRaIiBzdHlsZT0iZmlsbDojZWVhYzAxO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDozLjQzcHg7
Ii8+PHBhdGggZD0iTTU2NC42NDMsNDI2LjU5MWMwLC0yLjIxMyAtMS43OTYsLTQuMDEgLTQuMDEs
LTQuMDFsLTUzLjQ4NywtMGMtMi4yMTMsLTAgLTQuMDEsMS43OTcgLTQuMDEsNC4wMWwwLDU4Ljg5
NGMwLDIuMjEzIDEuNzk3LDQuMDEgNC4wMSw0LjAxbDUzLjQ4NywwYzIuMjE0LDAgNC4wMSwtMS43
OTcgNC4wMSwtNC4wMWwwLC01OC44OTRaIiBzdHlsZT0iZmlsbDojZWVhYzAxO3N0cm9rZTojMDAw
O3N0cm9rZS13aWR0aDozLjQzcHg7Ii8+PGNpcmNsZSBjeD0iNTE3LjAxNCIgY3k9IjEwOC43NzEi
IHI9IjQuMzUyIiBzdHlsZT0iZmlsbDojZDNkMmIzOyIvPjxjaXJjbGUgY3g9IjUzMS4zMiIgY3k9
IjEwOC43NzEiIHI9IjQuMzUyIiBzdHlsZT0iZmlsbDojZjRiMzAzOyIvPjxjaXJjbGUgY3g9IjU0
NC43OTIiIGN5PSIxMDguNzcxIiByPSI0LjM1MiIgc3R5bGU9ImZpbGw6I2Y5NjEzZDsiLz48cGF0
aCBkPSJNMTA1LjI1Nyw0ODQuNDNjLTAsMCAyMC4zNzQsLTI1LjIyNSA0MS43MTksLTcuNzYxYzIx
LjM0NCwxNy40NjMgLTEyLjYxMyw1MC40NSAtNDAuNzQ5LDQyLjY4OSIgc3R5bGU9ImZpbGw6bm9u
ZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6OS45cHg7Ii8+PHBhdGggZD0iTTExMy4wMTksNDYw
LjE0MWMtMCwtNC4yNjUgLTMuNDYzLC03LjcyNyAtNy43MjgsLTcuNzI3bC03OC42NTUsLTBjLTQu
MjY1LC0wIC03LjcyNywzLjQ2MiAtNy43MjcsNy43MjdsLTAsNzguNjU1Yy0wLDQuMjY1IDMuNDYy
LDcuNzI4IDcuNzI3LDcuNzI4bDc4LjY1NSwtMGM0LjI2NSwtMCA3LjcyOCwtMy40NjMgNy43Mjgs
LTcuNzI4bC0wLC03OC42NTVaIiBzdHlsZT0iZmlsbDojNjFlMGVlO3N0cm9rZTojMDAwO3N0cm9r
ZS13aWR0aDo0LjU1cHg7c3Ryb2tlLWxpbmVjYXA6c3F1YXJlOyIvPjxwYXRoIGQ9Ik03Ny4yNzcs
Mzk1LjQwMmMtMi4yMzQsOC43NTUgLTYuNDk2LDE1LjM2NiAtMTEuNjQ5LDE3LjcxNGMxLjk5NSwz
LjA1NyAzLjE2Nyw2Ljc4MSAzLjE2NywxMC43OTdjMCwxMC4zODcgLTcuODQsMTguODIgLTE3LjQ5
OCwxOC44MmMtOS42NTcsMCAtMTcuNDk4LC04LjQzMyAtMTcuNDk4LC0xOC44MmMwLC05LjczMyA2
Ljg4NCwtMTcuNzUxIDE1LjY5NywtMTguNzIyYy0zLjM5OSwtNS45NiAtNS41MjQsLTE0LjQyMyAt
NS41MjQsLTIzLjgwNWMwLC0xOC4wMjQgNy44NDEsLTMyLjY1NyAxNy40OTksLTMyLjY1N2M2Ljc5
MiwtMCAxMi42ODUsNy4yMzggMTUuNTg0LDE3LjgwMmMxLjYyOSwtMC41OTIgMy4zODcsLTAuOTE1
IDUuMjIsLTAuOTE1YzguNDUsMCAxNS4zMTEsNi44NjEgMTUuMzExLDE1LjMxMWMtMCw4LjQ1IC02
Ljg2MSwxNS4zMTEgLTE1LjMxMSwxNS4zMTFjLTEuNzUsMCAtMy40MzIsLTAuMjk0IC00Ljk5OCwt
MC44MzZaIiBzdHlsZT0iZmlsbDojZmZmO2ZpbGwtb3BhY2l0eTowLjU7Ii8+PHBhdGggZD0iTTMz
LjU4MywyMzMuOTNjMi41MjksLTAuMzM3IDQuNTEsLTEuMTc4IDYuNzE2LC0yLjQwM2MwLjk1Nywt
MC41MzIgMS45NjksLTAuOTA0IDIuODk4LC0xLjQ4NWMwLjc4OSwtMC40OTMgMS4zNDMsLTAuOTc2
IDEuMzQzLC0xLjk3OWMwLC0wLjIzNiAwLjIyNCwtMC42MzIgMCwtMC43MDdjLTAuOTUyLC0wLjMx
NyAtMi4zNjQsMC40NjUgLTMuMDQsMC45OWMtMC42MjUsMC40ODYgLTEuNDk0LDAuNTc5IC0yLjEy
LDEuMDZjLTEuMDM2LDAuNzk2IC0xLjY4MiwyLjQ2MyAtMS43NjcsMy43NDZjLTAuMDYsMC44OTYg
LTAuMzU3LDIuODQgMC41NjUsMy4zOTNjMC4zNTcsMC4yMTQgMC45MjksMC4xNDIgMS4zNDMsMC4x
NDJjMS4wOCwtMCAyLjEwNCwwLjAxMiAzLjE4MSwtMC4xNDJjMi4wODIsLTAuMjk3IDQuMTY1LC0x
LjQ1NSA2LjAwOSwtMi40MDNjMi4wMTQsLTEuMDM2IDQuMDEyLC0xLjkyMSA1LjU4NCwtMy42MDVj
MC42NDIsLTAuNjg4IDEuMDEsLTEuMjg2IDEuMjcyLC0yLjE5MWMwLjA1OSwtMC4yMDMgLTAuMDYs
LTAuNzE4IDAuMDcxLC0wLjg0OWMwLjEwOCwtMC4xMDggMC4wOTksMC4yMjYgMC4wNzEsMC4yODNj
LTAuMTg1LDAuMzY3IC0wLjQ2MiwwLjc0IC0wLjYzNywxLjEzMWMtMC4zMjQsMC43MjkgLTAuMzUz
LDEuNzU5IC0wLjM1MywyLjU0NWMwLDAuMTMyIC0wLjA3LDAuNTcxIDAsMC42MzZjMC4wNTUsMC4w
NTEgMC4zNjMsMC4xOTMgMC40MjQsMC4yMTJjMi4wMDIsMC42MzMgNC4zNDIsLTAuNTMyIDYuMDA5
LC0xLjQ4NGMwLjY0MSwtMC4zNjcgMS4zNjMsLTAuODUgMi4wNiwtMS4xMTJjMC4wMjYsLTAuMDEg
MC42NzgsLTAuMTk3IDAuNjk2LC0wLjE2MWMwLjI4MiwwLjU2MyAwLjExMSwxLjUxMSAwLjIxMiwy
LjEyMWMwLjI4NSwxLjcwNyAyLjM2NywxLjYxNSAzLjY3NiwxLjQ4NGMwLjIwNywtMC4wMiAwLjM2
MywtMC4yMTIgMC41NjYsLTAuMjEyIiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwO3N0cm9r
ZS13aWR0aDoyLjc1cHg7Ii8+PHBhdGggZD0iTTM2LjQ4MiwyNDIuMzQyYzEuMzQ2LC0wIDIuNjgy
LC0wLjE1NCA0LjAyOSwtMC4yMTJjNC4zMzIsLTAuMTg5IDguNjcxLC0wLjE5NiAxMy4wMDYsLTAu
MjgzYzYuNTI1LC0wLjEzMSAxMy4wNTYsMC4wNzEgMTkuNTgxLDAuMDcxIiBzdHlsZT0iZmlsbDpu
b25lO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDoyLjc1cHg7Ii8+PHBhdGggZD0iTTUxNi4wOSw0
NDIuMDMxYy0wLDAuOTM2IC0wLjUzMiwxLjg5MiAtMC4xMjYsMi44MzljMC41MjYsMS4yMjggMi4z
NjMsMS4yNzUgMy40NjksMC45NDZjMC42MjgsLTAuMTg3IDEuMzE2LC0xLjE3NiAxLjcwMywtMS42
NGMxLjAzOCwtMS4yNDcgMi40MzUsLTIuMzc1IDIuNzEyLC00LjAzN2MwLjA0MSwtMC4yNDggMC4x
OTQsLTAuOTM2IDAuMDYzLC0xLjE5OGMtMC43NjMsLTEuNTI1IC0zLjA1OSwtMS40NzggLTQuMTY4
LC0wLjM2OWMtMC4xNjQsMC4xNjQgLTAuNDQ2LDAuMjI3IC0wLjU2MywwLjQzMmMtMC45OTQsMS43
NDUgLTAuNDM5LDQuMzQxIDAuNDQyLDUuOTkyYzAuMzA0LDAuNTY5IDAuODI5LDEuMDQ4IDEuMzI0
LDEuNDUxYzAuMzM5LDAuMjc1IDAuNzc1LDAuNzE2IDEuMTk5LDAuOTI4YzMuMjYyLDEuNjMxIDcu
Njg5LC0wLjEzMyAxMC41OTYsLTEuNzQ4YzEuOTUyLC0xLjA4NSA0LjEsLTIuMTUyIDQuMSwtNC42
NjhjLTAsLTAuMzI1IDAuMDM3LC0wLjY5NCAtMC4wNjMsLTEuMDA5Yy0wLjM2MSwtMS4xMzMgLTIu
MzA3LC0xLjA0MyAtMy4xNTQsLTAuNjk0Yy0xLjE3OSwwLjQ4NiAtMi4zNzMsMS4zOTUgLTMuMDks
Mi40NmMtMS4xNzQsMS43NDIgLTEuMjA3LDQuMjgzIDAuNTY3LDUuNjE0YzEuMjU2LDAuOTQxIDIu
OTU4LDAuOTIzIDQuNDQ3LDAuNzY2YzIuMjQ3LC0wLjIzNyA0LjI1MywtMS4zMTMgNi4wMjMsLTIu
NjU5YzAuODc1LC0wLjY2NCAyLjA5NCwtMS4zMTYgMi42NDksLTIuMzMzYzAuMTczLC0wLjMxNiAw
LjIxNSwtMC43NDUgMC4zNzksLTEuMDczYzAuMDcxLC0wLjE0MyAtMC4xNTgsMC4yODUgLTAuMTg5
LDAuNDQyYy0wLjA1NiwwLjI3NiAtMC4wOTQsMC44NzggLTAsMS4xMzVjMC4zNzYsMS4wMjggMS4y
MzMsMS41MDkgMi4yNywxLjcwM2MxLjM0NiwwLjI1MiAzLjAwOSwtMC4wMSA0LjM1MiwtMC4xODlj
MC45MzksLTAuMTI1IDEuODkyLC0wLjUxNyAxLjg5MiwwLjc1NyIgc3R5bGU9ImZpbGw6bm9uZTtz
dHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Mi40NXB4OyIvPjxwYXRoIGQ9Ik01MTQuMjU5LDQ2MC4z
NTljMC4yNjYsMC4xMzQgMC4wOCwxLjM1NCAwLjU2NywxLjM4OGMwLjQ0MiwwLjAzMSAwLjg4NSww
LjA0IDEuMzI4LDAuMDVjMC41MTcsMC4wMTIgMS4xMDQsMC4wOTggMS42MzMsLTAuMDM0YzEuNjg4
LC0wLjQyMiAzLjc5NiwtMS41OTggNC40ODIsLTMuMjk2YzAuMTg4LC0wLjQ2NyAwLjI0NiwtMS4w
OTQgMC4zMTUsLTEuNTc3YzAuMDc1LC0wLjUyNCAwLjM3OCwtMS43MTggLTAuMDYzLC0yLjE0NGMt
MC4wODgsLTAuMDg1IC0wLjYwNCwtMC4wNjMgLTAuNjk0LC0wLjA2M2MtMC42OTUsLTAgLTEuNzk2
LDAuMzkyIC0yLjIwNywxLjAwOWMtMC4yNzMsMC40MDkgLTAuMTg5LDAuODUxIC0wLjE4OSwxLjMy
NGMtMCwwLjYyIC0wLjAzMywxLjIyNCAwLjEyNiwxLjgzYzAuMzYyLDEuMzc5IDIuMTExLDEuOTQx
IDMuMzQzLDIuMDE4YzIuNjk2LDAuMTY4IDUuNDQ3LC0wLjI0NSA4LjEzNiwtMGMyLjIwNywwLjIg
NC40NTQsMC43MTcgNi42ODYsMC41MDRjMS41OCwtMC4xNSAzLjE1NCwtMC41MjUgNC43MywtMC42
M2MzLjM5NCwtMC4yMjYgNi43OTEsLTAuNTA1IDEwLjIxOCwtMC41MDUiIHN0eWxlPSJmaWxsOm5v
bmU7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjIuNDVweDsiLz48cGF0aCBkPSJNNTE5LjA1NCw0
NzEuNjhsMCwwLjk0NmMwLDAuMTU1IC0wLDAuMjUxIDAuMDYzLDAuMzc5bDAsMC4xMjZjMC42MDIs
LTIuNDA3IDMuNzc1LC0yLjc4NCA1LjgxLC0yLjc4NGMwLjE1LC0wIDAuMjMyLDAuNDggMC4yNDUs
MC41NzZjMC4wNTUsMC4zODUgLTAuMTkyLDEuNDUgMC4zNzksMS42NGMwLjk4OSwwLjMzIDIuNDA2
LC0wLjMxOCAzLjM0MywtMC41NjhjMC4yMzUsLTAuMDYyIDEuMDA5LC0wLjQ0MSAxLjE5OCwtMC4y
NTJjMC45NTQsMC45NTQgMi40MTIsMC41NjIgMy43MjEsMC42MzFjMy4zMzIsMC4xNzUgNi42Mjks
LTAuMTg5IDkuOTY2LC0wLjE4OWMxLjA1OCwtMCAyLjE2MiwwLjA4NyAzLjIxNywtMGMwLjA2OCwt
MC4wMDYgMS4wMDksLTAuMDI3IDEuMDA5LC0wLjI1MyIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6
IzAwMDtzdHJva2Utd2lkdGg6Mi40NXB4OyIvPjwvZz48Zz48cGF0aCBkPSJNMzc0LjU3OCwxMTQu
NDU1Yy0wLjYxMywtNC4yMTYgLTQuNTMzLC03LjE0MiAtOC43NDksLTYuNTNsLTk5Ljk0NSwxNC41
MTVjLTQuMjE2LDAuNjEzIC03LjE0Miw0LjUzMyAtNi41Myw4Ljc0OWwyLjIxOSwxNS4yNzhjMC42
MTIsNC4yMTYgNC41MzIsNy4xNDIgOC43NDksNi41M2w5OS45NDUsLTE0LjUxNWM0LjIxNiwtMC42
MTIgNy4xNDIsLTQuNTMzIDYuNTI5LC04Ljc0OWwtMi4yMTgsLTE1LjI3OFoiIHN0eWxlPSJmaWxs
OiM5ZGFjY2Q7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjUuMjFweDsiLz48cGF0aCBkPSJNMzEz
Ljg2OSwxMTcuNDE0YzAsMCA2Ni4xMDIsOS44NzggNzcuNTgyLC0yMy43MzNjMTEuNDgsLTMzLjYx
MSAtMTIuOTg3LC01MS44NzMgLTQxLjYzNywtMzkuOTIxYy0yOC42NSwxMS45NTIgLTU0Ljg1LDQ2
LjE0MiAtNTQuODUsNDYuMTQyYzAsMCAtMzAuMDA1LC04LjEwOCAtNDEuMzE0LDUuMjIxYy0xMS4z
MDksMTMuMzMgLTE3LjE2NywyNi42NDYgLTIuNDM1LDMxLjUxOWMxNC43MzIsNC44NzMgNjIuNjU0
LC0xOS4yMjggNjIuNjU0LC0xOS4yMjhaIiBzdHlsZT0iZmlsbDojNzM1ZDkzO3N0cm9rZTojMDAw
O3N0cm9rZS13aWR0aDo1LjkzcHg7Ii8+PHBhdGggZD0iTTM3NC4xNDgsMTM2Ljg3NmMtMCwwIDM4
LjQ5MywtOS4zMzggMzYuNTYzLC0zLjU0OWMtMS45Myw1Ljc5IC0zNy42MzgsMjYuOTAxIC0xMTQu
ODMxLDE1Ljk2NmM3MC4zNDUsLTEwLjczMSA3OC4yNjgsLTEyLjQxNyA3OC4yNjgsLTEyLjQxN1oi
IHN0eWxlPSJmaWxsOiM1MDNhNzE7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48
cGF0aCBkPSJNMzU3LjM0NiwxMjQuOTA2bC01NC4wMjEsOS42MTJsLTAsNS4zOTlsNTQuMDIxLC05
LjYxMmwwLC01LjM5OVoiIHN0eWxlPSJmaWxsOiNmZmY7Ii8+PGNpcmNsZSBjeD0iMzAwLjE1IiBj
eT0iMTM3LjE5OCIgcj0iNC42NzciIHN0eWxlPSJmaWxsOiNmZmIyNWM7c3Ryb2tlOiMwMDA7c3Ry
b2tlLXdpZHRoOjQuOTVweDsiLz48Y2lyY2xlIGN4PSIzNjEuNDEiIGN5PSIxMjcuNTgxIiByPSI0
LjI4MiIgc3R5bGU9ImZpbGw6I2ZmYjI1YztzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6NC45NXB4
OyIvPjwvZz48L3N2Zz4=
`
var (
errMockDefault = errors.New("mock write error")
errMockNewline = errors.New("mock newline error")
errClosedWriter = errors.New("writer is already closed")
errMockDefault = errors.New("mock write error")
errMockNewline = errors.New("mock newline error")
)
// TestBase64LineBreaker tests the Write and Close methods of the Base64LineBreaker
func TestBase64LineBreaker(t *testing.T) {
l, err := os.Open("assets/gopher2.svg")
if err != nil {
t.Errorf("failed to open gopher logo asset: %s", err)
return
}
defer func() { _ = l.Close() }()
var wbuf bytes.Buffer
lb := Base64LineBreaker{out: &wbuf}
bw := base64.NewEncoder(base64.StdEncoding, &lb)
if _, err := io.Copy(bw, l); err != nil {
t.Errorf("failed to write logo asset to line breaker: %s", err)
return
}
if err := bw.Close(); err != nil {
t.Errorf("failed to close b64 encoder: %s", err)
}
if err := lb.Close(); err != nil {
t.Errorf("failed to close line breaker: %s", err)
}
ob := removeNewLines([]byte(logoB64))
nb := removeNewLines(wbuf.Bytes())
if string(ob) != string(nb) {
t.Errorf("generated line breaker output differs from original data")
}
}
// TestBase64LineBreakerFailures tests the cases in which the Base64LineBreaker would fail
func TestBase64LineBreakerFailures(t *testing.T) {
stt := []byte("short")
ltt := []byte(logoB64)
// No output writer defined
lb := Base64LineBreaker{}
if _, err := lb.Write(stt); err == nil {
t.Errorf("writing to Base64LineBreaker with no output io.Writer was supposed to failed, but didn't")
}
if err := lb.Close(); err != nil {
t.Errorf("failed to close Base64LineBreaker: %s", err)
}
// Closed output writer
wbuf := errorWriter{}
fb := Base64LineBreaker{out: wbuf}
if _, err := fb.Write(ltt); err == nil {
t.Errorf("writing to Base64LineBreaker with errorWriter was supposed to failed, but didn't")
}
if err := fb.Close(); err != nil {
t.Errorf("failed to close Base64LineBreaker: %s", err)
}
}
func TestBase64LineBreaker_WriteAndClose(t *testing.T) {
tests := []struct {
name string
data []byte
writer io.Writer
}{
{
name: "Write data within MaxBodyLength",
data: []byte("testdata"),
writer: &mockWriterExcess{writeError: errMockDefault},
},
{
name: "Write data exceeds MaxBodyLength",
data: []byte("verylongtestdataverylongtestdataverylongtestdata" +
"verylongtestdataverylongtestdataverylongtestdata"),
writer: &mockWriterExcess{writeError: errMockDefault},
},
{
name: "Write data exceeds MaxBodyLength with newline",
data: []byte("verylongtestdataverylongtestdataverylongtestdata" +
"verylongtestdataverylongtestdataverylongtestdata"),
writer: &mockWriterNewline{writeError: errMockDefault},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
blr := &Base64LineBreaker{out: tt.writer}
_, err := blr.Write(tt.data)
if err != nil && !errors.Is(err, errMockDefault) && !errors.Is(err, errMockNewline) {
t.Errorf("Unexpected error while writing: %v", err)
}
err = blr.Close()
if err != nil && !errors.Is(err, errMockDefault) && !errors.Is(err, errMockNewline) {
t.Errorf("Unexpected error while closing: %v", err)
t.Run("write, copy and close", func(t *testing.T) {
logoWriter := bytes.NewBuffer(nil)
lineBreaker := &Base64LineBreaker{out: logoWriter}
t.Cleanup(func() {
if err := lineBreaker.Close(); err != nil {
t.Errorf("failed to close line breaker: %s", err)
}
})
}
if _, err := lineBreaker.Write([]byte("testdata")); err != nil {
t.Errorf("failed to write to line breaker: %s", err)
}
})
t.Run("write actual data and compare with expected results", func(t *testing.T) {
logo, err := os.Open("testdata/logo.svg")
if err != nil {
t.Fatalf("failed to open test data file: %s", err)
}
t.Cleanup(func() {
if err := logo.Close(); err != nil {
t.Errorf("failed to close test data file: %s", err)
}
})
logoWriter := bytes.NewBuffer(nil)
lineBreaker := &Base64LineBreaker{out: logoWriter}
t.Cleanup(func() {
if err := lineBreaker.Close(); err != nil {
t.Errorf("failed to close line breaker: %s", err)
}
})
base64Encoder := base64.NewEncoder(base64.StdEncoding, lineBreaker)
t.Cleanup(func() {
if err := base64Encoder.Close(); err != nil {
t.Errorf("failed to close base64 encoder: %s", err)
}
})
copiedBytes, err := io.Copy(base64Encoder, logo)
if err != nil {
t.Errorf("failed to copy test data to line breaker: %s", err)
}
if err = base64Encoder.Close(); err != nil {
t.Errorf("failed to close base64 encoder: %s", err)
}
if err = lineBreaker.Close(); err != nil {
t.Errorf("failed to close line breaker: %s", err)
}
logoStat, err := os.Stat("testdata/logo.svg")
if err != nil {
t.Fatalf("failed to stat test data file: %s", err)
}
if logoStat.Size() != copiedBytes {
t.Errorf("copied %d bytes, but expected %d bytes", copiedBytes, logoStat.Size())
}
expectedRaw, err := os.ReadFile("testdata/logo.svg.base64")
if err != nil {
t.Errorf("failed to read expected base64 data from file: %s", err)
}
expected := removeNewLines(t, expectedRaw)
got := removeNewLines(t, logoWriter.Bytes())
if !bytes.EqualFold(expected, got) {
t.Errorf("generated line breaker output differs from expected data")
}
})
t.Run("fail with no writer defined", func(t *testing.T) {
lineBreaker := &Base64LineBreaker{}
_, err := lineBreaker.Write([]byte("testdata"))
if err == nil {
t.Errorf("writing to Base64LineBreaker with no output io.Writer was supposed to failed, but didn't")
}
if !errors.Is(err, ErrNoOutWriter) {
t.Errorf("unexpected error while writing to empty Base64LineBreaker: %s", err)
}
if err := lineBreaker.Close(); err != nil {
t.Errorf("failed to close Base64LineBreaker: %s", err)
}
})
t.Run("write on an already closed output writer", func(t *testing.T) {
logo, err := os.Open("testdata/logo.svg")
if err != nil {
t.Fatalf("failed to open test data file: %s", err)
}
t.Cleanup(func() {
if err := logo.Close(); err != nil {
t.Errorf("failed to close test data file: %s", err)
}
})
writeBuffer := &errorWriter{}
lineBreaker := &Base64LineBreaker{out: writeBuffer}
_, err = io.Copy(lineBreaker, logo)
if err == nil {
t.Errorf("writing to Base64LineBreaker with an already closed output io.Writer was " +
"supposed to failed, but didn't")
}
if !errors.Is(err, errClosedWriter) {
t.Errorf("unexpected error while writing to Base64LineBreaker: %s", err)
}
})
t.Run("fail on different scenarios with mock writer", func(t *testing.T) {
tests := []struct {
name string
data []byte
writer io.Writer
}{
{
name: "write data within MaxBodyLength",
data: []byte("testdata"),
writer: &mockWriterExcess{writeError: errMockDefault},
},
{
name: "write data exceeds MaxBodyLength",
data: []byte("verylongtestdataverylongtestdataverylongtestdata" +
"verylongtestdataverylongtestdataverylongtestdata"),
writer: &mockWriterExcess{writeError: errMockDefault},
},
{
name: "write data exceeds MaxBodyLength with newline",
data: []byte("verylongtestdataverylongtestdataverylongtestdata" +
"verylongtestdataverylongtestdataverylongtestdata"),
writer: &mockWriterNewline{writeError: errMockDefault},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
lineBreaker := &Base64LineBreaker{out: tt.writer}
_, err := lineBreaker.Write(tt.data)
if err != nil && !errors.Is(err, errMockDefault) && !errors.Is(err, errMockNewline) {
t.Errorf("unexpected error while writing to mock writer: %s", err)
}
err = lineBreaker.Close()
if err != nil && !errors.Is(err, errMockDefault) && !errors.Is(err, errMockNewline) {
t.Errorf("unexpected error while closing mock writer: %s", err)
}
})
}
})
}
// removeNewLines removes any newline characters from the given data
func removeNewLines(data []byte) []byte {
// removeNewLines is a test helper thatremoves all newline characters ('\r' and '\n') from the given byte slice.
func removeNewLines(t *testing.T, data []byte) []byte {
t.Helper()
result := make([]byte, len(data))
n := 0
@ -503,11 +181,11 @@ func removeNewLines(data []byte) []byte {
type errorWriter struct{}
func (e errorWriter) Write([]byte) (int, error) {
return 0, fmt.Errorf("supposed to always fail")
return 0, errClosedWriter
}
func (e errorWriter) Close() error {
return fmt.Errorf("supposed to always fail")
return errClosedWriter
}
type mockWriterExcess struct {
@ -539,19 +217,49 @@ func (w *mockWriterNewline) Write(p []byte) (n int, err error) {
}
}
func FuzzBase64LineBreaker_Write(f *testing.F) {
f.Add([]byte("abc"))
f.Add([]byte("def"))
f.Add([]uint8{0o0, 0o1, 0o2, 30, 255})
buf := bytes.Buffer{}
bw := bufio.NewWriter(&buf)
func FuzzBase64LineBreaker(f *testing.F) {
seedData := [][]byte{
[]byte(""),
[]byte("abc"),
[]byte("def"),
[]byte("Hello, World!"),
[]byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!§$%&/()=?`{[]}\\|^~*+#-._'"),
[]byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"),
bytes.Repeat([]byte("A"), MaxBodyLength-1), // Near the line length limit
bytes.Repeat([]byte("A"), MaxBodyLength), // Exactly the line length limit
bytes.Repeat([]byte("A"), MaxBodyLength+1), // Slightly above the line length limit
bytes.Repeat([]byte("A"), MaxBodyLength*3), // Tripple exceed the line length limit
bytes.Repeat([]byte("A"), MaxBodyLength*10), // Tenfold exceed the line length limit
{0o0, 0o1, 0o2, 30, 255},
}
for _, data := range seedData {
f.Add(data)
}
f.Fuzz(func(t *testing.T, data []byte) {
b := &Base64LineBreaker{out: bw}
if _, err := b.Write(data); err != nil {
t.Errorf("failed to write to B64LineBreaker: %s", err)
buffer := bytes.NewBuffer(nil)
lineBreaker := &Base64LineBreaker{
out: buffer,
}
if err := b.Close(); err != nil {
t.Errorf("failed to close B64LineBreaker: %s", err)
base64Encoder := base64.NewEncoder(base64.StdEncoding, lineBreaker)
_, err := base64Encoder.Write(data)
if err != nil {
t.Errorf("failed to write test data to base64 encoder: %s", err)
}
if err = base64Encoder.Close(); err != nil {
t.Errorf("failed to close base64 encoder: %s", err)
}
if err = lineBreaker.Close(); err != nil {
t.Errorf("failed to close base64 line breaker: %s", err)
}
decode, err := base64.StdEncoding.DecodeString(buffer.String())
if err != nil {
t.Errorf("failed to decode base64 data: %s", err)
}
if !bytes.Equal(data, decode) {
t.Error("generated line breaker output differs from original data")
}
})
}

View file

@ -242,6 +242,12 @@ var (
// provided as argument to the WithDSN Option.
ErrInvalidDSNRcptNotifyCombination = errors.New("DSN rcpt notify option NEVER cannot be " +
"combined with any of SUCCESS, FAILURE or DELAY")
// ErrSMTPAuthMethodIsNil indicates that the SMTP authentication method provided is nil
ErrSMTPAuthMethodIsNil = errors.New("SMTP auth method is nil")
// ErrDialContextFuncIsNil indicates that a required dial context function is not provided.
ErrDialContextFuncIsNil = errors.New("dial context function is nil")
)
// NewClient creates a new Client instance with the provided host and optional configuration Option functions.
@ -510,6 +516,9 @@ func WithSMTPAuth(authtype SMTPAuthType) Option {
// - An Option function that sets the custom SMTP authentication for the Client.
func WithSMTPAuthCustom(smtpAuth smtp.Auth) Option {
return func(c *Client) error {
if smtpAuth == nil {
return ErrSMTPAuthMethodIsNil
}
c.smtpAuth = smtpAuth
c.smtpAuthType = SMTPAuthCustom
return nil
@ -671,6 +680,9 @@ func WithoutNoop() Option {
// - An Option function that sets the custom DialContextFunc for the Client.
func WithDialContextFunc(dialCtxFunc DialContextFunc) Option {
return func(c *Client) error {
if dialCtxFunc == nil {
return ErrDialContextFuncIsNil
}
c.dialContextFunc = dialCtxFunc
return nil
}
@ -739,6 +751,7 @@ func (c *Client) SetTLSPolicy(policy TLSPolicy) {
func (c *Client) SetTLSPortPolicy(policy TLSPolicy) {
if c.port == DefaultPort {
c.port = DefaultPortTLS
c.fallbackPort = 0
if policy == TLSOpportunistic {
c.fallbackPort = DefaultPort
@ -1081,10 +1094,6 @@ func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) e
// - An error if the connection check fails, if no supported authentication method is found,
// or if the authentication process fails.
func (c *Client) auth() error {
if err := c.checkConn(); err != nil {
return fmt.Errorf("failed to authenticate: %w", err)
}
if c.smtpAuth == nil && c.smtpAuthType != SMTPAuthNoAuth {
hasSMTPAuth, smtpAuthType := c.smtpClient.Extension("AUTH")
if !hasSMTPAuth {
@ -1252,14 +1261,13 @@ func (c *Client) sendSingleMsg(message *Msg) error {
affectedMsg: message,
}
}
message.isDelivered = true
if err = writer.Close(); err != nil {
return &SendError{
Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err),
affectedMsg: message,
}
}
message.isDelivered = true
if err = c.Reset(); err != nil {
return &SendError{
@ -1267,12 +1275,6 @@ func (c *Client) sendSingleMsg(message *Msg) error {
affectedMsg: message,
}
}
if err = c.checkConn(); err != nil {
return &SendError{
Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err),
affectedMsg: message,
}
}
return nil
}
@ -1289,6 +1291,9 @@ func (c *Client) sendSingleMsg(message *Msg) error {
// - An error if there is no active connection, if the NOOP command fails, or if extending
// the deadline fails; otherwise, returns nil.
func (c *Client) checkConn() error {
if c.smtpClient == nil {
return ErrNoActiveConnection
}
if !c.smtpClient.HasConnection() {
return ErrNoActiveConnection
}
@ -1347,9 +1352,6 @@ func (c *Client) setDefaultHelo() error {
// - An error if there is no active connection, if STARTTLS is required but not supported,
// or if there are issues during the TLS handshake; otherwise, returns nil.
func (c *Client) tls() error {
if !c.smtpClient.HasConnection() {
return ErrNoActiveConnection
}
if !c.useSSL && c.tlspolicy != NoTLS {
hasStartTLS := false
extension, _ := c.smtpClient.Extension("STARTTLS")

126
client_121_test.go Normal file
View file

@ -0,0 +1,126 @@
// SPDX-FileCopyrightText: 2024 The go-mail Authors
//
// SPDX-License-Identifier: MIT
//go:build go1.21
// +build go1.21
package mail
import (
"bytes"
"context"
"errors"
"fmt"
"net"
"os"
"reflect"
"strings"
"testing"
"time"
"github.com/wneessen/go-mail/log"
)
func TestNewClientNewVersionsOnly(t *testing.T) {
tests := []struct {
name string
option Option
expectFunc func(c *Client) error
shouldfail bool
expectErr *error
}{
{
"WithLogger log.JSONlog", WithLogger(log.NewJSON(os.Stderr, log.LevelDebug)),
func(c *Client) error {
if c.logger == nil {
return errors.New("failed to set logger. Want logger bug got got nil")
}
loggerType := reflect.TypeOf(c.logger).String()
if loggerType != "*log.JSONlog" {
return fmt.Errorf("failed to set logger. Want logger type: %s, got: %s",
"*log.JSONlog", loggerType)
}
return nil
},
false, nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, err := NewClient(DefaultHost, tt.option)
if !tt.shouldfail && err != nil {
t.Fatalf("failed to create new client: %s", err)
}
if tt.shouldfail && err == nil {
t.Errorf("client creation was supposed to fail, but it didn't")
}
if tt.shouldfail && tt.expectErr != nil {
if !errors.Is(err, *tt.expectErr) {
t.Errorf("error for NewClient mismatch. Expected: %s, got: %s",
*tt.expectErr, err)
}
}
if tt.expectFunc != nil {
if err = tt.expectFunc(client); err != nil {
t.Errorf("NewClient with custom option failed: %s", err)
}
}
})
}
}
func TestClient_DialWithContextNewVersionsOnly(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
go func() {
if err := simpleSMTPServer(ctx, t, &serverProps{FeatureSet: featureSet, ListenPort: serverPort}); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 30)
t.Run("connect with full debug logging and auth logging", func(t *testing.T) {
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
t.Cleanup(cancelDial)
logBuffer := bytes.NewBuffer(nil)
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS),
WithDebugLog(), WithLogAuthData(), WithLogger(log.NewJSON(logBuffer, log.LevelDebug)),
WithSMTPAuth(SMTPAuthPlain), WithUsername("test"), WithPassword("password"))
if err != nil {
t.Fatalf("failed to create new client: %s", err)
}
if err = client.DialWithContext(ctxDial); err != nil {
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
t.Skip("failed to connect to the test server due to timeout")
}
t.Fatalf("failed to connect to the test server: %s", err)
}
t.Cleanup(func() {
if err = client.Close(); err != nil {
t.Errorf("failed to close the client: %s", err)
}
})
logs := parseJSONLog(t, logBuffer)
if len(logs.Lines) == 0 {
t.Errorf("failed to enable debug logging, but no logs were found")
}
authFound := false
for _, logline := range logs.Lines {
if strings.EqualFold(logline.Message, "AUTH PLAIN AHRlc3QAcGFzc3dvcmQ=") &&
logline.Direction.From == "client" && logline.Direction.To == "server" {
authFound = true
}
}
if !authFound {
t.Errorf("logAuthData not working, no authentication info found in logs")
}
})
}

File diff suppressed because it is too large Load diff

View file

@ -6,17 +6,17 @@ coverage:
status:
project:
default:
target: 85%
threshold: 5%
target: 90%
threshold: 2%
base: auto
if_ci_failed: error
only_pulls: false
patch:
default:
target: 80%
target: 90%
base: auto
if_ci_failed: error
threshold: 5%
threshold: 2%
comment:
require_changes: true

12
eml.go
View file

@ -60,7 +60,7 @@ func EMLToMsgFromReader(reader io.Reader) (*Msg, error) {
return msg, fmt.Errorf("failed to parse EML from reader: %w", err)
}
if err := parseEML(parsedMsg, bodybuf, msg); err != nil {
if err = parseEML(parsedMsg, bodybuf, msg); err != nil {
return msg, fmt.Errorf("failed to parse EML contents: %w", err)
}
@ -93,7 +93,7 @@ func EMLToMsgFromFile(filePath string) (*Msg, error) {
return msg, fmt.Errorf("failed to parse EML file: %w", err)
}
if err := parseEML(parsedMsg, bodybuf, msg); err != nil {
if err = parseEML(parsedMsg, bodybuf, msg); err != nil {
return msg, fmt.Errorf("failed to parse EML contents: %w", err)
}
@ -218,9 +218,9 @@ func parseEMLHeaders(mailHeader *netmail.Header, msg *Msg) error {
for _, addr := range parsedAddrs {
addrStrings = append(addrStrings, addr.String())
}
if err = addrFunc(addrStrings...); err != nil {
return fmt.Errorf(`failed to parse %q header: %w`, addrHeader, err)
}
// We can skip the error checking here since netmail.ParseAddressList already performed the
// same address checking that the msg methods do.
_ = addrFunc(addrStrings...)
}
}
@ -600,6 +600,8 @@ func parseEMLAttachmentEmbed(contentDisposition []string, multiPart *multipart.P
if err := msg.EmbedReader(filename, dataReader); err != nil {
return fmt.Errorf("failed to embed multipart body: %w", err)
}
default:
return errors.New("unsupported content disposition type")
}
return nil
}

View file

@ -6,11 +6,8 @@ package mail
import (
"bytes"
"fmt"
"os"
"strings"
"testing"
"time"
)
const (
@ -22,6 +19,23 @@ Subject: Saying Hello
Date: Fri, 21 Nov 1997 09:55:06 -0600
Message-ID: <1234@local.machine.example>
This is a message just to say hello.
So, "Hello".`
exampleMailRFC5322A11InvalidFrom = `From: §§§§§§§§§
To: Mary Smith <mary@example.net>
Subject: Saying Hello
Date: Fri, 21 Nov 1997 09:55:06 -0600
Message-ID: <1234@local.machine.example>
This is a message just to say hello.
So, "Hello".`
exampleMailInvalidHeader = `From: John Doe <jdoe@machine.example>
To: Mary Smith <mary@example.net>
Inva@id*Header; This is a header
Subject: Saying Hello
Date: Fri, 21 Nov 1997 09:55:06 -0600
Message-ID: <1234@local.machine.example>
This is a message just to say hello.
So, "Hello".`
exampleMailPlainNoEnc = `Date: Wed, 01 Nov 2023 00:00:00 +0000
@ -42,6 +56,52 @@ This is a test mail. Please do not reply to this. Also this line is very long so
should be wrapped.
Thank your for your business!
The go-mail team
--
This is a signature`
exampleMailPlainInvalidCTE = `Date: Wed, 01 Nov 2023 00:00:00 +0000
MIME-Version: 1.0
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
Subject: Example mail // plain text without encoding
User-Agent: go-mail v0.4.0 // https://github.com/wneessen/go-mail
X-Mailer: go-mail v0.4.0 // https://github.com/wneessen/go-mail
From: "Toni Tester" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: invalid
Dear Customer,
This is a test mail. Please do not reply to this. Also this line is very long so it
should be wrapped.
Thank your for your business!
The go-mail team
--
This is a signature`
exampleMailInvalidContentType = `Date: Wed, 01 Nov 2023 00:00:00 +0000
MIME-Version: 1.0
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
Subject: Example mail // plain text without encoding
User-Agent: go-mail v0.4.0 // https://github.com/wneessen/go-mail
X-Mailer: go-mail v0.4.0 // https://github.com/wneessen/go-mail
From: "Toni Tester" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
Content-Type: text/plain @ charset=UTF-8; $foo; bar; --invalid--
Content-Transfer-Encoding: 8bit
Dear Customer,
This is a test mail. Please do not reply to this. Also this line is very long so it
should be wrapped.
Thank your for your business!
The go-mail team
@ -304,6 +364,128 @@ VGhpcyBpcyBhIHNpbXBsZSB0ZXN0IHRleHQgZmlsZSBhdHRhY2htZW50LgoKSXQgCiAgaGFzCiAg
ICAgc2V2ZXJhbAogICAgICAgICAgICBuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh
Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg==
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7--`
exampleMailPlainB64WithAttachmentNoContentType = `Date: Wed, 01 Nov 2023 00:00:00 +0000
MIME-Version: 1.0
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
Subject: Example mail // plain text base64 with attachment
User-Agent: go-mail v0.4.1 // https://github.com/wneessen/go-mail
X-Mailer: go-mail v0.4.1 // https://github.com/wneessen/go-mail
From: "Toni Tester" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
Content-Type: multipart/mixed;
boundary=45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
Content-Transfer-Encoding: base64
RGVhciBDdXN0b21lciwKClRoaXMgaXMgYSB0ZXN0IG1haWwuIFBsZWFzZSBkbyBub3QgcmVwbHkg
dG8gdGhpcy4gQWxzbyB0aGlzIGxpbmUgaXMgdmVyeSBsb25nIHNvIGl0CnNob3VsZCBiZSB3cmFw
cGVkLgoKClRoYW5rIHlvdXIgZm9yIHlvdXIgYnVzaW5lc3MhClRoZSBnby1tYWlsIHRlYW0KCi0t
ClRoaXMgaXMgYSBzaWduYXR1cmU=
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
Content-Disposition: attachment; filename="test.attachment"
Content-Transfer-Encoding: base64
VGhpcyBpcyBhIHNpbXBsZSB0ZXN0IHRleHQgZmlsZSBhdHRhY2htZW50LgoKSXQgCiAgaGFzCiAg
ICAgc2V2ZXJhbAogICAgICAgICAgICBuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh
Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg==
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7--`
exampleMailPlainB64WithAttachmentBrokenB64 = `Date: Wed, 01 Nov 2023 00:00:00 +0000
MIME-Version: 1.0
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
Subject: Example mail // plain text base64 with attachment
User-Agent: go-mail v0.4.1 // https://github.com/wneessen/go-mail
X-Mailer: go-mail v0.4.1 // https://github.com/wneessen/go-mail
From: "Toni Tester" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
Content-Type: multipart/mixed;
boundary=45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
Content-Transfer-Encoding: base64
Content-Type: text/plain; charset=UTF-8
RGVhciBDdXN0b21lciwKClRoaXMgaXMgYSB0ZXN0IG1haWwuIFBsZWFzZSBkbyBub3QgcmVwbHkg
dG8gdGhpcy4gQWxzbyB0aGl§§§§§@@@@@XMgdmVyeSBsb25nIHNvIGl0CnNob3VsZCBiZSB3cmFw
cGVkLgoKClRoYW5rIHlvdXIgZm9yIHlvdXIgYnVzaW5lc3MhClRoZSBnby1tYWlsIHRlYW0KCi0t
ClRoaXMgaXMgYSBzaWduYXR1cmU=
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
Content-Disposition: attachment; filename="test.attachment"
Content-Transfer-Encoding: base64
Content-Type: application/octet-stream; name="test.attachment"
VGhpcyBpcyBhIHNpbXBsZSB0ZXN0IHRleHQgZmlsZSBhdHRhY2htZW50LgoKSXQgCiAgaGFzCiAg
ICAgc2V2ZXJhbAogICAg§§§§§@@@@@BuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh
Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg==
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7--`
exampleMailPlainB64WithAttachmentInvalidCTE = `Date: Wed, 01 Nov 2023 00:00:00 +0000
MIME-Version: 1.0
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
Subject: Example mail // plain text base64 with attachment
User-Agent: go-mail v0.4.1 // https://github.com/wneessen/go-mail
X-Mailer: go-mail v0.4.1 // https://github.com/wneessen/go-mail
From: "Toni Tester" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
Content-Type: multipart/mixed;
boundary=45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
Content-Transfer-Encoding: invalid
Content-Type: text/plain; charset=UTF-8
RGVhciBDdXN0b21lciwKClRoaXMgaXMgYSB0ZXN0IG1haWwuIFBsZWFzZSBkbyBub3QgcmVwbHkg
dG8gdGhpcy4gQWxzbyB0aGlzIGxpbmUgaXMgdmVyeSBsb25nIHNvIGl0CnNob3VsZCBiZSB3cmFw
cGVkLgoKClRoYW5rIHlvdXIgZm9yIHlvdXIgYnVzaW5lc3MhClRoZSBnby1tYWlsIHRlYW0KCi0t
ClRoaXMgaXMgYSBzaWduYXR1cmU=
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
Content-Disposition: attachment; filename="test.attachment"
Content-Transfer-Encoding: invalid
Content-Type: application/octet-stream; name="test.attachment"
VGhpcyBpcyBhIHNpbXBsZSB0ZXN0IHRleHQgZmlsZSBhdHRhY2htZW50LgoKSXQgCiAgaGFzCiAg
ICAgc2V2ZXJhbAogICAgICAgICAgICBuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh
Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg==
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7--`
exampleMailPlainB64WithAttachmentInvalidContentType = `Date: Wed, 01 Nov 2023 00:00:00 +0000
MIME-Version: 1.0
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
Subject: Example mail // plain text base64 with attachment
User-Agent: go-mail v0.4.1 // https://github.com/wneessen/go-mail
X-Mailer: go-mail v0.4.1 // https://github.com/wneessen/go-mail
From: "Toni Tester" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
Content-Type: multipart/mixed;
boundary=45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
Content-Transfer-Encoding: base64
Content-Type: text/plain; charset=UTF-8
RGVhciBDdXN0b21lciwKClRoaXMgaXMgYSB0ZXN0IG1haWwuIFBsZWFzZSBkbyBub3QgcmVwbHkg
dG8gdGhpcy4gQWxzbyB0aGlzIGxpbmUgaXMgdmVyeSBsb25nIHNvIGl0CnNob3VsZCBiZSB3cmFw
cGVkLgoKClRoYW5rIHlvdXIgZm9yIHlvdXIgYnVzaW5lc3MhClRoZSBnby1tYWlsIHRlYW0KCi0t
ClRoaXMgaXMgYSBzaWduYXR1cmU=
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7
Content-Disposition: attachment; filename="test.attachment"
Content-Transfer-Encoding: base64
Content-Type; text/plain @ charset=UTF-8; $foo; bar; --invalid--
VGhpcyBpcyBhIHNpbXBsZSB0ZXN0IHRleHQgZmlsZSBhdHRhY2htZW50LgoKSXQgCiAgaGFzCiAg
ICAgc2V2ZXJhbAogICAgICAgICAgICBuZXdsaW5lcwoJICAgICAgICAgICAgYW5kCgkgICAgc3Bh
Y2VzCiAgICAgaW4KICBpdAouCgpBcyB3ZWxsIGFzIGFuIGVtb2ppOiDwn5mCCg==
--45c75ff528359022eb03679fbe91877d75343f2e1f8193e349deffa33ff7--`
exampleMailPlainB64WithAttachmentNoBoundary = `Date: Wed, 01 Nov 2023 00:00:00 +0000
MIME-Version: 1.0
@ -578,6 +760,39 @@ Content-Disposition: attachment;
filename="testfile.txt"
VGhpcyBpcyBhIHRlc3QgaW4gQmFzZTY0
--------------26A45336F6C6196BD8BBA2A2--`
exampleMultiPart7BitBase64BrokenB64 = `Date: Wed, 01 Nov 2023 00:00:00 +0000
MIME-Version: 1.0
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
Subject: Example mail // 7bit with base64 attachment
User-Agent: go-mail v0.4.1 // https://github.com/wneessen/go-mail
X-Mailer: go-mail v0.4.1 // https://github.com/wneessen/go-mail
From: "Toni Tester" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Cc: <go-mail+cc@go-mail.dev>
Content-Type: multipart/mixed;
boundary="------------26A45336F6C6196BD8BBA2A2"
This is a multi-part message in MIME format.
--------------26A45336F6C6196BD8BBA2A2
Content-Type: text/plain; charset=US-ASCII; format=flowed
Content-Transfer-Encoding: 7bit
testtest
testtest
testtest
testtest
testtest
testtest
--------------26A45336F6C6196BD8BBA2A2
Content-Type: text/plain; charset=UTF-8;
name="testfile.txt"
Content-Transfer-Encoding: base64
Content-Disposition: attachment;
filename="testfile.txt"
VGh@@@@§§§§hIHRlc3QgaW4gQmFzZTY0
--------------26A45336F6C6196BD8BBA2A2--`
exampleMultiPart8BitBase64 = `Date: Wed, 01 Nov 2023 00:00:00 +0000
MIME-Version: 1.0
@ -612,8 +827,352 @@ Content-Disposition: attachment;
VGhpcyBpcyBhIHRlc3QgaW4gQmFzZTY0
--------------26A45336F6C6196BD8BBA2A2--`
exampleMailWithInlineEmbed = `Date: Wed, 01 Nov 2023 00:00:00 +0000
MIME-Version: 1.0
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
Subject: Example mail with inline embed
User-Agent: go-mail v0.4.1 // https://github.com/wneessen/go-mail
X-Mailer: go-mail v0.4.1 // https://github.com/wneessen/go-mail
From: "Toni Tester" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Content-Type: multipart/related; boundary="abc123"
--abc123
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: quoted-printable
<html>
<body>
<p>Hello,</p>
<p>This is an example email with an inline image:</p>
<img src="cid:12345@go-mail.dev" alt="Inline Image">
<p>Best regards,<br>The go-mail team</p>
</body>
</html>
--abc123
Content-Type: image/png
Content-Transfer-Encoding: base64
Content-ID: <12345@go-mail.dev>
Content-Disposition: inline; filename="test.png"
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2NgYGD4DwABBAEAwS2O
UAAAAABJRU5ErkJggg==
--abc123--`
exampleMailWithInlineEmbedWrongDisposition = `Date: Wed, 01 Nov 2023 00:00:00 +0000
MIME-Version: 1.0
Message-ID: <1305604950.683004066175.AAAAAAAAaaaaaaaaB@go-mail.dev>
Subject: Example mail with inline embed
User-Agent: go-mail v0.4.1 // https://github.com/wneessen/go-mail
X-Mailer: go-mail v0.4.1 // https://github.com/wneessen/go-mail
From: "Toni Tester" <go-mail@go-mail.dev>
To: <go-mail+test@go-mail.dev>
Content-Type: multipart/related; boundary="abc123"
--abc123
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: quoted-printable
<html>
<body>
<p>Hello,</p>
<p>This is an example email with an inline image:</p>
<img src="cid:12345@go-mail.dev" alt="Inline Image">
<p>Best regards,<br>The go-mail team</p>
</body>
</html>
--abc123
Content-Type: image/png
Content-Transfer-Encoding: base64
Content-ID: <12345@go-mail.dev>
Content-Disposition: broken; filename="test.png"
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2NgYGD4DwABBAEAwS2O
UAAAAABJRU5ErkJggg==
--abc123--`
)
func TestEMLToMsgFromReader(t *testing.T) {
t.Run("EMLToMsgFromReader via EMLToMsgFromString, check subject and encoding", func(t *testing.T) {
tests := []struct {
name string
emlString string
wantEncoding Encoding
wantSubject string
}{
{
"RFC5322 A1.1 example mail", exampleMailRFC5322A11, EncodingUSASCII,
"Saying Hello",
},
{
"Plain text no encoding (7bit)", exampleMailPlain7Bit, EncodingUSASCII,
"Example mail // plain text without encoding",
},
{
"Plain text no encoding", exampleMailPlainNoEnc, NoEncoding,
"Example mail // plain text without encoding",
},
{
"Plain text quoted-printable", exampleMailPlainQP, EncodingQP,
"Example mail // plain text quoted-printable",
},
{
"Plain text base64", exampleMailPlainB64, EncodingB64,
"Example mail // plain text base64",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parsed, err := EMLToMsgFromString(tt.emlString)
if err != nil {
t.Fatalf("failed to parse EML string: %s", err)
}
if parsed.Encoding() != tt.wantEncoding.String() {
t.Errorf("failed to parse EML string: want encoding %s, got %s", tt.wantEncoding,
parsed.Encoding())
}
gotSubject, ok := parsed.genHeader[HeaderSubject]
if !ok {
t.Fatalf("failed to parse EML string. No subject header found")
}
if len(gotSubject) != 1 {
t.Fatalf("failed to parse EML string, more than one subject header found")
}
if !strings.EqualFold(gotSubject[0], tt.wantSubject) {
t.Errorf("failed to parse EML string: want subject %s, got %s", tt.wantSubject,
gotSubject[0])
}
})
}
})
t.Run("EMLToMsgFromReader fails on reader", func(t *testing.T) {
emlReader := bytes.NewBufferString("invalid")
if _, err := EMLToMsgFromReader(emlReader); err == nil {
t.Errorf("EML parsing with invalid EML string should fail")
}
})
t.Run("EMLToMsgFromReader fails on parseEML", func(t *testing.T) {
emlReader := bytes.NewBufferString(exampleMailRFC5322A11InvalidFrom)
if _, err := EMLToMsgFromReader(emlReader); err == nil {
t.Errorf("EML parsing with invalid EML string should fail")
}
})
t.Run("EMLToMsgFromReader via EMLToMsgFromString on different examples", func(t *testing.T) {
tests := []struct {
name string
emlString string
shouldFail bool
}{
{
name: "Valid RFC 5322 Example",
emlString: exampleMailRFC5322A11,
shouldFail: false,
},
{
name: "Invalid From Header (RFC 5322)",
emlString: exampleMailRFC5322A11InvalidFrom,
shouldFail: true,
},
{
name: "Invalid Header",
emlString: exampleMailInvalidHeader,
shouldFail: true,
},
{
name: "Plain broken Content-Type",
emlString: exampleMailInvalidContentType,
shouldFail: true,
},
{
name: "Plain No Encoding",
emlString: exampleMailPlainNoEnc,
shouldFail: false,
},
{
name: "Plain invalid CTE",
emlString: exampleMailPlainInvalidCTE,
shouldFail: true,
},
{
name: "Plain 7bit",
emlString: exampleMailPlain7Bit,
shouldFail: false,
},
{
name: "Broken Body Base64",
emlString: exampleMailPlainBrokenBody,
shouldFail: true,
},
{
name: "Unknown Content Type",
emlString: exampleMailPlainUnknownContentType,
shouldFail: true,
},
{
name: "Broken Header",
emlString: exampleMailPlainBrokenHeader,
shouldFail: true,
},
{
name: "Broken From Header",
emlString: exampleMailPlainBrokenFrom,
shouldFail: true,
},
{
name: "Broken To Header",
emlString: exampleMailPlainBrokenTo,
shouldFail: true,
},
{
name: "Invalid Date",
emlString: exampleMailPlainNoEncInvalidDate,
shouldFail: true,
},
{
name: "No Date Header",
emlString: exampleMailPlainNoEncNoDate,
shouldFail: false,
},
{
name: "Quoted Printable Encoding",
emlString: exampleMailPlainQP,
shouldFail: false,
},
{
name: "Unsupported Transfer Encoding",
emlString: exampleMailPlainUnsupportedTransferEnc,
shouldFail: true,
},
{
name: "Base64 Encoding",
emlString: exampleMailPlainB64,
shouldFail: false,
},
{
name: "Base64 with Attachment",
emlString: exampleMailPlainB64WithAttachment,
shouldFail: false,
},
{
name: "Base64 with Attachment no content types",
emlString: exampleMailPlainB64WithAttachmentNoContentType,
shouldFail: true,
},
{
name: "Multipart Base64 with Attachment broken Base64",
emlString: exampleMailPlainB64WithAttachmentBrokenB64,
shouldFail: true,
},
{
name: "Base64 with Attachment with invalid content type in attachment",
emlString: exampleMailPlainB64WithAttachmentInvalidContentType,
shouldFail: true,
},
{
name: "Base64 with Attachment with invalid CTE in attachment",
emlString: exampleMailPlainB64WithAttachmentInvalidCTE,
shouldFail: true,
},
{
name: "Base64 with Attachment No Boundary",
emlString: exampleMailPlainB64WithAttachmentNoBoundary,
shouldFail: true,
},
{
name: "Broken Body Base64",
emlString: exampleMailPlainB64BrokenBody,
shouldFail: true,
},
{
name: "Base64 with Embedded Image",
emlString: exampleMailPlainB64WithEmbed,
shouldFail: false,
},
{
name: "Base64 with Embed No Content-ID",
emlString: exampleMailPlainB64WithEmbedNoContentID,
shouldFail: false,
},
{
name: "Multipart Mixed with Attachment, Embed, and Alternative Part",
emlString: exampleMailMultipartMixedAlternativeRelated,
shouldFail: false,
},
{
name: "Multipart 7bit Base64",
emlString: exampleMultiPart7BitBase64,
shouldFail: false,
},
{
name: "Multipart 7bit Base64 with broken Base64",
emlString: exampleMultiPart7BitBase64BrokenB64,
shouldFail: true,
},
{
name: "Multipart 8bit Base64",
emlString: exampleMultiPart8BitBase64,
shouldFail: false,
},
{
name: "Multipart with inline embed",
emlString: exampleMailWithInlineEmbed,
shouldFail: false,
},
{
name: "Multipart with inline embed disposition broken",
emlString: exampleMailWithInlineEmbedWrongDisposition,
shouldFail: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := EMLToMsgFromString(tt.emlString)
if tt.shouldFail && err == nil {
t.Errorf("parsing of EML was supposed to fail, but it did not")
}
if !tt.shouldFail && err != nil {
t.Errorf("parsing of EML failed: %s", err)
}
})
}
})
}
func TestEMLToMsgFromFile(t *testing.T) {
t.Run("EMLToMsgFromFile succeeds", func(t *testing.T) {
parsed, err := EMLToMsgFromFile("testdata/RFC5322-A1-1.eml")
if err != nil {
t.Fatalf("EMLToMsgFromFile failed: %s ", err)
}
if parsed.Encoding() != EncodingUSASCII.String() {
t.Errorf("EMLToMsgFromFile failed: want encoding %s, got %s", EncodingUSASCII,
parsed.Encoding())
}
gotSubject, ok := parsed.genHeader[HeaderSubject]
if !ok {
t.Fatalf("failed to parse EML string. No subject header found")
}
if len(gotSubject) != 1 {
t.Fatalf("failed to parse EML string, more than one subject header found")
}
if !strings.EqualFold(gotSubject[0], "Saying Hello") {
t.Errorf("failed to parse EML string: want subject %s, got %s", "Saying Hello",
gotSubject[0])
}
})
t.Run("EMLToMsgFromFile fails on file not found", func(t *testing.T) {
if _, err := EMLToMsgFromFile("testdata/not-existing.eml"); err == nil {
t.Errorf("EMLToMsgFromFile with invalid file should fail")
}
})
t.Run("EMLToMsgFromFile fails on parseEML", func(t *testing.T) {
if _, err := EMLToMsgFromFile("testdata/RFC5322-A1-1-invalid-from.eml"); err == nil {
t.Errorf("EMLToMsgFromFile with invalid EML message should fail")
}
})
}
/*
func TestEMLToMsgFromString(t *testing.T) {
tests := []struct {
name string
@ -621,26 +1180,6 @@ func TestEMLToMsgFromString(t *testing.T) {
enc string
sub string
}{
{
"RFC5322 A1.1", exampleMailRFC5322A11, "7bit",
"Saying Hello",
},
{
"Plain text no encoding (7bit)", exampleMailPlain7Bit, "7bit",
"Example mail // plain text without encoding",
},
{
"Plain text no encoding", exampleMailPlainNoEnc, "8bit",
"Example mail // plain text without encoding",
},
{
"Plain text quoted-printable", exampleMailPlainQP, "quoted-printable",
"Example mail // plain text quoted-printable",
},
{
"Plain text base64", exampleMailPlainB64, "base64",
"Example mail // plain text base64",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -1009,3 +1548,6 @@ func stringToTempFile(data, name string) (string, string, error) {
}
return tempDir, filePath, nil
}
*/

View file

@ -6,134 +6,183 @@ package mail
import "testing"
// TestFile_SetGetHeader tests the set-/getHeader method of the File object
func TestFile_SetGetHeader(t *testing.T) {
f := File{
Name: "testfile.txt",
Header: make(map[string][]string),
}
f.setHeader(HeaderContentType, "text/plain")
fi, ok := f.getHeader(HeaderContentType)
if !ok {
t.Errorf("getHeader method of File did not return a value")
return
}
if fi != "text/plain" {
t.Errorf("getHeader returned wrong value. Expected: %s, got: %s", "text/plain", fi)
}
fi, ok = f.getHeader(HeaderContentTransferEnc)
if ok {
t.Errorf("getHeader method of File did return a value, but wasn't supposed to")
return
}
if fi != "" {
t.Errorf("getHeader returned wrong value. Expected: %s, got: %s", "", fi)
}
}
// TestFile_WithFileDescription tests the WithFileDescription option
func TestFile_WithFileDescription(t *testing.T) {
tests := []struct {
name string
desc string
}{
{"File description: test", "test"},
{"File description: empty", ""},
}
for _, tt := range tests {
m := NewMsg()
t.Run(tt.name, func(t *testing.T) {
m.AttachFile("file.go", WithFileDescription(tt.desc))
al := m.GetAttachments()
if len(al) <= 0 {
t.Errorf("AttachFile() failed. Attachment list is empty")
}
a := al[0]
if a.Desc != tt.desc {
t.Errorf("WithFileDescription() failed. Expected: %s, got: %s", tt.desc, a.Desc)
}
})
}
}
// TestFile_WithContentID tests the WithFileContentID option
func TestFile_WithContentID(t *testing.T) {
tests := []struct {
name string
contentid string
}{
{"File Content-ID: test", "test"},
{"File Content-ID: empty", ""},
}
for _, tt := range tests {
m := NewMsg()
t.Run(tt.name, func(t *testing.T) {
m.AttachFile("file.go", WithFileContentID(tt.contentid))
al := m.GetAttachments()
if len(al) <= 0 {
t.Errorf("AttachFile() failed. Attachment list is empty")
}
a := al[0]
if a.Header.Get(HeaderContentID.String()) != tt.contentid {
t.Errorf("WithFileContentID() failed. Expected: %s, got: %s", tt.contentid,
a.Header.Get(HeaderContentID.String()))
}
})
}
}
// TestFile_WithFileEncoding tests the WithFileEncoding option
func TestFile_WithFileEncoding(t *testing.T) {
tests := []struct {
name string
enc Encoding
want Encoding
}{
{"File encoding: 8bit raw", NoEncoding, NoEncoding},
{"File encoding: Base64", EncodingB64, EncodingB64},
{"File encoding: quoted-printable (not allowed)", EncodingQP, ""},
}
for _, tt := range tests {
m := NewMsg()
t.Run(tt.name, func(t *testing.T) {
m.AttachFile("file.go", WithFileEncoding(tt.enc))
al := m.GetAttachments()
if len(al) <= 0 {
t.Errorf("AttachFile() failed. Attachment list is empty")
}
a := al[0]
if a.Enc != tt.want {
t.Errorf("WithFileEncoding() failed. Expected: %s, got: %s", tt.enc, a.Enc)
}
})
}
}
// TestFile_WithFileContentType tests the WithFileContentType option
func TestFile_WithFileContentType(t *testing.T) {
tests := []struct {
name string
ct ContentType
want string
}{
{"File content-type: text/plain", TypeTextPlain, "text/plain"},
{"File content-type: html/html", TypeTextHTML, "text/html"},
{"File content-type: application/octet-stream", TypeAppOctetStream, "application/octet-stream"},
{"File content-type: application/pgp-encrypted", TypePGPEncrypted, "application/pgp-encrypted"},
{"File content-type: application/pgp-signature", TypePGPSignature, "application/pgp-signature"},
}
for _, tt := range tests {
m := NewMsg()
t.Run(tt.name, func(t *testing.T) {
m.AttachFile("file.go", WithFileContentType(tt.ct))
al := m.GetAttachments()
if len(al) <= 0 {
t.Errorf("AttachFile() failed. Attachment list is empty")
}
a := al[0]
if a.ContentType != ContentType(tt.want) {
t.Errorf("WithFileContentType() failed. Expected: %s, got: %s", tt.want, a.ContentType)
}
})
}
func TestFile(t *testing.T) {
t.Run("setHeader", func(t *testing.T) {
f := File{
Name: "testfile.txt",
Header: make(map[string][]string),
}
f.setHeader(HeaderContentType, "text/plain")
contentType, ok := f.Header[HeaderContentType.String()]
if !ok {
t.Fatalf("setHeader failed. Expected header %s to be set", HeaderContentType)
}
if len(contentType) != 1 {
t.Fatalf("setHeader failed. Expected header %s to have one value, got: %d", HeaderContentType,
len(contentType))
}
if contentType[0] != "text/plain" {
t.Fatalf("setHeader failed. Expected header %s to have value %s, got: %s",
HeaderContentType.String(), "text/plain", contentType[0])
}
})
t.Run("getHeader", func(t *testing.T) {
f := File{
Name: "testfile.txt",
Header: make(map[string][]string),
}
f.setHeader(HeaderContentType, "text/plain")
contentType, ok := f.getHeader(HeaderContentType)
if !ok {
t.Fatalf("setHeader failed. Expected header %s to be set", HeaderContentType)
}
if contentType != "text/plain" {
t.Fatalf("setHeader failed. Expected header %s to have value %s, got: %s",
HeaderContentType.String(), "text/plain", contentType)
}
})
t.Run("WithFileDescription", func(t *testing.T) {
tests := []struct {
name string
desc string
}{
{"File description: test", "test"},
{"File description: with newline", "test\n"},
{"File description: empty", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
message := NewMsg()
message.AttachFile("file.go", WithFileDescription(tt.desc))
attachments := message.GetAttachments()
if len(attachments) <= 0 {
t.Fatalf("failed to retrieve attachments list")
}
firstAttachment := attachments[0]
if firstAttachment == nil {
t.Fatalf("failed to retrieve first attachment, got nil")
}
if firstAttachment.Desc != tt.desc {
t.Errorf("WithFileDescription() failed. Expected: %s, got: %s", tt.desc,
firstAttachment.Desc)
}
})
}
})
t.Run("WithFileContentID", func(t *testing.T) {
tests := []struct {
name string
id string
}{
{"Content-ID: test", "test"},
{"Content-ID: with newline", "test\n"},
{"Content-ID: empty", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
message := NewMsg()
message.AttachFile("file.go", WithFileContentID(tt.id))
attachments := message.GetAttachments()
if len(attachments) <= 0 {
t.Fatalf("failed to retrieve attachments list")
}
firstAttachment := attachments[0]
if firstAttachment == nil {
t.Fatalf("failed to retrieve first attachment, got nil")
}
contentID := firstAttachment.Header.Get(HeaderContentID.String())
if contentID != tt.id {
t.Errorf("WithFileContentID() failed. Expected: %s, got: %s", tt.id,
contentID)
}
})
}
})
t.Run("WithFileEncoding", func(t *testing.T) {
tests := []struct {
name string
encoding Encoding
want Encoding
}{
{"File encoding: US-ASCII", EncodingUSASCII, EncodingUSASCII},
{"File encoding: 8bit raw", NoEncoding, NoEncoding},
{"File encoding: Base64", EncodingB64, EncodingB64},
{"File encoding: quoted-printable (not allowed)", EncodingQP, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
message := NewMsg()
message.AttachFile("file.go", WithFileEncoding(tt.encoding))
attachments := message.GetAttachments()
if len(attachments) <= 0 {
t.Fatalf("failed to retrieve attachments list")
}
firstAttachment := attachments[0]
if firstAttachment == nil {
t.Fatalf("failed to retrieve first attachment, got nil")
}
if firstAttachment.Enc != tt.want {
t.Errorf("WithFileEncoding() failed. Expected: %s, got: %s", tt.want, firstAttachment.Enc)
}
})
}
})
t.Run("WithFileName", func(t *testing.T) {
tests := []struct {
name string
fileName string
}{
{"File name: test", "test"},
{"File name: with newline", "test\n"},
{"File name: empty", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
message := NewMsg()
message.AttachFile("file.go", WithFileName(tt.fileName))
attachments := message.GetAttachments()
if len(attachments) <= 0 {
t.Fatalf("failed to retrieve attachments list")
}
firstAttachment := attachments[0]
if firstAttachment == nil {
t.Fatalf("failed to retrieve first attachment, got nil")
}
if firstAttachment.Name != tt.fileName {
t.Errorf("WithFileName() failed. Expected: %s, got: %s", tt.fileName,
firstAttachment.Name)
}
})
}
})
t.Run("WithFileContentType", func(t *testing.T) {
tests := []struct {
name string
contentType ContentType
}{
{"File content-type: text/plain", TypeTextPlain},
{"File content-type: html/html", TypeTextHTML},
{"File content-type: application/octet-stream", TypeAppOctetStream},
{"File content-type: application/pgp-encrypted", TypePGPEncrypted},
{"File content-type: application/pgp-signature", TypePGPSignature},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
message := NewMsg()
message.AttachFile("file.go", WithFileContentType(tt.contentType))
attachments := message.GetAttachments()
if len(attachments) <= 0 {
t.Fatalf("failed to retrieve attachments list")
}
firstAttachment := attachments[0]
if firstAttachment == nil {
t.Fatalf("failed to retrieve first attachment, got nil")
}
if firstAttachment.ContentType != tt.contentType {
t.Errorf("WithFileContentType() failed. Expected: %s, got: %s", tt.contentType,
firstAttachment.ContentType)
}
})
}
})
}

View file

@ -8,69 +8,13 @@ import (
"testing"
)
// TestImportance_StringFuncs tests the different string method of the Importance object
func TestImportance_StringFuncs(t *testing.T) {
tests := []struct {
var (
genHeaderTests = []struct {
name string
imp Importance
wantns string
xprio string
header Header
want string
}{
{"Importance: Non-Urgent", ImportanceNonUrgent, "0", "5", "non-urgent"},
{"Importance: Low", ImportanceLow, "0", "5", "low"},
{"Importance: Normal", ImportanceNormal, "", "", ""},
{"Importance: High", ImportanceHigh, "1", "1", "high"},
{"Importance: Urgent", ImportanceUrgent, "1", "1", "urgent"},
{"Importance: Unknown", 9, "", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.imp.NumString() != tt.wantns {
t.Errorf("wrong number string for Importance returned. Expected: %s, got: %s",
tt.wantns, tt.imp.NumString())
}
if tt.imp.XPrioString() != tt.xprio {
t.Errorf("wrong x-prio string for Importance returned. Expected: %s, got: %s",
tt.xprio, tt.imp.XPrioString())
}
if tt.imp.String() != tt.want {
t.Errorf("wrong string for Importance returned. Expected: %s, got: %s",
tt.want, tt.imp.String())
}
})
}
}
// TestAddrHeader_String tests the string method of the AddrHeader object
func TestAddrHeader_String(t *testing.T) {
tests := []struct {
name string
ah AddrHeader
want string
}{
{"Address header: From", HeaderFrom, "From"},
{"Address header: To", HeaderTo, "To"},
{"Address header: Cc", HeaderCc, "Cc"},
{"Address header: Bcc", HeaderBcc, "Bcc"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.ah.String() != tt.want {
t.Errorf("wrong string for AddrHeader returned. Expected: %s, got: %s",
tt.want, tt.ah.String())
}
})
}
}
// TestHeader_String tests the string method of the Header object
func TestHeader_String(t *testing.T) {
tests := []struct {
name string
h Header
want string
}{
{"Header: Content-Description", HeaderContentDescription, "Content-Description"},
{"Header: Content-Disposition", HeaderContentDisposition, "Content-Disposition"},
{"Header: Content-ID", HeaderContentID, "Content-ID"},
{"Header: Content-Language", HeaderContentLang, "Content-Language"},
@ -78,6 +22,10 @@ func TestHeader_String(t *testing.T) {
{"Header: Content-Transfer-Encoding", HeaderContentTransferEnc, "Content-Transfer-Encoding"},
{"Header: Content-Type", HeaderContentType, "Content-Type"},
{"Header: Date", HeaderDate, "Date"},
{
"Header: Disposition-Notification-To", HeaderDispositionNotificationTo,
"Disposition-Notification-To",
},
{"Header: Importance", HeaderImportance, "Importance"},
{"Header: In-Reply-To", HeaderInReplyTo, "In-Reply-To"},
{"Header: List-Unsubscribe", HeaderListUnsubscribe, "List-Unsubscribe"},
@ -87,19 +35,90 @@ func TestHeader_String(t *testing.T) {
{"Header: Organization", HeaderOrganization, "Organization"},
{"Header: Precedence", HeaderPrecedence, "Precedence"},
{"Header: Priority", HeaderPriority, "Priority"},
{"Header: HeaderReferences", HeaderReferences, "References"},
{"Header: References", HeaderReferences, "References"},
{"Header: Reply-To", HeaderReplyTo, "Reply-To"},
{"Header: Subject", HeaderSubject, "Subject"},
{"Header: User-Agent", HeaderUserAgent, "User-Agent"},
{"Header: X-Auto-Response-Suppress", HeaderXAutoResponseSuppress, "X-Auto-Response-Suppress"},
{"Header: X-Mailer", HeaderXMailer, "X-Mailer"},
{"Header: X-MSMail-Priority", HeaderXMSMailPriority, "X-MSMail-Priority"},
{"Header: X-Priority", HeaderXPriority, "X-Priority"},
}
for _, tt := range tests {
addrHeaderTests = []struct {
name string
header AddrHeader
want string
}{
{"From", HeaderFrom, "From"},
{"To", HeaderTo, "To"},
{"Cc", HeaderCc, "Cc"},
{"Bcc", HeaderBcc, "Bcc"},
}
)
func TestImportance_Stringer(t *testing.T) {
tests := []struct {
name string
imp Importance
wantnum string
xprio string
want string
}{
{"Non-Urgent", ImportanceNonUrgent, "0", "5", "non-urgent"},
{"Low", ImportanceLow, "0", "5", "low"},
{"Normal", ImportanceNormal, "", "", ""},
{"High", ImportanceHigh, "1", "1", "high"},
{"Urgent", ImportanceUrgent, "1", "1", "urgent"},
{"Unknown", 9, "", "", ""},
}
t.Run("String", func(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.imp.String() != tt.want {
t.Errorf("wrong string for Importance returned. Expected: %s, got: %s", tt.want, tt.imp.String())
}
})
}
})
t.Run("NumString", func(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.imp.NumString() != tt.wantnum {
t.Errorf("wrong number string for Importance returned. Expected: %s, got: %s", tt.wantnum,
tt.imp.NumString())
}
})
}
})
t.Run("XPrioString", func(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.imp.XPrioString() != tt.xprio {
t.Errorf("wrong x-prio string for Importance returned. Expected: %s, got: %s", tt.xprio,
tt.imp.XPrioString())
}
})
}
})
}
func TestAddrHeader_Stringer(t *testing.T) {
for _, tt := range addrHeaderTests {
t.Run(tt.name, func(t *testing.T) {
if tt.h.String() != tt.want {
t.Errorf("wrong string for Header returned. Expected: %s, got: %s",
tt.want, tt.h.String())
if tt.header.String() != tt.want {
t.Errorf("wrong string for AddrHeader returned. Expected: %s, got: %s",
tt.want, tt.header.String())
}
})
}
}
func TestHeader_Stringer(t *testing.T) {
for _, tt := range genHeaderTests {
t.Run(tt.name, func(t *testing.T) {
if tt.header.String() != tt.want {
t.Errorf("wrong string for Header returned. Expected: %s, got: %s",
tt.want, tt.header.String())
}
})
}

91
msg.go
View file

@ -583,6 +583,9 @@ func (m *Msg) SetAddrHeader(header AddrHeader, values ...string) error {
// References:
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.4
func (m *Msg) SetAddrHeaderIgnoreInvalid(header AddrHeader, values ...string) {
if m.addrHeader == nil {
m.addrHeader = make(map[AddrHeader][]*mail.Address)
}
var addresses []*mail.Address
for _, addrVal := range values {
address, err := mail.ParseAddress(m.encodeString(addrVal))
@ -591,7 +594,14 @@ func (m *Msg) SetAddrHeaderIgnoreInvalid(header AddrHeader, values ...string) {
}
addresses = append(addresses, address)
}
m.addrHeader[header] = addresses
switch header {
case HeaderFrom:
if len(addresses) > 0 {
m.addrHeader[header] = []*mail.Address{addresses[0]}
}
default:
m.addrHeader[header] = addresses
}
}
// EnvelopeFrom sets the envelope from address for the Msg.
@ -743,7 +753,16 @@ func (m *Msg) ToIgnoreInvalid(rcpts ...string) {
// References:
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3
func (m *Msg) ToFromString(rcpts string) error {
return m.To(strings.Split(rcpts, ",")...)
src := strings.Split(rcpts, ",")
var dst []string
for _, address := range src {
address = strings.TrimSpace(address)
if address == "" {
continue
}
dst = append(dst, address)
}
return m.To(dst...)
}
// Cc sets one or more "CC" (carbon copy) addresses in the mail body for the Msg.
@ -828,7 +847,16 @@ func (m *Msg) CcIgnoreInvalid(rcpts ...string) {
// References:
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3
func (m *Msg) CcFromString(rcpts string) error {
return m.Cc(strings.Split(rcpts, ",")...)
src := strings.Split(rcpts, ",")
var dst []string
for _, address := range src {
address = strings.TrimSpace(address)
if address == "" {
continue
}
dst = append(dst, address)
}
return m.Cc(dst...)
}
// Bcc sets one or more "BCC" (blind carbon copy) addresses in the mail body for the Msg.
@ -914,7 +942,16 @@ func (m *Msg) BccIgnoreInvalid(rcpts ...string) {
// References:
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.3
func (m *Msg) BccFromString(rcpts string) error {
return m.Bcc(strings.Split(rcpts, ",")...)
src := strings.Split(rcpts, ",")
var dst []string
for _, address := range src {
address = strings.TrimSpace(address)
if address == "" {
continue
}
dst = append(dst, address)
}
return m.Bcc(dst...)
}
// ReplyTo sets the "Reply-To" address for the Msg, specifying where replies should be sent.
@ -1050,8 +1087,7 @@ func (m *Msg) SetBulk() {
// - https://datatracker.ietf.org/doc/html/rfc5322#section-3.3
// - https://datatracker.ietf.org/doc/html/rfc1123
func (m *Msg) SetDate() {
now := time.Now().Format(time.RFC1123Z)
m.SetGenHeader(HeaderDate, now)
m.SetDateWithValue(time.Now())
}
// SetDateWithValue sets the "Date" header for the Msg using the provided time value in a valid RFC 1123 format.
@ -1151,6 +1187,9 @@ func (m *Msg) IsDelivered() bool {
// References:
// - https://datatracker.ietf.org/doc/html/rfc8098
func (m *Msg) RequestMDNTo(rcpts ...string) error {
if m.genHeader == nil {
m.genHeader = make(map[Header][]string)
}
var addresses []string
for _, addrVal := range rcpts {
address, err := mail.ParseAddress(addrVal)
@ -1159,9 +1198,7 @@ func (m *Msg) RequestMDNTo(rcpts ...string) error {
}
addresses = append(addresses, address.String())
}
if _, ok := m.genHeader[HeaderDispositionNotificationTo]; ok {
m.genHeader[HeaderDispositionNotificationTo] = addresses
}
m.genHeader[HeaderDispositionNotificationTo] = addresses
return nil
}
@ -1200,11 +1237,11 @@ func (m *Msg) RequestMDNAddTo(rcpt string) error {
return fmt.Errorf(errParseMailAddr, rcpt, err)
}
var addresses []string
addresses = append(addresses, m.genHeader[HeaderDispositionNotificationTo]...)
addresses = append(addresses, address.String())
if _, ok := m.genHeader[HeaderDispositionNotificationTo]; ok {
m.genHeader[HeaderDispositionNotificationTo] = addresses
if current, ok := m.genHeader[HeaderDispositionNotificationTo]; ok {
addresses = current
}
addresses = append(addresses, address.String())
m.genHeader[HeaderDispositionNotificationTo] = addresses
return nil
}
@ -1644,11 +1681,11 @@ func (m *Msg) SetBodyHTMLTemplate(tpl *ht.Template, data interface{}, opts ...Pa
if tpl == nil {
return errors.New(errTplPointerNil)
}
buffer := bytes.Buffer{}
if err := tpl.Execute(&buffer, data); err != nil {
buffer := bytes.NewBuffer(nil)
if err := tpl.Execute(buffer, data); err != nil {
return fmt.Errorf(errTplExecuteFailed, err)
}
writeFunc := writeFuncFromBuffer(&buffer)
writeFunc := writeFuncFromBuffer(buffer)
m.SetBodyWriter(TypeTextHTML, writeFunc, opts...)
return nil
}
@ -1675,11 +1712,11 @@ func (m *Msg) SetBodyTextTemplate(tpl *tt.Template, data interface{}, opts ...Pa
if tpl == nil {
return errors.New(errTplPointerNil)
}
buf := bytes.Buffer{}
if err := tpl.Execute(&buf, data); err != nil {
buffer := bytes.NewBuffer(nil)
if err := tpl.Execute(buffer, data); err != nil {
return fmt.Errorf(errTplExecuteFailed, err)
}
writeFunc := writeFuncFromBuffer(&buf)
writeFunc := writeFuncFromBuffer(buffer)
m.SetBodyWriter(TypeTextPlain, writeFunc, opts...)
return nil
}
@ -1749,11 +1786,11 @@ func (m *Msg) AddAlternativeHTMLTemplate(tpl *ht.Template, data interface{}, opt
if tpl == nil {
return errors.New(errTplPointerNil)
}
buffer := bytes.Buffer{}
if err := tpl.Execute(&buffer, data); err != nil {
buffer := bytes.NewBuffer(nil)
if err := tpl.Execute(buffer, data); err != nil {
return fmt.Errorf(errTplExecuteFailed, err)
}
writeFunc := writeFuncFromBuffer(&buffer)
writeFunc := writeFuncFromBuffer(buffer)
m.AddAlternativeWriter(TypeTextHTML, writeFunc, opts...)
return nil
}
@ -1779,11 +1816,11 @@ func (m *Msg) AddAlternativeTextTemplate(tpl *tt.Template, data interface{}, opt
if tpl == nil {
return errors.New(errTplPointerNil)
}
buffer := bytes.Buffer{}
if err := tpl.Execute(&buffer, data); err != nil {
buffer := bytes.NewBuffer(nil)
if err := tpl.Execute(buffer, data); err != nil {
return fmt.Errorf(errTplExecuteFailed, err)
}
writeFunc := writeFuncFromBuffer(&buffer)
writeFunc := writeFuncFromBuffer(buffer)
m.AddAlternativeWriter(TypeTextPlain, writeFunc, opts...)
return nil
}
@ -2334,8 +2371,8 @@ func (m *Msg) WriteToSendmailWithContext(ctx context.Context, sendmailPath strin
// - https://datatracker.ietf.org/doc/html/rfc5322
func (m *Msg) NewReader() *Reader {
reader := &Reader{}
buffer := bytes.Buffer{}
_, err := m.Write(&buffer)
buffer := bytes.NewBuffer(nil)
_, err := m.Write(buffer)
if err != nil {
reader.err = fmt.Errorf("failed to write Msg to Reader buffer: %w", err)
}

View file

@ -1,85 +0,0 @@
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT
//go:build !windows
// +build !windows
package mail
import (
"context"
"os"
"testing"
"time"
)
// TestMsg_WriteToSendmailWithContext tests the WriteToSendmailWithContext() method of the Msg
func TestMsg_WriteToSendmailWithContext(t *testing.T) {
if os.Getenv("TEST_SKIP_SENDMAIL") != "" {
t.Skipf("TEST_SKIP_SENDMAIL variable is set. Skipping sendmail test")
}
tests := []struct {
name string
sp string
sf bool
}{
{"Sendmail path: /dev/null", "/dev/null", true},
{"Sendmail path: /bin/cat", "/bin/cat", true},
{"Sendmail path: /is/invalid", "/is/invalid", true},
{"Sendmail path: /bin/echo", "/bin/echo", false},
}
m := NewMsg()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx, cfn := context.WithTimeout(context.Background(), time.Second*10)
defer cfn()
m.SetBodyString(TypeTextPlain, "Plain")
if err := m.WriteToSendmailWithContext(ctx, tt.sp); err != nil && !tt.sf {
t.Errorf("WriteToSendmailWithCommand() failed: %s", err)
}
m.Reset()
})
}
}
// TestMsg_WriteToSendmail will test the output to the local sendmail command
func TestMsg_WriteToSendmail(t *testing.T) {
if os.Getenv("TEST_SKIP_SENDMAIL") != "" {
t.Skipf("TEST_SKIP_SENDMAIL variable is set. Skipping sendmail test")
}
_, err := os.Stat(SendmailPath)
if err != nil {
t.Skipf("local sendmail command not found in expected path. Skipping")
}
m := NewMsg()
_ = m.From("Toni Tester <tester@example.com>")
_ = m.To(TestRcpt)
m.SetBodyString(TypeTextPlain, "This is a test")
if err := m.WriteToSendmail(); err != nil {
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")
}
}

File diff suppressed because it is too large Load diff

146
msg_unix_test.go Normal file
View file

@ -0,0 +1,146 @@
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT
//go:build linux || freebsd
// +build linux freebsd
package mail
import (
"bytes"
"errors"
"os"
"testing"
)
func TestMsg_AttachFile_unixOnly(t *testing.T) {
t.Run("AttachFile with fileFromFS fails on open", func(t *testing.T) {
if os.Getenv("PERFORM_UNIX_OPEN_WRITE_TESTS") != "true" {
t.Skipf("PERFORM_UNIX_OPEN_WRITE_TESTS variable is not set. Skipping unix open/write tests")
}
tempFile, err := os.CreateTemp("", "attachfile-open-write-test.*.txt")
if err != nil {
t.Fatalf("failed to create temp file: %s", err)
}
t.Cleanup(func() {
if err := os.Remove(tempFile.Name()); err != nil {
t.Errorf("failed to remove temp file: %s", err)
}
})
if err = os.Chmod(tempFile.Name(), 0o000); err != nil {
t.Fatalf("failed to chmod temp file: %s", err)
}
message := NewMsg()
if message == nil {
t.Fatal("message is nil")
}
message.AttachFile(tempFile.Name())
attachments := message.GetAttachments()
if len(attachments) != 1 {
t.Fatalf("failed to get attachments, expected 1, got: %d", len(attachments))
}
messageBuf := bytes.NewBuffer(nil)
_, err = attachments[0].Writer(messageBuf)
if err == nil {
t.Error("writer func expected to fail, but didn't")
}
if !errors.Is(err, os.ErrPermission) {
t.Errorf("expected error to be %s, got: %s", os.ErrPermission, err)
}
})
}
func TestMsg_EmbedFile_unixOnly(t *testing.T) {
t.Run("EmbedFile with fileFromFS fails on open", func(t *testing.T) {
if os.Getenv("PERFORM_UNIX_OPEN_WRITE_TESTS") != "true" {
t.Skipf("PERFORM_UNIX_OPEN_WRITE_TESTS variable is not set. Skipping unix open/write tests")
}
tempFile, err := os.CreateTemp("", "embedfile-open-write-test.*.txt")
if err != nil {
t.Fatalf("failed to create temp file: %s", err)
}
t.Cleanup(func() {
if err := os.Remove(tempFile.Name()); err != nil {
t.Errorf("failed to remove temp file: %s", err)
}
})
if err = os.Chmod(tempFile.Name(), 0o000); err != nil {
t.Fatalf("failed to chmod temp file: %s", err)
}
message := NewMsg()
if message == nil {
t.Fatal("message is nil")
}
message.EmbedFile(tempFile.Name())
embeds := message.GetEmbeds()
if len(embeds) != 1 {
t.Fatalf("failed to get embeds, expected 1, got: %d", len(embeds))
}
messageBuf := bytes.NewBuffer(nil)
_, err = embeds[0].Writer(messageBuf)
if err == nil {
t.Error("writer func expected to fail, but didn't")
}
if !errors.Is(err, os.ErrPermission) {
t.Errorf("expected error to be %s, got: %s", os.ErrPermission, err)
}
})
}
func TestMsg_WriteToFile_unixOnly(t *testing.T) {
t.Run("WriteToFile fails on create", func(t *testing.T) {
if os.Getenv("PERFORM_UNIX_OPEN_WRITE_TESTS") != "true" {
t.Skipf("PERFORM_UNIX_OPEN_WRITE_TESTS variable is not set. Skipping unix open/write tests")
}
tempfile, err := os.CreateTemp("", "testmail-create.*.eml")
if err != nil {
t.Fatalf("failed to create temp file: %s", err)
}
if err = os.Chmod(tempfile.Name(), 0o000); err != nil {
t.Fatalf("failed to chmod temp file: %s", err)
}
t.Cleanup(func() {
if err = tempfile.Close(); err != nil {
t.Fatalf("failed to close temp file: %s", err)
}
if err = os.Remove(tempfile.Name()); err != nil {
t.Fatalf("failed to remove temp file: %s", err)
}
})
message := testMessage(t)
if err = message.WriteToFile(tempfile.Name()); err == nil {
t.Errorf("expected error, got nil")
}
})
}
func TestMsg_WriteToTempFile_unixOnly(t *testing.T) {
if os.Getenv("PERFORM_UNIX_OPEN_WRITE_TESTS") != "true" {
t.Skipf("PERFORM_UNIX_OPEN_WRITE_TESTS variable is not set. Skipping unix open/write tests")
}
t.Run("WriteToTempFile fails on invalid TMPDIR", func(t *testing.T) {
// We store the current TMPDIR variable so we can set it back when the test is over
curTmpDir := os.Getenv("TMPDIR")
t.Cleanup(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.Fatalf("failed to set TMPDIR environment variable: %s", err)
}
message := testMessage(t)
_, err := message.WriteToTempFile()
if err == nil {
t.Errorf("expected writing to invalid TMPDIR to fail, got: %s", err)
}
})
}

View file

@ -6,151 +6,673 @@ package mail
import (
"bytes"
"errors"
"fmt"
"io"
"mime"
"runtime"
"strings"
"testing"
"time"
)
// brokenWriter implements a broken writer for io.Writer testing
type brokenWriter struct {
io.Writer
}
// Write implements the io.Writer interface but intentionally returns an error at
// any time
func (bw *brokenWriter) Write([]byte) (int, error) {
return 0, fmt.Errorf("intentionally failed")
}
// TestMsgWriter_Write tests the WriteTo() method of the msgWriter
func TestMsgWriter_Write(t *testing.T) {
bw := &brokenWriter{}
mw := &msgWriter{writer: bw, charset: CharsetUTF8, encoder: mime.QEncoding}
_, err := mw.Write([]byte("test"))
if err == nil {
t.Errorf("msgWriter WriteTo() with brokenWriter should fail, but didn't")
}
// Also test the part when a previous error happened
mw.err = fmt.Errorf("broken")
_, err = mw.Write([]byte("test"))
if err == nil {
t.Errorf("msgWriter WriteTo() with brokenWriter should fail, but didn't")
}
}
// TestMsgWriter_writeMsg tests the writeMsg method of the msgWriter
func TestMsgWriter_writeMsg(t *testing.T) {
m := NewMsg()
_ = m.From(`"Toni Tester" <test@example.com>`)
_ = m.To(`"Toni Receiver" <receiver@example.com>`)
m.Subject("This is a subject")
m.SetBulk()
now := time.Now()
m.SetDateWithValue(now)
m.SetMessageIDWithValue("message@id.com")
m.SetBodyString(TypeTextPlain, "This is the body")
m.AddAlternativeString(TypeTextHTML, "This is the alternative body")
buf := bytes.Buffer{}
mw := &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding}
mw.writeMsg(m)
ms := buf.String()
var ea []string
if !strings.Contains(ms, `MIME-Version: 1.0`) {
ea = append(ea, "MIME-Version")
}
if !strings.Contains(ms, fmt.Sprintf("Date: %s", now.Format(time.RFC1123Z))) {
ea = append(ea, "Date")
}
if !strings.Contains(ms, `Message-ID: <message@id.com>`) {
ea = append(ea, "Message-ID")
}
if !strings.Contains(ms, `Precedence: bulk`) {
ea = append(ea, "Precedence")
}
if !strings.Contains(ms, `Subject: This is a subject`) {
ea = append(ea, "Subject")
}
if !strings.Contains(ms, `User-Agent: go-mail v`) {
ea = append(ea, "User-Agent")
}
if !strings.Contains(ms, `X-Mailer: go-mail v`) {
ea = append(ea, "X-Mailer")
}
if !strings.Contains(ms, `From: "Toni Tester" <test@example.com>`) {
ea = append(ea, "From")
}
if !strings.Contains(ms, `To: "Toni Receiver" <receiver@example.com>`) {
ea = append(ea, "To")
}
if !strings.Contains(ms, `Content-Type: text/plain; charset=UTF-8`) {
ea = append(ea, "Content-Type")
}
if !strings.Contains(ms, `Content-Transfer-Encoding: quoted-printable`) {
ea = append(ea, "Content-Transfer-Encoding")
}
if !strings.Contains(ms, "\r\n\r\nThis is the body") {
ea = append(ea, "Message body")
}
pl := m.GetParts()
if len(pl) <= 0 {
t.Errorf("expected multiple parts but got none")
return
}
if len(pl) == 2 {
ap := pl[1]
ap.SetCharset(CharsetISO88591)
}
buf.Reset()
mw.writeMsg(m)
ms = buf.String()
if !strings.Contains(ms, "\r\n\r\nThis is the alternative body") {
ea = append(ea, "Message alternative body")
}
if !strings.Contains(ms, `Content-Type: text/html; charset=ISO-8859-1`) {
ea = append(ea, "alternative body charset")
}
if len(ea) > 0 {
em := "writeMsg() failed. The following errors occurred:\n"
for e := range ea {
em += fmt.Sprintf("* incorrect %q field", ea[e])
t.Run("msgWriter writes to memory for all charsets", func(t *testing.T) {
for _, tt := range charsetTests {
t.Run(tt.name, func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter := &msgWriter{
writer: buffer,
charset: tt.value,
encoder: mime.QEncoding,
}
_, err := msgwriter.Write([]byte("test"))
if err != nil {
t.Errorf("msgWriter failed to write: %s", err)
}
})
}
em += fmt.Sprintf("\n\nFull message:\n%s", ms)
t.Error(em)
}
})
t.Run("msgWriter writes to memory for all encodings", func(t *testing.T) {
for _, tt := range encodingTests {
t.Run(tt.name, func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter := &msgWriter{
writer: buffer,
charset: CharsetUTF8,
encoder: getEncoder(tt.value),
}
_, err := msgwriter.Write([]byte("test"))
if err != nil {
t.Errorf("msgWriter failed to write: %s", err)
}
})
}
})
t.Run("msgWriter should fail on write", func(t *testing.T) {
msgwriter := &msgWriter{
writer: failReadWriteSeekCloser{},
charset: CharsetUTF8,
encoder: getEncoder(EncodingQP),
}
_, err := msgwriter.Write([]byte("test"))
if err == nil {
t.Fatalf("msgWriter was supposed to fail on write")
}
})
t.Run("msgWriter should fail on previous error", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter := &msgWriter{
writer: buffer,
charset: CharsetUTF8,
encoder: getEncoder(EncodingQP),
}
_, err := msgwriter.Write([]byte("test"))
if err != nil {
t.Errorf("msgWriter failed to write: %s", err)
}
msgwriter.err = errors.New("intentionally failed")
_, err = msgwriter.Write([]byte("test2"))
if err == nil {
t.Fatalf("msgWriter was supposed to fail on second write")
}
})
}
// TestMsgWriter_writeMsg_PGP tests the writeMsg method of the msgWriter with PGP types set
func TestMsgWriter_writeMsg_PGP(t *testing.T) {
m := NewMsg(WithPGPType(PGPEncrypt))
_ = m.From(`"Toni Tester" <test@example.com>`)
_ = m.To(`"Toni Receiver" <receiver@example.com>`)
m.Subject("This is a subject")
m.SetBodyString(TypeTextPlain, "This is the body")
buf := bytes.Buffer{}
mw := &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding}
mw.writeMsg(m)
ms := buf.String()
if !strings.Contains(ms, `encrypted; protocol="application/pgp-encrypted"`) {
t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output")
func TestMsgWriter_writeMsg(t *testing.T) {
msgwriter := &msgWriter{
charset: CharsetUTF8,
encoder: getEncoder(EncodingQP),
}
t.Run("msgWriter writes a simple message", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
now := time.Now()
msgwriter.writer = buffer
message := testMessage(t)
message.SetDateWithValue(now)
message.SetMessageIDWithValue("message@id.com")
message.SetBulk()
msgwriter.writeMsg(message)
if msgwriter.err != nil {
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
}
m = NewMsg(WithPGPType(PGPSignature))
_ = m.From(`"Toni Tester" <test@example.com>`)
_ = m.To(`"Toni Receiver" <receiver@example.com>`)
m.Subject("This is a subject")
m.SetBodyString(TypeTextPlain, "This is the body")
buf = bytes.Buffer{}
mw = &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding}
mw.writeMsg(m)
ms = buf.String()
if !strings.Contains(ms, `signed; protocol="application/pgp-signature"`) {
t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output")
}
var incorrectFields []string
if !strings.Contains(buffer.String(), "MIME-Version: 1.0\r\n") {
incorrectFields = append(incorrectFields, "MIME-Version")
}
if !strings.Contains(buffer.String(), fmt.Sprintf("Date: %s\r\n", now.Format(time.RFC1123Z))) {
incorrectFields = append(incorrectFields, "Date")
}
if !strings.Contains(buffer.String(), "Message-ID: <message@id.com>\r\n") {
incorrectFields = append(incorrectFields, "Message-ID")
}
if !strings.Contains(buffer.String(), "Precedence: bulk\r\n") {
incorrectFields = append(incorrectFields, "Precedence")
}
if !strings.Contains(buffer.String(), "X-Auto-Response-Suppress: All\r\n") {
incorrectFields = append(incorrectFields, "X-Auto-Response-Suppress")
}
if !strings.Contains(buffer.String(), "Subject: Testmail\r\n") {
incorrectFields = append(incorrectFields, "Subject")
}
if !strings.Contains(buffer.String(), "User-Agent: go-mail v") {
incorrectFields = append(incorrectFields, "User-Agent")
}
if !strings.Contains(buffer.String(), "X-Mailer: go-mail v") {
incorrectFields = append(incorrectFields, "X-Mailer")
}
if !strings.Contains(buffer.String(), `From: <`+TestSenderValid+`>`) {
incorrectFields = append(incorrectFields, "From")
}
if !strings.Contains(buffer.String(), `To: <`+TestRcptValid+`>`) {
incorrectFields = append(incorrectFields, "From")
}
if !strings.Contains(buffer.String(), "Content-Type: text/plain; charset=UTF-8\r\n") {
incorrectFields = append(incorrectFields, "Content-Type")
}
if !strings.Contains(buffer.String(), "Content-Transfer-Encoding: quoted-printable\r\n") {
incorrectFields = append(incorrectFields, "Content-Transfer-Encoding")
}
if !strings.HasSuffix(buffer.String(), "\r\n\r\nTestmail") {
incorrectFields = append(incorrectFields, "Message body")
}
if len(incorrectFields) > 0 {
t.Fatalf("msgWriter failed to write correct fields: %s - mail: %s",
strings.Join(incorrectFields, ", "), buffer.String())
}
})
t.Run("msgWriter with no from address uses envelope from", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
message := NewMsg()
if message == nil {
t.Fatal("failed to create new message")
}
if err := message.EnvelopeFrom(TestSenderValid); err != nil {
t.Errorf("failed to set sender address: %s", err)
}
if err := message.To(TestRcptValid); err != nil {
t.Errorf("failed to set recipient address: %s", err)
}
message.Subject("Testmail")
message.SetBodyString(TypeTextPlain, "Testmail")
msgwriter.writeMsg(message)
if msgwriter.err != nil {
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
}
if !strings.Contains(buffer.String(), "From: <"+TestSenderValid+">") {
t.Errorf("expected envelope from address as from address, got: %s", buffer.String())
}
})
t.Run("msgWriter with no from address or envelope from", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
message := NewMsg()
if message == nil {
t.Fatal("failed to create new message")
}
msgwriter.writeMsg(message)
if msgwriter.err != nil {
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
}
if strings.Contains(buffer.String(), "From:") {
t.Errorf("expected no from address, got: %s", buffer.String())
}
})
t.Run("msgWriter writes a multipart/mixed message", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
message := testMessage(t, WithBoundary("testboundary"))
message.AttachFile("testdata/attachment.txt")
msgwriter.writeMsg(message)
if msgwriter.err != nil {
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
}
if !strings.Contains(buffer.String(), "Content-Type: multipart/mixed") {
t.Errorf("expected multipart/mixed, got: %s", buffer.String())
}
if !strings.Contains(buffer.String(), "--testboundary\r\n") {
t.Errorf("expected boundary, got: %s", buffer.String())
}
if !strings.Contains(buffer.String(), "--testboundary--") {
t.Errorf("expected end boundary, got: %s", buffer.String())
}
})
t.Run("msgWriter writes a multipart/related message", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
message := testMessage(t, WithBoundary("testboundary"))
message.EmbedFile("testdata/embed.txt")
msgwriter.writeMsg(message)
if msgwriter.err != nil {
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
}
if !strings.Contains(buffer.String(), "Content-Type: multipart/related") {
t.Errorf("expected multipart/related, got: %s", buffer.String())
}
if !strings.Contains(buffer.String(), "--testboundary\r\n") {
t.Errorf("expected boundary, got: %s", buffer.String())
}
if !strings.Contains(buffer.String(), "--testboundary--") {
t.Errorf("expected end boundary, got: %s", buffer.String())
}
})
t.Run("msgWriter writes a multipart/alternative message", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
message := testMessage(t, WithBoundary("testboundary"))
message.AddAlternativeString(TypeTextHTML, "<html><body><h1>Testmail</h1></body></html>")
msgwriter.writeMsg(message)
if msgwriter.err != nil {
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
}
if !strings.Contains(buffer.String(), "Content-Type: multipart/alternative") {
t.Errorf("expected multipart/alternative, got: %s", buffer.String())
}
if !strings.Contains(buffer.String(), "--testboundary\r\n") {
t.Errorf("expected boundary, got: %s", buffer.String())
}
if !strings.Contains(buffer.String(), "--testboundary--") {
t.Errorf("expected end boundary, got: %s", buffer.String())
}
})
t.Run("msgWriter writes a application/pgp-encrypted message", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
message := testMessage(t, WithPGPType(PGPEncrypt), WithBoundary("testboundary"))
msgwriter.writeMsg(message)
if msgwriter.err != nil {
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
}
if !strings.Contains(buffer.String(), "Content-Type: multipart/encrypted") {
t.Errorf("expected multipart/encrypted, got: %s", buffer.String())
}
if !strings.Contains(buffer.String(), "--testboundary\r\n") {
t.Errorf("expected boundary, got: %s", buffer.String())
}
})
t.Run("msgWriter writes a application/pgp-signature message", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
message := testMessage(t, WithPGPType(PGPSignature), WithBoundary("testboundary"))
msgwriter.writeMsg(message)
if msgwriter.err != nil {
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
}
if !strings.Contains(buffer.String(), "Content-Type: multipart/signed") {
t.Errorf("expected multipart/signed, got: %s", buffer.String())
}
if !strings.Contains(buffer.String(), "--testboundary\r\n") {
t.Errorf("expected boundary, got: %s", buffer.String())
}
})
t.Run("msgWriter should ignore NoPGP", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
message := testMessage(t, WithBoundary("testboundary"))
message.pgptype = 9
msgwriter.writeMsg(message)
if msgwriter.err != nil {
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
}
if !strings.Contains(buffer.String(), "--testboundary\r\n") {
t.Errorf("expected boundary, got: %s", buffer.String())
}
})
}
func TestMsgWriter_writePreformattedGenHeader(t *testing.T) {
t.Run("message with no preformatted headerset", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter := &msgWriter{
writer: buffer,
charset: CharsetUTF8,
encoder: getEncoder(EncodingQP),
}
message := testMessage(t)
message.SetGenHeaderPreformatted(HeaderContentID, "This is a content id")
msgwriter.writeMsg(message)
if !strings.Contains(buffer.String(), "Content-ID: This is a content id\r\n") {
t.Errorf("expected preformatted header, got: %s", buffer.String())
}
})
}
func TestMsgWriter_addFiles(t *testing.T) {
msgwriter := &msgWriter{
charset: CharsetUTF8,
encoder: getEncoder(EncodingQP),
}
t.Run("message with a single file attached", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
message := testMessage(t)
message.AttachFile("testdata/attachment.txt")
msgwriter.writeMsg(message)
if msgwriter.err != nil {
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
}
switch runtime.GOOS {
case "windows":
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudA0K") {
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
}
default:
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo=") {
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
}
}
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
}
switch runtime.GOOS {
case "freebsd":
if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment.txt"`) {
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
}
default:
if !strings.Contains(buffer.String(), `Content-Type: text/plain; charset=utf-8; name="attachment.txt"`) {
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
}
}
})
t.Run("message with a single file attached no extension", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
message := testMessage(t)
message.AttachFile("testdata/attachment")
msgwriter.writeMsg(message)
if msgwriter.err != nil {
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
}
switch runtime.GOOS {
case "windows":
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudA0K") {
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
}
default:
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo=") {
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
}
}
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment"`) {
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
}
if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment"`) {
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
}
})
t.Run("message with a single file attached custom content-type", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
message := testMessage(t)
message.AttachFile("testdata/attachment.txt", WithFileContentType(TypeAppOctetStream))
msgwriter.writeMsg(message)
if msgwriter.err != nil {
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
}
switch runtime.GOOS {
case "windows":
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudA0K") {
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
}
default:
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo=") {
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
}
}
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
}
if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment.txt"`) {
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
}
})
t.Run("message with a single file attached custom transfer-encoding", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
message := testMessage(t)
message.AttachFile("testdata/attachment.txt", WithFileEncoding(EncodingUSASCII))
msgwriter.writeMsg(message)
if msgwriter.err != nil {
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
}
if !strings.Contains(buffer.String(), "\r\n\r\nThis is a test attachment") {
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
}
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
}
switch runtime.GOOS {
case "freebsd":
if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment.txt"`) {
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
}
default:
if !strings.Contains(buffer.String(), `Content-Type: text/plain; charset=utf-8; name="attachment.txt"`) {
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
}
}
if !strings.Contains(buffer.String(), `Content-Transfer-Encoding: 7bit`) {
t.Errorf("Content-Transfer-Encoding header not found for attachment. Mail: %s", buffer.String())
}
})
t.Run("message with a single file attached custom description", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
message := testMessage(t)
message.AttachFile("testdata/attachment.txt", WithFileDescription("Testdescription"))
msgwriter.writeMsg(message)
if msgwriter.err != nil {
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
}
switch runtime.GOOS {
case "windows":
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudA0K") {
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
}
default:
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo=") {
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
}
}
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
}
switch runtime.GOOS {
case "freebsd":
if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment.txt"`) {
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
}
default:
if !strings.Contains(buffer.String(), `Content-Type: text/plain; charset=utf-8; name="attachment.txt"`) {
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
}
}
if !strings.Contains(buffer.String(), `Content-Transfer-Encoding: base64`) {
t.Errorf("Content-Transfer-Encoding header not found for attachment. Mail: %s", buffer.String())
}
if !strings.Contains(buffer.String(), `Content-Description: Testdescription`) {
t.Errorf("Content-Description header not found for attachment. Mail: %s", buffer.String())
}
})
t.Run("message with attachment but no body part", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
message := testMessage(t)
message.parts = nil
message.AttachFile("testdata/attachment.txt")
msgwriter.writeMsg(message)
if msgwriter.err != nil {
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
}
switch runtime.GOOS {
case "windows":
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudA0K") {
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
}
default:
if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo=") {
t.Errorf("attachment not found in mail message. Mail: %s", buffer.String())
}
}
if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) {
t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String())
}
switch runtime.GOOS {
case "freebsd":
if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment.txt"`) {
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
}
default:
if !strings.Contains(buffer.String(), `Content-Type: text/plain; charset=utf-8; name="attachment.txt"`) {
t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String())
}
}
if !strings.Contains(buffer.String(), `Content-Transfer-Encoding: base64`) {
t.Errorf("Content-Transfer-Encoding header not found for attachment. Mail: %s", buffer.String())
}
})
}
func TestMsgWriter_writePart(t *testing.T) {
msgwriter := &msgWriter{
charset: CharsetUTF8,
encoder: getEncoder(EncodingQP),
}
t.Run("message with no part charset should use default message charset", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
message := testMessage(t, WithCharset(CharsetUTF7))
message.AddAlternativeString(TypeTextPlain, "thisisatest")
message.parts[1].charset = ""
msgwriter.writeMsg(message)
if msgwriter.err != nil {
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
}
if !strings.Contains(buffer.String(), "ontent-Type: text/plain; charset=UTF-7\r\n\r\nTestmail") {
t.Errorf("part not found in mail message. Mail: %s", buffer.String())
}
if !strings.Contains(buffer.String(), "ontent-Type: text/plain; charset=UTF-7\r\n\r\nthisisatest") {
t.Errorf("part not found in mail message. Mail: %s", buffer.String())
}
})
t.Run("message with parts that have a description", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
message := testMessage(t)
message.AddAlternativeString(TypeTextPlain, "thisisatest")
message.parts[1].description = "thisisadescription"
msgwriter.writeMsg(message)
if msgwriter.err != nil {
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
}
if !strings.Contains(buffer.String(), "Content-Description: thisisadescription") {
t.Errorf("part description not found in mail message. Mail: %s", buffer.String())
}
})
}
func TestMsgWriter_writeString(t *testing.T) {
msgwriter := &msgWriter{
charset: CharsetUTF8,
encoder: getEncoder(EncodingQP),
}
t.Run("writeString succeeds", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
msgwriter.writeString("thisisatest")
if !strings.EqualFold(buffer.String(), "thisisatest") {
t.Errorf("writeString failed, expected: thisisatest got: %s", buffer.String())
}
})
t.Run("writeString fails", func(t *testing.T) {
msgwriter.writer = failReadWriteSeekCloser{}
msgwriter.writeString("thisisatest")
if msgwriter.err == nil {
t.Errorf("writeString succeeded, expected error")
}
})
t.Run("writeString on errored writer should return", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
msgwriter.err = errors.New("intentional error")
msgwriter.writeString("thisisatest")
if !strings.EqualFold(buffer.String(), "") {
t.Errorf("writeString succeeded, expected: empty string, got: %s", buffer.String())
}
})
}
func TestMsgWriter_writeHeader(t *testing.T) {
msgwriter := &msgWriter{
charset: CharsetUTF8,
encoder: getEncoder(EncodingQP),
}
t.Run("writeHeader with single value", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
msgwriter.writeHeader(HeaderMessageID, "this.is.a.test")
if !strings.EqualFold(buffer.String(), "Message-ID: this.is.a.test\r\n") {
t.Errorf("writeHeader failed, expected: %s, got: %s", "Message-ID: this.is.a.test",
buffer.String())
}
})
t.Run("writeHeader with multiple values", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
msgwriter.writeHeader(HeaderMessageID, "this.is.a.test", "this.as.well")
if !strings.EqualFold(buffer.String(), "Message-ID: this.is.a.test, this.as.well\r\n") {
t.Errorf("writeHeader failed, expected: %s, got: %s", "Message-ID: this.is.a.test, this.as.well",
buffer.String())
}
})
t.Run("writeHeader with no values", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
msgwriter.writeHeader(HeaderMessageID)
// While technically it is permitted to have empty headers, it's recommend to omit them if
// no value is present. We follow this recommendation.
if !strings.EqualFold(buffer.String(), "") {
t.Errorf("writeHeader failed, expected: %s, got: %s", "", buffer.String())
}
})
t.Run("writeHeader with very long value", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
msgwriter.writeHeader(HeaderMessageID, strings.Repeat("a", MaxHeaderLength-13), "next-row")
want := "Message-ID:\r\n " + strings.Repeat("a", MaxHeaderLength-13) + ",\r\n next-row\r\n"
if !strings.EqualFold(buffer.String(), want) {
t.Errorf("writeHeader failed, expected: %s, got: %s", want, buffer.String())
}
})
}
func TestMsgWriter_writeBody(t *testing.T) {
t.Log("We only cover some edge-cases here, most of the functionality is tested already very thoroughly.")
msgwriter := &msgWriter{
charset: CharsetUTF8,
encoder: getEncoder(EncodingQP),
}
t.Run("writeBody on NoEncoding", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
message := testMessage(t)
msgwriter.writeBody(message.parts[0].writeFunc, NoEncoding)
if msgwriter.err != nil {
t.Errorf("writeBody failed to write: %s", msgwriter.err)
}
})
t.Run("writeBody on NoEncoding fails on write", func(t *testing.T) {
msgwriter.writer = failReadWriteSeekCloser{}
message := testMessage(t)
msgwriter.writeBody(message.parts[0].writeFunc, NoEncoding)
if msgwriter.err == nil {
t.Errorf("writeBody succeeded, expected error")
}
if !strings.EqualFold(msgwriter.err.Error(), "bodyWriter io.Copy: intentional write failure") {
t.Errorf("expected error: bodyWriter io.Copy: intentional write failure, got: %s", msgwriter.err)
}
})
t.Run("writeBody on NoEncoding fails on writeFunc", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
writeFunc := func(io.Writer) (int64, error) {
return 0, errors.New("intentional write failure")
}
msgwriter.writeBody(writeFunc, NoEncoding)
if msgwriter.err == nil {
t.Errorf("writeBody succeeded, expected error")
}
if !strings.EqualFold(msgwriter.err.Error(), "bodyWriter function: intentional write failure") {
t.Errorf("expected error: bodyWriter function: intentional write failure, got: %s", msgwriter.err)
}
})
t.Run("writeBody Quoted-Printable fails on write", func(t *testing.T) {
msgwriter.writer = failReadWriteSeekCloser{}
message := testMessage(t)
msgwriter.writeBody(message.parts[0].writeFunc, EncodingQP)
if msgwriter.err == nil {
t.Errorf("writeBody succeeded, expected error")
}
if !strings.EqualFold(msgwriter.err.Error(), "bodyWriter function: intentional write failure") {
t.Errorf("expected error: bodyWriter function: intentional write failure, got: %s", msgwriter.err)
}
})
t.Run("writeBody Quoted-Printable fails on writeFunc", func(t *testing.T) {
buffer := bytes.NewBuffer(nil)
msgwriter.writer = buffer
writeFunc := func(io.Writer) (int64, error) {
return 0, errors.New("intentional write failure")
}
msgwriter.writeBody(writeFunc, EncodingQP)
if msgwriter.err == nil {
t.Errorf("writeBody succeeded, expected error")
}
if !strings.EqualFold(msgwriter.err.Error(), "bodyWriter function: intentional write failure") {
t.Errorf("expected error: bodyWriter function: intentional write failure, got: %s", msgwriter.err)
}
})
}

View file

@ -554,9 +554,9 @@ func (c *Client) Noop() error {
// Quit sends the QUIT command and closes the connection to the server.
func (c *Client) Quit() error {
if err := c.hello(); err != nil {
return err
}
// See https://github.com/golang/go/issues/70011
_ = c.hello() // ignore error; we're quitting anyhow
_, _, err := c.cmd(221, "QUIT")
if err != nil {
return err

View file

@ -900,6 +900,35 @@ Goodbye.
QUIT
`
func TestHELOFailed(t *testing.T) {
serverLines := `502 EH?
502 EH?
221 OK
`
clientLines := `EHLO localhost
HELO localhost
QUIT
`
server := strings.Join(strings.Split(serverLines, "\n"), "\r\n")
client := strings.Join(strings.Split(clientLines, "\n"), "\r\n")
var cmdbuf strings.Builder
bcmdbuf := bufio.NewWriter(&cmdbuf)
var fake faker
fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
c := &Client{Text: textproto.NewConn(fake), localName: "localhost"}
if err := c.Hello("localhost"); err == nil {
t.Fatal("expected EHLO to fail")
}
if err := c.Quit(); err != nil {
t.Errorf("QUIT failed: %s", err)
}
_ = bcmdbuf.Flush()
actual := cmdbuf.String()
if client != actual {
t.Errorf("Got:\n%s\nWant:\n%s", actual, client)
}
}
func TestExtensions(t *testing.T) {
fake := func(server string) (c *Client, bcmdbuf *bufio.Writer, cmdbuf *strings.Builder) {
server = strings.Join(strings.Split(server, "\n"), "\r\n")

View file

@ -0,0 +1,8 @@
From: §§§§§§§§
To: Mary Smith <mary@example.net>
Subject: Saying Hello
Date: Fri, 21 Nov 1997 09:55:06 -0600
Message-ID: <1234@local.machine.example>
This is a message just to say hello.
So, "Hello".

View file

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

8
testdata/RFC5322-A1-1.eml vendored Normal file
View file

@ -0,0 +1,8 @@
From: John Doe <jdoe@machine.example>
To: Mary Smith <mary@example.net>
Subject: Saying Hello
Date: Fri, 21 Nov 1997 09:55:06 -0600
Message-ID: <1234@local.machine.example>
This is a message just to say hello.
So, "Hello".

3
testdata/RFC5322-A1-1.eml.license vendored Normal file
View file

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

1
testdata/attachment vendored Normal file
View file

@ -0,0 +1 @@
This is a test attachment

3
testdata/attachment.license vendored Normal file
View file

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

1
testdata/attachment.txt vendored Normal file
View file

@ -0,0 +1 @@
This is a test attachment

3
testdata/attachment.txt.license vendored Normal file
View file

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

1
testdata/embed.txt vendored Normal file
View file

@ -0,0 +1 @@
This is a test embed

3
testdata/embed.txt.license vendored Normal file
View file

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

1
testdata/logo.svg vendored Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

367
testdata/logo.svg.base64 vendored Normal file
View file

@ -0,0 +1,367 @@
PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PCFE
T0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53
My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHdpZHRoPSIxMDAlIiBo
ZWlnaHQ9IjEwMCUiIHZpZXdCb3g9IjAgMCA2MDAgNjAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJo
dHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3Jn
LzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiIHhtbG5zOnNlcmlmPSJodHRwOi8vd3d3
LnNlcmlmLmNvbS8iIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZDtjbGlwLXJ1bGU6ZXZlbm9kZDtz
dHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGlt
aXQ6MS41OyI+PHJlY3QgaWQ9ItCc0L7QvdGC0LDQttC90LDRjy3QvtCx0LvQsNGB0YLRjDEiIHNl
cmlmOmlkPSLQnNC+0L3RgtCw0LbQvdCw0Y8g0L7QsdC70LDRgdGC0YwxIiB4PSIwIiB5PSIwIiB3
aWR0aD0iNjAwIiBoZWlnaHQ9IjYwMCIgc3R5bGU9ImZpbGw6bm9uZTsiLz48Zz48cGF0aCBkPSJN
NTg0LjcyNiw5OC4zOTVjLTAsLTkuNzM4IC03LjkwNywtMTcuNjQ1IC0xNy42NDUsLTE3LjY0NWwt
NTEwLjAyNywwYy05LjczOSwwIC0xNy42NDUsNy45MDcgLTE3LjY0NSwxNy42NDVsLTAsMzE5Ljc5
NGMtMCw5LjczOSA3LjkwNiwxNy42NDYgMTcuNjQ1LDE3LjY0Nmw1MTAuMDI3LC0wYzkuNzM4LC0w
IDE3LjY0NSwtNy45MDcgMTcuNjQ1LC0xNy42NDZsLTAsLTMxOS43OTRaIiBzdHlsZT0iZmlsbDoj
MmMyYzJjO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDoyLjFweDsiLz48cGF0aCBkPSJNNTg1LjEy
Niw5OC4zOTVjLTAsLTkuNzM4IC03LjkwNywtMTcuNjQ1IC0xNy42NDUsLTE3LjY0NWwtNDk0Ljgz
OSwwYy05LjczOCwwIC0xNy42NDUsNy45MDcgLTE3LjY0NSwxNy42NDVsMCwzMTkuNzk0YzAsOS43
MzkgNy45MDcsMTcuNjQ2IDE3LjY0NSwxNy42NDZsNDk0LjgzOSwtMGM5LjczOCwtMCAxNy42NDUs
LTcuOTA3IDE3LjY0NSwtMTcuNjQ2bC0wLC0zMTkuNzk0WiIgc3R5bGU9ImZpbGw6IzM3MzczNztz
dHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Mi4wN3B4OyIvPjxwYXRoIGQ9Ik01NjcuMjU5LDExNC40
NzJjLTAsLTguNzYgLTcuMTEyLC0xNS44NzIgLTE1Ljg3MiwtMTUuODcybC00NjIuNjUxLDBjLTgu
NzYsMCAtMTUuODcyLDcuMTEyIC0xNS44NzIsMTUuODcybDAsMjg3LjY0MWMwLDguNzYgNy4xMTIs
MTUuODcxIDE1Ljg3MiwxNS44NzFsNDYyLjY1MSwwYzguNzYsMCAxNS44NzIsLTcuMTExIDE1Ljg3
MiwtMTUuODcxbC0wLC0yODcuNjQxWiIgc3R5bGU9ImZpbGw6I2ZmZjJlYjtzdHJva2U6IzAwMDtz
dHJva2Utd2lkdGg6MS45cHg7Ii8+PHBhdGggZD0iTTIzNi4xNDUsNDM1Ljg3OGwtMjMuNDIsOTEu
MDA4bDEzMC43NjUsLTBsMjIuMzY1LC05MC43NTZsLTEyOS43MSwtMC4yNTJaIiBzdHlsZT0iZmls
bDojMmMyYzJjO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDoyLjQ3cHg7Ii8+PHBhdGggZD0iTTI0
NS4zOTcsNDM1Ljg3OGwtMjMuNDIxLDkxLjAwOGwxMzAuNzY2LC0wbDIyLjM2NSwtOTAuNzU2bC0x
MjkuNzEsLTAuMjUyWiIgc3R5bGU9ImZpbGw6IzM3MzczNztzdHJva2U6IzAwMDtzdHJva2Utd2lk
dGg6Mi40N3B4OyIvPjxwYXRoIGQ9Ik00NzIuNTg0LDUzMS4wOTNjLTAsLTIuODI3IC0yLjI5NSwt
NS4xMjEgLTUuMTIxLC01LjEyMWwtMjkyLjU0MSwtMGMtMi44MjYsLTAgLTUuMTIxLDIuMjk0IC01
LjEyMSw1LjEyMWwwLDEwLjI0M2MwLDIuODI2IDIuMjk1LDUuMTIxIDUuMTIxLDUuMTIxbDI5Mi41
NDEsLTBjMi44MjYsLTAgNS4xMjEsLTIuMjk1IDUuMTIxLC01LjEyMWwtMCwtMTAuMjQzWiIgc3R5
bGU9ImZpbGw6IzJjMmMyYztzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Mi40N3B4OyIvPjxnPjxw
YXRoIGQ9Ik0xOTAuMzc5LDQzMS40NTVjLTAsLTEuOTE2IC0xLjU1NiwtMy40NzIgLTMuNDcyLC0z
LjQ3MmwtNTQuNTYzLDBjLTEuOTE3LDAgLTMuNDcyLDEuNTU2IC0zLjQ3MiwzLjQ3MmwtMCw0Ni4z
MDljLTAsMS45MTYgMS41NTUsMy40NzIgMy40NzIsMy40NzJsNTQuNTYzLC0wYzEuOTE2LC0wIDMu
NDcyLC0xLjU1NiAzLjQ3MiwtMy40NzJsLTAsLTQ2LjMwOVoiIHN0eWxlPSJmaWxsOiNlZWFjMDE7
c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjMuMXB4OyIvPjxnPjxwYXRoIGQ9Ik0xMzkuOTk2LDQ0
Mi44NTNjLTAsMC40MiAtMC4wNzEsMC45MjIgMC4wNjMsMS4zMjNjMC41MywxLjU5MiAyLjc1LDEu
Mzg2IDQuMDk1LDEuMzg2YzIuMzk3LC0wIDQuNjc0LC0wLjEwOCA2LjQ4OSwtMS44MjdjMC40NSwt
MC40MjYgMC45NjYsLTAuOTQgMS4wNzIsLTEuNTc1YzAuMDYxLC0wLjM2NyAwLjEwMiwtMS4yMDgg
LTAuMTI2LC0xLjUxMmMtMC44MDMsLTEuMDcxIC0yLjg0NCwtMC4zNzMgLTMuNjU1LDAuMjUyYy0w
LjcxNCwwLjU1IC0wLjgyMiwxLjgyNiAtMC45NDUsMi42NDZjLTAuNDA2LDIuNzA1IDAuMzg3LDUu
MDMgMy4xNSw1Ljg1OWMzLjQwNSwxLjAyMSA2Ljc4NCwwLjA2IDEwLjA4NiwtMC44MDljMS41OTcs
LTAuNDIxIDMuNjYsLTAuNzcgNS4wMTcsLTEuNzJjMC40NTIsLTAuMzE2IDAuNDQ0LC0wLjI4NCAw
LjkwMSwtMC41NThjMS4yMDgsLTAuNzI1IDMuODY5LC0xLjkyNyAyLjQ1NywtMy41OTFjLTAuMTks
LTAuMjI0IC0xLjEzOSwtMC4yMzcgLTEuMjYsMC4xMjZjLTAuNDIsMS4yNTcgLTEuMzI2LDMuMzAy
IDAuNTY3LDMuNzhjMS4zOTgsMC4zNTMgMi45MTksMC4xODkgNC4zNDcsMC4xODljMC4xOTMsLTAg
MS40ODUsLTAuMjc5IDEuNzAxLC0wLjA2M2MwLjU1NSwwLjU1NSAxLjU0MywwLjgxMSAyLjMzMSwx
LjAwOGMwLjkzMiwwLjIzMyAxLjk0NCwwLjYwNiAyLjg5OSwwLjY5M2MwLjg2OSwwLjA3OSAxLjc5
LC0wLjA0NSAyLjY0NiwwLjEyNiIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Ut
d2lkdGg6MS45NnB4OyIvPjxwYXRoIGQ9Ik0xNDIuNzA1LDQ1Mi42ODFjMC4xMTksMC41NTEgMC44
OTMsMC42NjkgMS4zMTgsMC44NDZjMS41OTgsMC42NjYgMy41MTQsMC44NzQgNS4yMzQsMC45ODJj
Mi4zNzgsMC4xNDggNC43MDQsLTAuMTU5IDcuMDU3LC0wLjMxNWMxLjg2MSwtMC4xMjUgMy43NDYs
MC4xMTYgNS42MDcsLTBjMi4wNTEsLTAuMTI5IDQuMzIxLC0wLjM3NyA2LjM2NCwtMC4wNjNjMS4w
NzIsMC4xNjQgMi4wODksMC40OCAzLjE1LDAuNjkzIiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZToj
MDAwO3N0cm9rZS13aWR0aDoxLjk2cHg7Ii8+PHBhdGggZD0iTTE3Ni4zNDksNDU1LjA3NmMxLjEx
MywtMC4wMjEgMi4yMjYsLTAuMDQyIDMuMzQsLTAuMDYzIiBzdHlsZT0iZmlsbDpub25lO3N0cm9r
ZTojMDAwO3N0cm9rZS13aWR0aDoxLjk2cHg7Ii8+PHBhdGggZD0iTTE0NS4xNjIsNDU5Ljg2NGwy
MS42NzQsLTAiIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjEuOTZw
eDsiLz48L2c+PC9nPjxnPjxnPjxwYXRoIGQ9Ik03Ni4yNzMsMTIxLjQ5NGw0ODcuNTU3LC0wLjAx
NSIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6I2Y0ZGRkMDtzdHJva2Utd2lkdGg6NC45M3B4O3N0
cm9rZS1saW5lY2FwOnNxdWFyZTsiLz48cGF0aCBkPSJNMjMwLjk1MSwyMjQuMTc1Yy0wLjgwMiwt
My4zMjcgLTguMDU5LC03Ljk4NCAtMTEuMDA1LC04Ljk0NWMtMzUuODQ2LC0xMS42OTkgLTIwLjc4
MywyNy4zNzIgLTAuNjE4LDI0LjM0MiIgc3R5bGU9ImZpbGw6IzYxZTBlZTsiLz48cGF0aCBkPSJN
MjM0Ljk0NCwyMjMuMjEzYy0wLjM3OSwtMS41NzQgLTEuNTIyLC0zLjQzNiAtMy4yNzYsLTUuMTc0
Yy0zLjA2MSwtMy4wMzMgLTguMDE0LC01LjkyIC0xMC40NDgsLTYuNzE0Yy0xMy4xMzUsLTQuMjg3
IC0yMC42MTgsLTEuOTk0IC0yNC4wNzEsMS44MTljLTMuNjM4LDQuMDE3IC0zLjgzNiwxMC40OTIg
LTAuODI1LDE2LjY1MWMzLjk4MSw4LjE0MyAxMy4zMjEsMTUuMzg2IDIzLjYxNSwxMy44MzljMi4y
NDIsLTAuMzM3IDMuNzg4LC0yLjQzIDMuNDUxLC00LjY3MmMtMC4zMzcsLTIuMjQyIC0yLjQzLC0z
Ljc4OSAtNC42NzIsLTMuNDUyYy02LjY0NywwLjk5OSAtMTIuNDQ0LC00LjA2NSAtMTUuMDE0LC05
LjMyM2MtMC44MDEsLTEuNjM3IC0xLjI5MywtMy4zMDggLTEuMzAyLC00Ljg0N2MtMC4wMDUsLTEu
MDIgMC4xOTUsLTEuOTc0IDAuODM2LC0yLjY4MmMwLjgzMiwtMC45MTggMi4yMjUsLTEuMzU0IDQu
MTMzLC0xLjQ4MWMyLjg1MiwtMC4xOTEgNi41NjcsMC40MTIgMTEuMywxLjk1N2MxLjQ1NiwwLjQ3
NSA0LjE2LDIuMDk2IDYuMjU3LDMuODY5YzAuNjM2LDAuNTM3IDEuMjE1LDEuMDg4IDEuNjU4LDEu
NjM2YzAuMTUyLDAuMTg4IDAuMzMxLDAuMzI3IDAuMzcyLDAuNDk4YzAuNTMxLDIuMjA0IDIuNzUx
LDMuNTYyIDQuOTU1LDMuMDMxYzIuMjA0LC0wLjUzMSAzLjU2MiwtMi43NTIgMy4wMzEsLTQuOTU1
WiIvPjxwYXRoIGQ9Ik0zNDkuMzI2LDEyMy41MjdjMCwwIC0xNS4zNSwtMTMuODYyIC0xOC4xMTQs
MjIuODM0IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojNjFlMGVlO3N0cm9rZS13aWR0aDo2LjE2
cHg7Ii8+PHBhdGggZD0iTTI5OS42NjIsMTIzLjQ4NWMtMCwtMCAxMi42NzEsLTE1LjI2NSAyMC4x
MTIsMjEuODk4IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojNjFlMGVlO3N0cm9rZS13aWR0aDo2
LjE2cHg7Ii8+PHBhdGggZD0iTTMzNi4xMTEsMTA4Ljc3M2MtMCwwIC0xNi44MDgsLTkuMDk5IC0x
MC44ODIsMzIuMTI0IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojNjFlMGVlO3N0cm9rZS13aWR0
aDo2LjE2cHg7Ii8+PHBhdGggZD0iTTQxNy43OTYsMTg0LjQ1M2MzLjI4MiwwLjk3IDYuNTgzLC0x
LjI5IDguODg5LC0zLjM2MWMyOC4wNTUsLTI1LjE5NCAtMTMuMTk4LC0zMS41MzggLTIwLjY1Niwt
MTIuNTU5IiBzdHlsZT0iZmlsbDojNjFlMGVlOyIvPjxwYXRoIGQ9Ik00MTYuNjMyLDE4OC4zOTNj
NC42NTcsMS4zNzUgOS41MjYsLTEuMzA3IDEyLjc5NywtNC4yNDVjMTAuMjc5LC05LjIzIDEyLjA0
NiwtMTYuODQzIDEwLjQ4OCwtMjEuNzE2Yy0xLjY0NSwtNS4xNSAtNy4xNDIsLTguNTQ0IC0xMy45
NzgsLTguOTljLTkuMDExLC0wLjU4NyAtMTkuOTI3LDMuOTA0IC0yMy43MzMsMTMuNTg5Yy0wLjgz
LDIuMTEgMC4yMSw0LjQ5NiAyLjMyLDUuMzI1YzIuMTEsMC44MjkgNC40OTYsLTAuMjExIDUuMzI1
LC0yLjMyMWMyLjQ2LC02LjI1OSA5LjczLC04Ljc3NSAxNS41NTMsLTguMzk1YzEuODA3LDAuMTE3
IDMuNDg5LDAuNTE3IDQuODE3LDEuMjY5YzAuODcxLDAuNDk0IDEuNTg2LDEuMTMgMS44NzEsMi4w
MjNjMC4zNzUsMS4xNzIgMC4wNDksMi41ODggLTAuNzk1LDQuMjk4Yy0xLjI2NiwyLjU2MiAtMy42
NDksNS40NzcgLTcuMzU2LDguODA2Yy0wLjg2OCwwLjc4IC0xLjkwNywxLjYxOCAtMy4wNTQsMi4x
NDdjLTAuNjE2LDAuMjg0IC0xLjI2NSwwLjUyNyAtMS45MjgsMC4zMzFjLTIuMTc0LC0wLjY0MiAt
NC40NjEsMC42MDIgLTUuMTAzLDIuNzc2Yy0wLjY0MiwyLjE3NCAwLjYwMiw0LjQ2MSAyLjc3Niw1
LjEwM1oiLz48cGF0aCBkPSJNNDM2LjAxNCw0MTkuNjQ1YzMuNjAxLC0xMC45MzQgOS4wMDMsLTMx
LjYwOSA1LjY4MiwtNDkuNTYzYy0xNC45NTMsLTgwLjg0NyA3Ljg5NSwtOTguNTQ5IC01LjAzNSwt
MTQ3LjU5M2MtNC40OSwtMTcuMDI5IC0xMi4wNDksLTM2LjI1IC0yMy41NTQsLTQ5Ljc5MmMtMjUu
Mjg1LC0yOS43NjIgLTcwLjczNiwtNDIuOTEzIC0xMDguNzMsLTMyLjc4NWMtMTUuMjEzLDQuMDU1
IC0yOC42NjIsMTEuNzI3IC00MS45MjMsMjAuMDM0Yy00Ni4xMzYsMjguOSAtNTIuMjU5LDgzLjM3
NyAtNDIuNzkxLDEyOC4yNDhjNC4xOTEsMTkuODY0IDIzLjY3Myw0OS44OTcgMjIuNTg2LDcwLjE5
NGMtMS4zODcsMjUuOTEgLTAuNzY1LDQ0LjQyNiAzLjM3Niw2MC44ODQiIHN0eWxlPSJmaWxsOiM2
MWUwZWU7Ii8+PGNsaXBQYXRoIGlkPSJfY2xpcDEiPjxwYXRoIGQ9Ik00MzYuMDE0LDQxOS42NDVj
My42MDEsLTEwLjkzNCA5LjAwMywtMzEuNjA5IDUuNjgyLC00OS41NjNjLTE0Ljk1MywtODAuODQ3
IDcuODk1LC05OC41NDkgLTUuMDM1LC0xNDcuNTkzYy00LjQ5LC0xNy4wMjkgLTEyLjA0OSwtMzYu
MjUgLTIzLjU1NCwtNDkuNzkyYy0yNS4yODUsLTI5Ljc2MiAtNzAuNzM2LC00Mi45MTMgLTEwOC43
MywtMzIuNzg1Yy0xNS4yMTMsNC4wNTUgLTI4LjY2MiwxMS43MjcgLTQxLjkyMywyMC4wMzRjLTQ2
LjEzNiwyOC45IC01Mi4yNTksODMuMzc3IC00Mi43OTEsMTI4LjI0OGM0LjE5MSwxOS44NjQgMjMu
NjczLDQ5Ljg5NyAyMi41ODYsNzAuMTk0Yy0xLjM4NywyNS45MSAtMC43NjUsNDQuNDI2IDMuMzc2
LDYwLjg4NCIvPjwvY2xpcFBhdGg+PGcgY2xpcC1wYXRoPSJ1cmwoI19jbGlwMSkiPjxwYXRoIGQ9
Ik0zMTEuNzc2LDIzNi44MzVjMCwwIC0zLjA2OCwyNC4xOTkgLTMxLjU5NywyNC44MzhjLTI4LjUz
LDAuNjM5IC0zMi41OTEsLTI4LjI0NiAtMzIuNTkxLC0yOC4yNDZsNjQuMTg4LDMuNDA4WiIgc3R5
bGU9ImZpbGw6IzRmY2JlMDsiLz48cGF0aCBkPSJNNDMyLjUwOSwyMTYuMDIzYzAsLTAgLTMuMDY3
LDI0LjE5OCAtMzEuNTk3LDI0LjgzN2MtMjguNTMsMC42MzkgLTMyLjU5MSwtMjguMjQ2IC0zMi41
OTEsLTI4LjI0Nmw2NC4xODgsMy40MDlaIiBzdHlsZT0iZmlsbDojNGZjYmUwOyIvPjxwYXRoIGQ9
Ik0yODYuODQ1LDQxOC45NjFsMTI3Ljg4NiwtMGMwLjM4NiwtMS4yNjcgMC42NjYsLTIuNDMyIDAu
ODcsLTMuNDcxYzMuODY3LC0xOS43NDMgMy43MjksLTU3LjQ2OCAtMi40MTUsLTc3LjA5NWMtMi4x
MzMsLTYuODE2IC01Ljc5MywtMTQuNDkgLTExLjQ3NiwtMTkuODUyYy0xMi40OSwtMTEuNzg1IC02
NC4zOTgsLTkuMTgxIC04My41NjEsLTQuODM1Yy03LjY3MywxLjc0MSAtMTQuNDg3LDQuOTIyIC0y
MS4yMSw4LjM1OGMtMTQuOTExLDcuNjIgLTIwLjI1Myw1OC43MTIgLTEwLjA5NCw5Ni44OTVaIiBz
dHlsZT0iZmlsbDojYjdmOGZmOyIvPjxwYXRoIGQ9Ik0yNDYuNzExLDQxOC45NjFjLTUuMTA4LC0y
MC42MTMgLTYuNDksLTQyLjk3NSAtNS41NjIsLTYwLjg1NWMyLjEzNiwtNDEuMTMzIC0zMS4zMTQs
LTUwLjQ1MyAtMjUuNDYxLC0xMjEuNTEzYzE0LjUzNCwtNDIuNDA5IDE1LjA4OSwtNDAuMTA3IDI3
LjY1MywtNTUuNDQ1YzAsMCAtMTMuMTQ3LDM5Ljk2OCAtMS4xOTMsNzAuNzI4YzExLjk1NCwzMC43
NTkgMzIuMTc0LDczLjQxOCAzMi41OSwxMTcuNDkxYzAuMTg2LDE5Ljc3MSAxLjYzNCwzNi4zMSA0
LjEzLDQ5LjU5NGwtMzIuMTU3LC0wWiIgc3R5bGU9ImZpbGw6IzRmY2JlMDsiLz48L2c+PHBhdGgg
ZD0iTTQzOS45MTUsNDIwLjkzYzMuNzQ4LC0xMS4zOCA5LjI3NiwtMzIuOTA4IDUuODIsLTUxLjU5
NWMtOC44NjUsLTQ3LjkzMyAtNC4yNTksLTczLjQwNSAtMS45MzUsLTk2LjU4YzEuNjEzLC0xNi4w
OTMgMi4xNTYsLTMxLjEyMiAtMy4xNjcsLTUxLjMxM2MtNC42MzgsLTE3LjU5MSAtMTIuNTExLC0z
Ny40MTYgLTI0LjM5NiwtNTEuNDA1Yy0yNi4yNjEsLTMwLjkxMSAtNzMuNDU2LC00NC42MTMgLTEx
Mi45MTcsLTM0LjA5NGMtMTUuNjE0LDQuMTYxIC0yOS40MzcsMTEuOTk2IC00My4wNDcsMjAuNTIy
Yy00Ny43MzUsMjkuOTAyIC01NC40MjYsODYuMTUgLTQ0LjYyOSwxMzIuNTc3YzIuMTM5LDEwLjEz
OCA4LjEzNiwyMi44ODggMTMuNTA0LDM1LjY3NmM0Ljk5NCwxMS45IDkuNTE1LDIzLjgxIDguOTk5
LDMzLjQ1Yy0xLjQxNSwyNi40MzIgLTAuNzMsNDUuMzE3IDMuNDk1LDYyLjEwN2wxLjAwMiwzLjk4
M2w3Ljk2NywtMi4wMDVsLTEuMDAyLC0zLjk4M2MtNC4wNTgsLTE2LjEyNyAtNC42MTgsLTM0LjI3
NCAtMy4yNTgsLTU5LjY2M2MwLjQ2LC04LjYwOSAtMi40NiwtMTguODk1IC02LjU0NSwtMjkuNDU3
Yy01LjY5NCwtMTQuNzI0IC0xMy42NDYsLTMwLjA1OSAtMTYuMTI0LC00MS44MDRjLTkuMTQsLTQz
LjMxNSAtMy41ODQsLTk2LjAyMiA0MC45NTIsLTEyMy45MTljMTIuOTEyLC04LjA4OSAyNS45ODks
LTE1LjU5OCA0MC44MDEsLTE5LjU0N2MzNi41MjYsLTkuNzM1IDgwLjIzNCwyLjg2NSAxMDQuNTQx
LDMxLjQ3NmMxMS4xMjYsMTMuMDk2IDE4LjM3MiwzMS43MTMgMjIuNzEzLDQ4LjE4YzYuMzEsMjMu
OTMyIDMuODIsNDAuMjE5IDEuNjQ3LDYwLjM4M2MtMi4yNTcsMjAuOTQ5IC00LjI1OSw0NS45Mjcg
My4zMjEsODYuOTFjMy4xODYsMTcuMjIyIC0yLjA5LDM3LjA0MyAtNS41NDQsNDcuNTMxbC0xLjI4
NSwzLjkwMmw3LjgwMywyLjU2OWwxLjI4NCwtMy45MDFaIi8+PHBhdGggZD0iTTQyNC41MzksMjQy
LjI0N2MtMS44NjgsMC4xMzQgLTMuMjc2LDEuNzU4IC0zLjE0MiwzLjYyNmMwLjEzMywxLjg2NyAx
Ljc1NywzLjI3NSAzLjYyNSwzLjE0MWMxLjg2NywtMC4xMzMgMy4yNzUsLTEuNzU3IDMuMTQyLC0z
LjYyNWMtMC4xMzQsLTEuODY3IC0xLjc1OCwtMy4yNzUgLTMuNjI1LC0zLjE0MloiIHN0eWxlPSJm
aWxsOiNiN2Y4ZmY7Ii8+PHBhdGggZD0iTTI1Ni45OTcsMjYwLjM5MWMtMS44NjgsMC4xMzMgLTMu
Mjc1LDEuNzU4IC0zLjE0MiwzLjYyNWMwLjEzMywxLjg2OCAxLjc1OCwzLjI3NSAzLjYyNSwzLjE0
MmMxLjg2OCwtMC4xMzMgMy4yNzUsLTEuNzU4IDMuMTQyLC0zLjYyNWMtMC4xMzMsLTEuODY4IC0x
Ljc1OCwtMy4yNzUgLTMuNjI1LC0zLjE0MloiIHN0eWxlPSJmaWxsOiNiN2Y4ZmY7Ii8+PGc+PHBh
dGggZD0iTTM2NC4wOSwyNjkuMjI5Yy0xLjE4Niw0Ljk2NyAtMS40NDMsMTQuMzA2IC00LjMxNSwx
OC40Yy0xLjM5MywxLjk4NSAtNC40OTUsMi4wODYgLTYuNTE2LDIuMTUzYy0xMS42MTIsMC4zODMg
LTExLjQ0NiwtMTEuNTEzIC0xMy4xODEsLTIxLjg1NSIgc3R5bGU9ImZpbGw6I2VkZWRlZDsiLz48
cGF0aCBkPSJNMzYxLjA5NCwyNjguNTEzYy0wLjcxOSwzLjAwOSAtMS4xMDYsNy42MDcgLTEuOTIy
LDExLjY5OGMtMC40NDUsMi4yMjggLTAuOTcxLDQuMjk4IC0xLjkxOSw1LjY0OWMtMC4yNjMsMC4z
NzYgLTAuNzQ5LDAuNDU5IC0xLjIwMywwLjU2NmMtMC45OTUsMC4yMzMgLTIuMDU2LDAuMjUgLTIu
ODkzLDAuMjc3Yy0yLjA1NSwwLjA2OCAtMy42MSwtMC4zMjYgLTQuNzc0LC0xLjE5MmMtMS4xOTMs
LTAuODg3IC0xLjk1NCwtMi4yMTIgLTIuNTQ0LC0zLjc0M2MtMS41NjEsLTQuMDQ3IC0xLjg5LC05
LjM5IC0yLjcyMywtMTQuMzUxYy0wLjI4MSwtMS42NzcgLTEuODcxLC0yLjgxIC0zLjU0OCwtMi41
MjhjLTEuNjc3LDAuMjgxIC0yLjgwOSwxLjg3MSAtMi41MjgsMy41NDhjMC45MDMsNS4zODEgMS4z
NTgsMTEuMTU4IDMuMDUxLDE1LjU0OGMxLjAzNiwyLjY4OCAyLjUyLDQuOTEyIDQuNjE1LDYuNDdj
Mi4xMjUsMS41ODEgNC45MDMsMi41MyA4LjY1NCwyLjQwNmMxLjQ2OCwtMC4wNDggMy40LC0wLjE1
MiA1LjA3OSwtMC43MTRjMS41NiwtMC41MjIgMi45MTksLTEuNDEgMy44NTgsLTIuNzQ5YzEuMzUs
LTEuOTI0IDIuMjg0LC00LjgwOCAyLjkxNywtNy45ODFjMC44LC00LjAxMSAxLjE2OCwtOC41MjIg
MS44NzMsLTExLjQ3M2MwLjM5NSwtMS42NTMgLTAuNjI3LC0zLjMxNyAtMi4yODEsLTMuNzEyYy0x
LjY1NCwtMC4zOTUgLTMuMzE3LDAuNjI3IC0zLjcxMiwyLjI4MVoiLz48cGF0aCBkPSJNMzQ5LjAw
MSwyMzQuMDVjLTkuMzMyLDEuMjQ2IC0xNi4yNjUsNy4wNzUgLTE1LjQ3MywxMy4wMDhjMC43OTIs
NS45MzMgOS4wMTIsOS43MzggMTguMzQ0LDguNDkyYzkuMzMzLC0xLjI0NiAxNi4yNjYsLTcuMDc1
IDE1LjQ3NCwtMTMuMDA4Yy0wLjc5MiwtNS45MzMgLTkuMDEyLC05LjczOCAtMTguMzQ1LC04LjQ5
MloiLz48cGF0aCBkPSJNMzQ4LjkwMSwyNTIuNTVjMC40Niw1LjIwNCAxLjY5NCwxMC4xNDIgMi40
NTMsMTUuMjc3YzAuMjQ4LDEuNjgyIDEuODE1LDIuODQ2IDMuNDk3LDIuNTk4YzEuNjgyLC0wLjI0
OCAyLjg0NiwtMS44MTYgMi41OTgsLTMuNDk4Yy0wLjc0MSwtNS4wMTUgLTEuOTYxLC05LjgzNyAt
Mi40MTEsLTE0LjkyYy0wLjE1LC0xLjY5NCAtMS42NDcsLTIuOTQ3IC0zLjM0LC0yLjc5N2MtMS42
OTQsMC4xNSAtMi45NDcsMS42NDcgLTIuNzk3LDMuMzRaIi8+PHBhdGggZD0iTTM3NC4yNiwyNTku
ODE0Yy04LjAyMiw1LjM1IC0xNS45MDEsNy44MjUgLTIzLjU5NCw3LjI3NGMtNy42OTQsLTAuNTUx
IC0xNS4xNDgsLTQuMTI0IC0yMi4zNDIsLTEwLjU4OWMtMS4yNjUsLTEuMTM2IC0zLjIxNCwtMS4w
MzIgLTQuMzUxLDAuMjMzYy0xLjEzNiwxLjI2NCAtMS4wMzIsMy4yMTQgMC4yMzMsNC4zNWM4LjM1
Niw3LjUwOCAxNy4wODQsMTEuNTExIDI2LjAyLDEyLjE1MWM4LjkzNiwwLjY0IDE4LjEzNSwtMi4w
NzggMjcuNDUzLC04LjI5NGMxLjQxNCwtMC45NDMgMS43OTcsLTIuODU4IDAuODUzLC00LjI3MmMt
MC45NDMsLTEuNDE1IC0yLjg1OCwtMS43OTcgLTQuMjcyLC0wLjg1M1oiLz48cGF0aCBkPSJNMzc4
LjYwNSwxNTIuMTU2Yy0zLjY0NywtMC44MjYgLTYuOTU2LDAuMDM4IC03LjM4NSwxLjkyOWMtMC40
MjgsMS44OTEgMi4xODUsNC4wOTcgNS44MzIsNC45MjRjMy42NDcsMC44MjcgNi45NTYsLTAuMDM3
IDcuMzg1LC0xLjkyOGMwLjQyOCwtMS44OTEgLTIuMTg1LC00LjA5OCAtNS44MzIsLTQuOTI1WiIg
c3R5bGU9ImZpbGw6I2I3ZjhmZjsiLz48cGF0aCBkPSJNMjQ5LjUsMTc3LjI3Yy0yLjE5NSwzLjAy
OCAtMi43MDIsNi40MSAtMS4xMzIsNy41NDhjMS41NywxLjEzOCA0LjYyNiwtMC4zOTYgNi44MjEs
LTMuNDI0YzIuMTk1LC0zLjAyOCAyLjcwMiwtNi40MSAxLjEzMiwtNy41NDhjLTEuNTcsLTEuMTM4
IC00LjYyNiwwLjM5NiAtNi44MjEsMy40MjRaIiBzdHlsZT0iZmlsbDojYjdmOGZmOyIvPjxwYXRo
IGQ9Ik00MDMuNjQ4LDE2Mi44MjVjLTE5LjIxMywtMS4zNSAtMzUuOTA3LDEzLjE1MSAtMzcuMjU3
LDMyLjM2NGMtMS4zNSwxOS4yMTIgMTMuMTUyLDM1LjkwNiAzMi4zNjQsMzcuMjU2YzE5LjIxMywx
LjM1MSAzNS45MDcsLTEzLjE1MSAzNy4yNTcsLTMyLjM2NGMxLjM1LC0xOS4yMTIgLTEzLjE1Miwt
MzUuOTA2IC0zMi4zNjQsLTM3LjI1NloiIHN0eWxlPSJmaWxsOiNmZmY7c3Ryb2tlOiMwMDA7c3Ry
b2tlLXdpZHRoOjYuMTZweDsiLz48cGF0aCBkPSJNMjgxLjY5NiwxODEuNzhjLTE5LjIxMiwtMS4z
NSAtMzUuOTA2LDEzLjE1MiAtMzcuMjU3LDMyLjM2NGMtMS4zNSwxOS4yMTIgMTMuMTUyLDM1Ljkw
NyAzMi4zNjUsMzcuMjU3YzE5LjIxMiwxLjM1IDM1LjkwNiwtMTMuMTUyIDM3LjI1NiwtMzIuMzY0
YzEuMzUsLTE5LjIxMyAtMTMuMTUyLC0zNS45MDcgLTMyLjM2NCwtMzcuMjU3WiIgc3R5bGU9ImZp
bGw6I2ZmZjtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Ni4xNnB4OyIvPjxwYXRoIGQ9Ik0zOTgu
NDk5LDE5Mi44ODFjLTYuODI0LC0wLjQ3OSAtMTIuNzU0LDQuNjcyIC0xMy4yMzMsMTEuNDk2Yy0w
LjQ4LDYuODI0IDQuNjcxLDEyLjc1MyAxMS40OTUsMTMuMjMzYzYuODI0LDAuNDc5IDEyLjc1NCwt
NC42NzIgMTMuMjMzLC0xMS40OTZjMC40OCwtNi44MjQgLTQuNjcxLC0xMi43NTMgLTExLjQ5NSwt
MTMuMjMzWiIvPjxwYXRoIGQ9Ik0yNzguMTQyLDIxMC4yMjdjLTYuODI0LC0wLjQ3OSAtMTIuNzU0
LDQuNjcxIC0xMy4yMzMsMTEuNDk2Yy0wLjQ4LDYuODI0IDQuNjcxLDEyLjc1MyAxMS40OTUsMTMu
MjMzYzYuODI0LDAuNDc5IDEyLjc1NCwtNC42NzIgMTMuMjMzLC0xMS40OTZjMC40OCwtNi44MjQg
LTQuNjcxLC0xMi43NTMgLTExLjQ5NSwtMTMuMjMzWiIvPjxwYXRoIGQ9Ik0zOTkuODkyLDE5Ny45
NjFjLTEuOTE2LC0wLjEzNSAtMy41ODEsMS4zMTEgLTMuNzE2LDMuMjI3Yy0wLjEzNCwxLjkxNiAx
LjMxMiwzLjU4MSAzLjIyOCwzLjcxNmMxLjkxNiwwLjEzNCAzLjU4MSwtMS4zMTIgMy43MTUsLTMu
MjI4YzAuMTM1LC0xLjkxNiAtMS4zMTEsLTMuNTgxIC0zLjIyNywtMy43MTVaIiBzdHlsZT0iZmls
bDojZmZmOyIvPjxwYXRoIGQ9Ik0yNzkuNTM1LDIxNS4zMDZjLTEuOTE2LC0wLjEzNCAtMy41ODEs
MS4zMTIgLTMuNzE1LDMuMjI4Yy0wLjEzNSwxLjkxNiAxLjMxMSwzLjU4MSAzLjIyNywzLjcxNmMx
LjkxNiwwLjEzNCAzLjU4MSwtMS4zMTIgMy43MTYsLTMuMjI4YzAuMTM0LC0xLjkxNiAtMS4zMTIs
LTMuNTgxIC0zLjIyOCwtMy43MTZaIiBzdHlsZT0iZmlsbDojZmZmOyIvPjwvZz48L2c+PHBhdGgg
ZD0iTTUxMi4yOTEsMzMzLjcyNmMwLC0wIC0yNy40OTYsMTMuNDAyIC00My4yNDUsMy43OTJjLTE1
Ljc1LC05LjYxIC0xMC44LC0yNC43ODggMy4xMDIsLTIyLjUyYzEzLjkwMywyLjI2OCAyNy4xNjgs
My4xOTUgMzUuOTQzLC0zLjkzN2M4Ljc3NiwtNy4xMzIgMjEuNjMzLDE0LjM3NiA0LjIsMjIuNjY1
WiIgc3R5bGU9ImZpbGw6I2Q2YzI5NTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Ni4xNnB4OyIv
PjwvZz48Zz48cGF0aCBkPSJNNDQxLjI2OCwyOTEuMzQ5Yy0wLDAgMjguMDgsNDcuNDg4IC0xMjIu
NTkxLDExNC4wNGMtMTEuNjI5LC0yLjAxNyAtMTQuMzMsLTguNTE1IC0xNC4zMywtOC41MTVjMCwt
MCA0My42MTcsLTEyLjU4NiA4My44MTIsLTQ0Ljg3NmM0My41OTcsLTM1LjAyNCA1My4xMDksLTYw
LjY0OSA1My4xMDksLTYwLjY0OVoiIHN0eWxlPSJmaWxsOiM3YjQ3MjI7c3Ryb2tlOiMwMDA7c3Ry
b2tlLXdpZHRoOjYuNjhweDsiLz48cGF0aCBkPSJNMjMzLjk1NywzNzIuMTM4YzMuNTYsLTUuNTA2
IDEwLjcyOSwtNy4zOTIgMTYuNTM4LC00LjM1MWMxNy43NjcsOS4yMzUgNTMuNjE0LDI4LjAzMiA2
Ny4xNTQsMzYuMzc2YzUuOTcxLDMuNjggLTYuNDc3LDE2LjkxMyAtMTkuMjgzLDM3LjkyN2MtMy4z
NjgsNS41NjcgLTEwLjQzNiw3LjYyOCAtMTYuMjY5LDQuNzQ2Yy0yMS42NTYsLTEwLjcxNiAtNDEu
MDQ3LC0yMC45NTggLTU0LjIzNCwtMjguMDczYy00LjU5MiwtMi41MDEgLTcuOTM0LC02LjgwMiAt
OS4yMjMsLTExLjg2OWMtMS4yODgsLTUuMDY4IC0wLjQwNywtMTAuNDQzIDIuNDMzLC0xNC44MzNj
NC4zMzUsLTYuNzA4IDkuMTYyLC0xNC4xNyAxMi44ODQsLTE5LjkyM1oiIHN0eWxlPSJmaWxsOiM3
YjQ3MjI7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48cGF0aCBkPSJNMjM5Ljg1
LDM2MS4xNzFsLTI1Ljg0OCwyMy4yOTZsNzkuMTcsNDUuNjA1bDI1LjE5NiwtMjQuMjk1bC03OC41
MTgsLTQ0LjYwNloiIHN0eWxlPSJmaWxsOiNhZDZjNGE7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRo
OjQuOTVweDsiLz48cGF0aCBkPSJNMjM4LjA4MSwzNzcuNzA0YzIuNTI2LDAuNTMgNC4xNDcsMy4w
MTEgMy42MTgsNS41MzdjLTAuNTMsMi41MjYgLTMuMDExLDQuMTQ3IC01LjUzOCwzLjYxOGMtMi41
MjYsLTAuNTMgLTQuMTQ3LC0zLjAxMSAtMy42MTcsLTUuNTM4YzAuNTMsLTIuNTI2IDMuMDExLC00
LjE0NyA1LjUzNywtMy42MTdaIiBzdHlsZT0iZmlsbDojZmZiMjVjO3N0cm9rZTojMDAwO3N0cm9r
ZS13aWR0aDo0Ljk1cHg7Ii8+PHBhdGggZD0iTTI5MC4zMyw0MDcuOWMyLjUyNywwLjUzIDQuMTQ3
LDMuMDExIDMuNjE4LDUuNTM3Yy0wLjUzLDIuNTI3IC0zLjAxMSw0LjE0NyAtNS41MzcsMy42MThj
LTIuNTI3LC0wLjUzIC00LjE0OCwtMy4wMTEgLTMuNjE4LC01LjUzN2MwLjUzLC0yLjUyNyAzLjAx
MSwtNC4xNDggNS41MzcsLTMuNjE4WiIgc3R5bGU9ImZpbGw6I2ZmYjI1YztzdHJva2U6IzAwMDtz
dHJva2Utd2lkdGg6NC45NXB4OyIvPjwvZz48Zz48cGF0aCBkPSJNMzA1LjMyMywzMzYuMjM1YzAs
MCAyNC4yMDQsMTEyLjU3OCAzNS4zNTcsMTE0LjM1NWMxMS4xNTMsMS43NzYgMTgyLjk5OCwtNTcu
MTYyIDE4Mi45OTgsLTU3LjE2MmMwLC0wIC0yNC4yNywtMTA1Ljc3MyAtMzQuNDU2LC0xMTQuNjAy
Yy0xMC4xODYsLTguODMgLTE4My44OTksNTcuNDA5IC0xODMuODk5LDU3LjQwOVoiIHN0eWxlPSJm
aWxsOiNmZmYyZWI7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuNjhweDsiLz48cGF0aCBkPSJN
NDEzLjU2MywzNjcuNjU2bC0xMDguMjQsLTMxLjQyMWwxMDguMjQsMzEuNDIxWiIgc3R5bGU9ImZp
bGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6NS40NnB4OyIvPjxwYXRoIGQ9Ik00ODgu
NTY2LDI3OS4xNTZsLTc1LjAwMyw4OC41IiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwO3N0
cm9rZS13aWR0aDo1LjQ2cHg7Ii8+PHBhdGggZD0iTTMzOS4xNjcsNDQ5LjEyN2w2Mi40MDEsLTgz
LjUwNSIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6NS40NnB4OyIv
PjxwYXRoIGQ9Ik00MjMuMjcsMzU1LjgzNWw5OS4xOTQsMzcuMDAxIiBzdHlsZT0iZmlsbDpub25l
O3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo1LjQ2cHg7Ii8+PC9nPjxwYXRoIGQ9Ik0zMDQuNDE2
LDM3NC43ODVjLTAsMCAtMzAuNDYsLTIuOCAtMzguOTQzLC0xOS4xODVjLTguNDgyLC0xNi4zODQg
My42MjIsLTI2Ljc5NCAxNC4zMzIsLTE3LjY0NmMxMC43MTEsOS4xNDkgMjEuNTcyLDE2LjgyMSAz
Mi43NzQsMTUuMjc0YzExLjIwMSwtMS41NDcgMTEuMDQsMjMuNTExIC04LjE2MywyMS41NTdaIiBz
dHlsZT0iZmlsbDojZDZjMjk1O3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo2LjE2cHg7Ii8+PHBh
dGggZD0iTTEwOS45NTEsMzExLjcxMmw2OC4wNzYsLTAiIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tl
OiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48cGF0aCBkPSJNMTM5LjMwNSwzNTEuNjgzbDY4
LjA3NiwwIiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo0Ljk1cHg7
Ii8+PHBhdGggZD0iTTEzOS42MTcsMzMxLjY5OGwzOC4wOTgsLTAiIHN0eWxlPSJmaWxsOm5vbmU7
c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48cGF0aCBkPSJNODUuMjAxLDIxMS4y
NTNjLTAsLTIuMjEzIC0xLjc5NywtNC4wMSAtNC4wMSwtNC4wMWwtNTMuNDg3LC0wYy0yLjIxNCwt
MCAtNC4wMTEsMS43OTcgLTQuMDExLDQuMDFsMCw1OC44OTRjMCwyLjIxMyAxLjc5Nyw0LjAxIDQu
MDExLDQuMDFsNTMuNDg3LDBjMi4yMTMsMCA0LjAxLC0xLjc5NyA0LjAxLC00LjAxbC0wLC01OC44
OTRaIiBzdHlsZT0iZmlsbDojZWVhYzAxO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDozLjQzcHg7
Ii8+PHBhdGggZD0iTTU2NC42NDMsNDI2LjU5MWMwLC0yLjIxMyAtMS43OTYsLTQuMDEgLTQuMDEs
LTQuMDFsLTUzLjQ4NywtMGMtMi4yMTMsLTAgLTQuMDEsMS43OTcgLTQuMDEsNC4wMWwwLDU4Ljg5
NGMwLDIuMjEzIDEuNzk3LDQuMDEgNC4wMSw0LjAxbDUzLjQ4NywwYzIuMjE0LDAgNC4wMSwtMS43
OTcgNC4wMSwtNC4wMWwwLC01OC44OTRaIiBzdHlsZT0iZmlsbDojZWVhYzAxO3N0cm9rZTojMDAw
O3N0cm9rZS13aWR0aDozLjQzcHg7Ii8+PGNpcmNsZSBjeD0iNTE3LjAxNCIgY3k9IjEwOC43NzEi
IHI9IjQuMzUyIiBzdHlsZT0iZmlsbDojZDNkMmIzOyIvPjxjaXJjbGUgY3g9IjUzMS4zMiIgY3k9
IjEwOC43NzEiIHI9IjQuMzUyIiBzdHlsZT0iZmlsbDojZjRiMzAzOyIvPjxjaXJjbGUgY3g9IjU0
NC43OTIiIGN5PSIxMDguNzcxIiByPSI0LjM1MiIgc3R5bGU9ImZpbGw6I2Y5NjEzZDsiLz48cGF0
aCBkPSJNMTA1LjI1Nyw0ODQuNDNjLTAsMCAyMC4zNzQsLTI1LjIyNSA0MS43MTksLTcuNzYxYzIx
LjM0NCwxNy40NjMgLTEyLjYxMyw1MC40NSAtNDAuNzQ5LDQyLjY4OSIgc3R5bGU9ImZpbGw6bm9u
ZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6OS45cHg7Ii8+PHBhdGggZD0iTTExMy4wMTksNDYw
LjE0MWMtMCwtNC4yNjUgLTMuNDYzLC03LjcyNyAtNy43MjgsLTcuNzI3bC03OC42NTUsLTBjLTQu
MjY1LC0wIC03LjcyNywzLjQ2MiAtNy43MjcsNy43MjdsLTAsNzguNjU1Yy0wLDQuMjY1IDMuNDYy
LDcuNzI4IDcuNzI3LDcuNzI4bDc4LjY1NSwtMGM0LjI2NSwtMCA3LjcyOCwtMy40NjMgNy43Mjgs
LTcuNzI4bC0wLC03OC42NTVaIiBzdHlsZT0iZmlsbDojNjFlMGVlO3N0cm9rZTojMDAwO3N0cm9r
ZS13aWR0aDo0LjU1cHg7c3Ryb2tlLWxpbmVjYXA6c3F1YXJlOyIvPjxwYXRoIGQ9Ik03Ny4yNzcs
Mzk1LjQwMmMtMi4yMzQsOC43NTUgLTYuNDk2LDE1LjM2NiAtMTEuNjQ5LDE3LjcxNGMxLjk5NSwz
LjA1NyAzLjE2Nyw2Ljc4MSAzLjE2NywxMC43OTdjMCwxMC4zODcgLTcuODQsMTguODIgLTE3LjQ5
OCwxOC44MmMtOS42NTcsMCAtMTcuNDk4LC04LjQzMyAtMTcuNDk4LC0xOC44MmMwLC05LjczMyA2
Ljg4NCwtMTcuNzUxIDE1LjY5NywtMTguNzIyYy0zLjM5OSwtNS45NiAtNS41MjQsLTE0LjQyMyAt
NS41MjQsLTIzLjgwNWMwLC0xOC4wMjQgNy44NDEsLTMyLjY1NyAxNy40OTksLTMyLjY1N2M2Ljc5
MiwtMCAxMi42ODUsNy4yMzggMTUuNTg0LDE3LjgwMmMxLjYyOSwtMC41OTIgMy4zODcsLTAuOTE1
IDUuMjIsLTAuOTE1YzguNDUsMCAxNS4zMTEsNi44NjEgMTUuMzExLDE1LjMxMWMtMCw4LjQ1IC02
Ljg2MSwxNS4zMTEgLTE1LjMxMSwxNS4zMTFjLTEuNzUsMCAtMy40MzIsLTAuMjk0IC00Ljk5OCwt
MC44MzZaIiBzdHlsZT0iZmlsbDojZmZmO2ZpbGwtb3BhY2l0eTowLjU7Ii8+PHBhdGggZD0iTTMz
LjU4MywyMzMuOTNjMi41MjksLTAuMzM3IDQuNTEsLTEuMTc4IDYuNzE2LC0yLjQwM2MwLjk1Nywt
MC41MzIgMS45NjksLTAuOTA0IDIuODk4LC0xLjQ4NWMwLjc4OSwtMC40OTMgMS4zNDMsLTAuOTc2
IDEuMzQzLC0xLjk3OWMwLC0wLjIzNiAwLjIyNCwtMC42MzIgMCwtMC43MDdjLTAuOTUyLC0wLjMx
NyAtMi4zNjQsMC40NjUgLTMuMDQsMC45OWMtMC42MjUsMC40ODYgLTEuNDk0LDAuNTc5IC0yLjEy
LDEuMDZjLTEuMDM2LDAuNzk2IC0xLjY4MiwyLjQ2MyAtMS43NjcsMy43NDZjLTAuMDYsMC44OTYg
LTAuMzU3LDIuODQgMC41NjUsMy4zOTNjMC4zNTcsMC4yMTQgMC45MjksMC4xNDIgMS4zNDMsMC4x
NDJjMS4wOCwtMCAyLjEwNCwwLjAxMiAzLjE4MSwtMC4xNDJjMi4wODIsLTAuMjk3IDQuMTY1LC0x
LjQ1NSA2LjAwOSwtMi40MDNjMi4wMTQsLTEuMDM2IDQuMDEyLC0xLjkyMSA1LjU4NCwtMy42MDVj
MC42NDIsLTAuNjg4IDEuMDEsLTEuMjg2IDEuMjcyLC0yLjE5MWMwLjA1OSwtMC4yMDMgLTAuMDYs
LTAuNzE4IDAuMDcxLC0wLjg0OWMwLjEwOCwtMC4xMDggMC4wOTksMC4yMjYgMC4wNzEsMC4yODNj
LTAuMTg1LDAuMzY3IC0wLjQ2MiwwLjc0IC0wLjYzNywxLjEzMWMtMC4zMjQsMC43MjkgLTAuMzUz
LDEuNzU5IC0wLjM1MywyLjU0NWMwLDAuMTMyIC0wLjA3LDAuNTcxIDAsMC42MzZjMC4wNTUsMC4w
NTEgMC4zNjMsMC4xOTMgMC40MjQsMC4yMTJjMi4wMDIsMC42MzMgNC4zNDIsLTAuNTMyIDYuMDA5
LC0xLjQ4NGMwLjY0MSwtMC4zNjcgMS4zNjMsLTAuODUgMi4wNiwtMS4xMTJjMC4wMjYsLTAuMDEg
MC42NzgsLTAuMTk3IDAuNjk2LC0wLjE2MWMwLjI4MiwwLjU2MyAwLjExMSwxLjUxMSAwLjIxMiwy
LjEyMWMwLjI4NSwxLjcwNyAyLjM2NywxLjYxNSAzLjY3NiwxLjQ4NGMwLjIwNywtMC4wMiAwLjM2
MywtMC4yMTIgMC41NjYsLTAuMjEyIiBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwO3N0cm9r
ZS13aWR0aDoyLjc1cHg7Ii8+PHBhdGggZD0iTTM2LjQ4MiwyNDIuMzQyYzEuMzQ2LC0wIDIuNjgy
LC0wLjE1NCA0LjAyOSwtMC4yMTJjNC4zMzIsLTAuMTg5IDguNjcxLC0wLjE5NiAxMy4wMDYsLTAu
MjgzYzYuNTI1LC0wLjEzMSAxMy4wNTYsMC4wNzEgMTkuNTgxLDAuMDcxIiBzdHlsZT0iZmlsbDpu
b25lO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDoyLjc1cHg7Ii8+PHBhdGggZD0iTTUxNi4wOSw0
NDIuMDMxYy0wLDAuOTM2IC0wLjUzMiwxLjg5MiAtMC4xMjYsMi44MzljMC41MjYsMS4yMjggMi4z
NjMsMS4yNzUgMy40NjksMC45NDZjMC42MjgsLTAuMTg3IDEuMzE2LC0xLjE3NiAxLjcwMywtMS42
NGMxLjAzOCwtMS4yNDcgMi40MzUsLTIuMzc1IDIuNzEyLC00LjAzN2MwLjA0MSwtMC4yNDggMC4x
OTQsLTAuOTM2IDAuMDYzLC0xLjE5OGMtMC43NjMsLTEuNTI1IC0zLjA1OSwtMS40NzggLTQuMTY4
LC0wLjM2OWMtMC4xNjQsMC4xNjQgLTAuNDQ2LDAuMjI3IC0wLjU2MywwLjQzMmMtMC45OTQsMS43
NDUgLTAuNDM5LDQuMzQxIDAuNDQyLDUuOTkyYzAuMzA0LDAuNTY5IDAuODI5LDEuMDQ4IDEuMzI0
LDEuNDUxYzAuMzM5LDAuMjc1IDAuNzc1LDAuNzE2IDEuMTk5LDAuOTI4YzMuMjYyLDEuNjMxIDcu
Njg5LC0wLjEzMyAxMC41OTYsLTEuNzQ4YzEuOTUyLC0xLjA4NSA0LjEsLTIuMTUyIDQuMSwtNC42
NjhjLTAsLTAuMzI1IDAuMDM3LC0wLjY5NCAtMC4wNjMsLTEuMDA5Yy0wLjM2MSwtMS4xMzMgLTIu
MzA3LC0xLjA0MyAtMy4xNTQsLTAuNjk0Yy0xLjE3OSwwLjQ4NiAtMi4zNzMsMS4zOTUgLTMuMDks
Mi40NmMtMS4xNzQsMS43NDIgLTEuMjA3LDQuMjgzIDAuNTY3LDUuNjE0YzEuMjU2LDAuOTQxIDIu
OTU4LDAuOTIzIDQuNDQ3LDAuNzY2YzIuMjQ3LC0wLjIzNyA0LjI1MywtMS4zMTMgNi4wMjMsLTIu
NjU5YzAuODc1LC0wLjY2NCAyLjA5NCwtMS4zMTYgMi42NDksLTIuMzMzYzAuMTczLC0wLjMxNiAw
LjIxNSwtMC43NDUgMC4zNzksLTEuMDczYzAuMDcxLC0wLjE0MyAtMC4xNTgsMC4yODUgLTAuMTg5
LDAuNDQyYy0wLjA1NiwwLjI3NiAtMC4wOTQsMC44NzggLTAsMS4xMzVjMC4zNzYsMS4wMjggMS4y
MzMsMS41MDkgMi4yNywxLjcwM2MxLjM0NiwwLjI1MiAzLjAwOSwtMC4wMSA0LjM1MiwtMC4xODlj
MC45MzksLTAuMTI1IDEuODkyLC0wLjUxNyAxLjg5MiwwLjc1NyIgc3R5bGU9ImZpbGw6bm9uZTtz
dHJva2U6IzAwMDtzdHJva2Utd2lkdGg6Mi40NXB4OyIvPjxwYXRoIGQ9Ik01MTQuMjU5LDQ2MC4z
NTljMC4yNjYsMC4xMzQgMC4wOCwxLjM1NCAwLjU2NywxLjM4OGMwLjQ0MiwwLjAzMSAwLjg4NSww
LjA0IDEuMzI4LDAuMDVjMC41MTcsMC4wMTIgMS4xMDQsMC4wOTggMS42MzMsLTAuMDM0YzEuNjg4
LC0wLjQyMiAzLjc5NiwtMS41OTggNC40ODIsLTMuMjk2YzAuMTg4LC0wLjQ2NyAwLjI0NiwtMS4w
OTQgMC4zMTUsLTEuNTc3YzAuMDc1LC0wLjUyNCAwLjM3OCwtMS43MTggLTAuMDYzLC0yLjE0NGMt
MC4wODgsLTAuMDg1IC0wLjYwNCwtMC4wNjMgLTAuNjk0LC0wLjA2M2MtMC42OTUsLTAgLTEuNzk2
LDAuMzkyIC0yLjIwNywxLjAwOWMtMC4yNzMsMC40MDkgLTAuMTg5LDAuODUxIC0wLjE4OSwxLjMy
NGMtMCwwLjYyIC0wLjAzMywxLjIyNCAwLjEyNiwxLjgzYzAuMzYyLDEuMzc5IDIuMTExLDEuOTQx
IDMuMzQzLDIuMDE4YzIuNjk2LDAuMTY4IDUuNDQ3LC0wLjI0NSA4LjEzNiwtMGMyLjIwNywwLjIg
NC40NTQsMC43MTcgNi42ODYsMC41MDRjMS41OCwtMC4xNSAzLjE1NCwtMC41MjUgNC43MywtMC42
M2MzLjM5NCwtMC4yMjYgNi43OTEsLTAuNTA1IDEwLjIxOCwtMC41MDUiIHN0eWxlPSJmaWxsOm5v
bmU7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjIuNDVweDsiLz48cGF0aCBkPSJNNTE5LjA1NCw0
NzEuNjhsMCwwLjk0NmMwLDAuMTU1IC0wLDAuMjUxIDAuMDYzLDAuMzc5bDAsMC4xMjZjMC42MDIs
LTIuNDA3IDMuNzc1LC0yLjc4NCA1LjgxLC0yLjc4NGMwLjE1LC0wIDAuMjMyLDAuNDggMC4yNDUs
MC41NzZjMC4wNTUsMC4zODUgLTAuMTkyLDEuNDUgMC4zNzksMS42NGMwLjk4OSwwLjMzIDIuNDA2
LC0wLjMxOCAzLjM0MywtMC41NjhjMC4yMzUsLTAuMDYyIDEuMDA5LC0wLjQ0MSAxLjE5OCwtMC4y
NTJjMC45NTQsMC45NTQgMi40MTIsMC41NjIgMy43MjEsMC42MzFjMy4zMzIsMC4xNzUgNi42Mjks
LTAuMTg5IDkuOTY2LC0wLjE4OWMxLjA1OCwtMCAyLjE2MiwwLjA4NyAzLjIxNywtMGMwLjA2OCwt
MC4wMDYgMS4wMDksLTAuMDI3IDEuMDA5LC0wLjI1MyIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6
IzAwMDtzdHJva2Utd2lkdGg6Mi40NXB4OyIvPjwvZz48Zz48cGF0aCBkPSJNMzc0LjU3OCwxMTQu
NDU1Yy0wLjYxMywtNC4yMTYgLTQuNTMzLC03LjE0MiAtOC43NDksLTYuNTNsLTk5Ljk0NSwxNC41
MTVjLTQuMjE2LDAuNjEzIC03LjE0Miw0LjUzMyAtNi41Myw4Ljc0OWwyLjIxOSwxNS4yNzhjMC42
MTIsNC4yMTYgNC41MzIsNy4xNDIgOC43NDksNi41M2w5OS45NDUsLTE0LjUxNWM0LjIxNiwtMC42
MTIgNy4xNDIsLTQuNTMzIDYuNTI5LC04Ljc0OWwtMi4yMTgsLTE1LjI3OFoiIHN0eWxlPSJmaWxs
OiM5ZGFjY2Q7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjUuMjFweDsiLz48cGF0aCBkPSJNMzEz
Ljg2OSwxMTcuNDE0YzAsMCA2Ni4xMDIsOS44NzggNzcuNTgyLC0yMy43MzNjMTEuNDgsLTMzLjYx
MSAtMTIuOTg3LC01MS44NzMgLTQxLjYzNywtMzkuOTIxYy0yOC42NSwxMS45NTIgLTU0Ljg1LDQ2
LjE0MiAtNTQuODUsNDYuMTQyYzAsMCAtMzAuMDA1LC04LjEwOCAtNDEuMzE0LDUuMjIxYy0xMS4z
MDksMTMuMzMgLTE3LjE2NywyNi42NDYgLTIuNDM1LDMxLjUxOWMxNC43MzIsNC44NzMgNjIuNjU0
LC0xOS4yMjggNjIuNjU0LC0xOS4yMjhaIiBzdHlsZT0iZmlsbDojNzM1ZDkzO3N0cm9rZTojMDAw
O3N0cm9rZS13aWR0aDo1LjkzcHg7Ii8+PHBhdGggZD0iTTM3NC4xNDgsMTM2Ljg3NmMtMCwwIDM4
LjQ5MywtOS4zMzggMzYuNTYzLC0zLjU0OWMtMS45Myw1Ljc5IC0zNy42MzgsMjYuOTAxIC0xMTQu
ODMxLDE1Ljk2NmM3MC4zNDUsLTEwLjczMSA3OC4yNjgsLTEyLjQxNyA3OC4yNjgsLTEyLjQxN1oi
IHN0eWxlPSJmaWxsOiM1MDNhNzE7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQuOTVweDsiLz48
cGF0aCBkPSJNMzU3LjM0NiwxMjQuOTA2bC01NC4wMjEsOS42MTJsLTAsNS4zOTlsNTQuMDIxLC05
LjYxMmwwLC01LjM5OVoiIHN0eWxlPSJmaWxsOiNmZmY7Ii8+PGNpcmNsZSBjeD0iMzAwLjE1IiBj
eT0iMTM3LjE5OCIgcj0iNC42NzciIHN0eWxlPSJmaWxsOiNmZmIyNWM7c3Ryb2tlOiMwMDA7c3Ry
b2tlLXdpZHRoOjQuOTVweDsiLz48Y2lyY2xlIGN4PSIzNjEuNDEiIGN5PSIxMjcuNTgxIiByPSI0
LjI4MiIgc3R5bGU9ImZpbGw6I2ZmYjI1YztzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6NC45NXB4
OyIvPjwvZz48L3N2Zz4=

3
testdata/logo.svg.base64.license vendored Normal file
View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2022 Maria Letta, the go-mail Team
SPDX-License-Identifier: CC-BY-ND-4.0

3
testdata/logo.svg.license vendored Normal file
View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2022 Maria Letta, the go-mail Team
SPDX-License-Identifier: CC-BY-ND-4.0

0
testdata/tmp/.gitkeep vendored Normal file
View file