From 20887960496479719e48990cbf4eae121259e38a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 11:15:32 +0200 Subject: [PATCH 001/188] 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. --- .gitignore | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 3c0fb1e..9aec0cb 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,4 @@ atlassian-ide-plugin.xml com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties -fabric.properties - -testdata \ No newline at end of file +fabric.properties \ No newline at end of file From 946d9888d6bdbc2661f90fc92feb311a4539eba5 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 11:16:09 +0200 Subject: [PATCH 002/188] 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. --- b64linebreaker.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/b64linebreaker.go b/b64linebreaker.go index cc83973..abc356a 100644 --- a/b64linebreaker.go +++ b/b64linebreaker.go @@ -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 { From bdfe13dc93f5216cfcb6c253a05cac87f8d74b3b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 11:16:16 +0200 Subject: [PATCH 003/188] 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. --- b64linebreaker_test.go | 661 ++++++++++++----------------------------- 1 file changed, 183 insertions(+), 478 deletions(-) diff --git a/b64linebreaker_test.go b/b64linebreaker_test.go index 9340d8d..06599d2 100644 --- a/b64linebreaker_test.go +++ b/b64linebreaker_test.go @@ -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.Buffer{} + 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.Buffer{} + 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,46 @@ 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) + var buffer bytes.Buffer + 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 !bytes.Equal(data, decode) { + t.Error("generated line breaker output differs from original data") } }) } From 421451f179e69261f5d3a32b9feecae891725906 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 11:20:24 +0200 Subject: [PATCH 004/188] 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. --- testdata/logo.svg | 1 + testdata/logo.svg.base64 | 367 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 368 insertions(+) create mode 100644 testdata/logo.svg create mode 100644 testdata/logo.svg.base64 diff --git a/testdata/logo.svg b/testdata/logo.svg new file mode 100644 index 0000000..9a8dcaa --- /dev/null +++ b/testdata/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/testdata/logo.svg.base64 b/testdata/logo.svg.base64 new file mode 100644 index 0000000..59e4a6a --- /dev/null +++ b/testdata/logo.svg.base64 @@ -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= From 854361090aead4a21b124136431db3b40c922ae1 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 11:21:03 +0200 Subject: [PATCH 005/188] 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. --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9aec0cb..5ce8347 100644 --- a/.gitignore +++ b/.gitignore @@ -54,4 +54,8 @@ atlassian-ide-plugin.xml com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties -fabric.properties \ No newline at end of file +fabric.properties + +## Coverage data +coverage.coverprofile +coverage.html \ No newline at end of file From b31c7cf3a7793932fef37abc5f8ff79f60e4d323 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 11:26:42 +0200 Subject: [PATCH 006/188] 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. --- testdata/logo.svg.base64.license | 3 +++ testdata/logo.svg.license | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 testdata/logo.svg.base64.license create mode 100644 testdata/logo.svg.license diff --git a/testdata/logo.svg.base64.license b/testdata/logo.svg.base64.license new file mode 100644 index 0000000..da2e7e7 --- /dev/null +++ b/testdata/logo.svg.base64.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2022 Maria Letta, the go-mail Team + +SPDX-License-Identifier: CC-BY-ND-4.0 diff --git a/testdata/logo.svg.license b/testdata/logo.svg.license new file mode 100644 index 0000000..da2e7e7 --- /dev/null +++ b/testdata/logo.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2022 Maria Letta, the go-mail Team + +SPDX-License-Identifier: CC-BY-ND-4.0 From 0bac51746d4717caae26f281ef2e738185419e5e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 11:29:04 +0200 Subject: [PATCH 007/188] 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. --- b64linebreaker_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/b64linebreaker_test.go b/b64linebreaker_test.go index 06599d2..370fa41 100644 --- a/b64linebreaker_test.go +++ b/b64linebreaker_test.go @@ -255,6 +255,9 @@ func FuzzBase64LineBreaker(f *testing.F) { } 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") } From ab835b78704f35e93d54a10305b5ed42d60fdb5e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 11:36:06 +0200 Subject: [PATCH 008/188] 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. --- b64linebreaker_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b64linebreaker_test.go b/b64linebreaker_test.go index 370fa41..9f88c7e 100644 --- a/b64linebreaker_test.go +++ b/b64linebreaker_test.go @@ -219,7 +219,7 @@ func (w *mockWriterNewline) Write(p []byte) (n int, err error) { func FuzzBase64LineBreaker(f *testing.F) { seedData := [][]byte{ - //[]byte(""), + []byte(""), []byte("abc"), []byte("def"), []byte("Hello, World!"), From 1c8b2904f50bd0296630cf0c1440e35aa31706b6 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 13:10:13 +0200 Subject: [PATCH 009/188] 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. --- client.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client.go b/client.go index 3f36a94..af36b47 100644 --- a/client.go +++ b/client.go @@ -242,6 +242,9 @@ 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") ) // NewClient creates a new Client instance with the provided host and optional configuration Option functions. @@ -510,6 +513,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 From 3251d74c36cb2e79a272cc4ba7e8fcf5f4cdb6e4 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 13:23:01 +0200 Subject: [PATCH 010/188] 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. --- client_test.go | 538 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 464 insertions(+), 74 deletions(-) diff --git a/client_test.go b/client_test.go index 80a5669..e9b927e 100644 --- a/client_test.go +++ b/client_test.go @@ -5,17 +5,11 @@ package mail import ( - "bufio" - "context" "crypto/tls" "errors" "fmt" - "io" - "net" "os" - "strconv" - "strings" - "sync" + "reflect" "testing" "time" @@ -36,54 +30,470 @@ const ( TestServerPortBase = 2025 ) -// TestNewClient tests the NewClient() method with its default options func TestNewClient(t *testing.T) { - host := "mail.example.com" - tests := []struct { - name string - host string - shouldfail bool - }{ - {"Default", "mail.example.com", false}, - {"Empty host should fail", "", true}, - } + t.Run("create new Client", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if client.smtpAuthType != SMTPAuthNoAuth { + t.Errorf("new Client failed. Expected smtpAuthType: %s, got: %s", SMTPAuthNoAuth, + client.smtpAuthType) + } + if client.connTimeout != DefaultTimeout { + t.Errorf("new Client failed. Expected connTimeout: %s, got: %s", DefaultTimeout.String(), + client.connTimeout.String()) + } + if client.host != DefaultHost { + t.Errorf("new Client failed. Expected host: %s, got: %s", DefaultHost, client.host) + } + if client.port != DefaultPort { + t.Errorf("new Client failed. Expected port: %d, got: %d", DefaultPort, client.port) + } + if client.tlsconfig == nil { + t.Fatal("new Client failed. Expected tlsconfig but got nil") + } + if client.tlsconfig.MinVersion != DefaultTLSMinVersion { + t.Errorf("new Client failed. Expected tlsconfig min TLS version: %d, got: %d", + DefaultTLSMinVersion, client.tlsconfig.MinVersion) + } + if client.tlsconfig.ServerName != DefaultHost { + t.Errorf("new Client failed. Expected tlsconfig server name: %s, got: %s", + DefaultHost, client.tlsconfig.ServerName) + } + if client.tlspolicy != DefaultTLSPolicy { + t.Errorf("new Client failed. Expected tlsconfig policy: %s, got: %s", DefaultTLSPolicy, + client.tlspolicy) + } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c, err := NewClient(tt.host) - if err != nil && !tt.shouldfail { - t.Errorf("failed to create new client: %s", err) - return - } - if c.host != tt.host { - t.Errorf("failed to create new client. Host expected: %s, got: %s", host, c.host) - } - if c.connTimeout != DefaultTimeout { - t.Errorf("failed to create new client. Timeout expected: %s, got: %s", DefaultTimeout.String(), - c.connTimeout.String()) - } - if c.port != DefaultPort { - t.Errorf("failed to create new client. Port expected: %d, got: %d", DefaultPort, c.port) - } - if c.tlspolicy != TLSMandatory { - t.Errorf("failed to create new client. TLS policy expected: %d, got: %d", TLSMandatory, c.tlspolicy) - } - if c.tlsconfig.ServerName != tt.host { - t.Errorf("failed to create new client. TLS config host expected: %s, got: %s", - host, c.tlsconfig.ServerName) - } - if c.tlsconfig.MinVersion != DefaultTLSMinVersion { - t.Errorf("failed to create new client. TLS config min versino expected: %d, got: %d", - DefaultTLSMinVersion, c.tlsconfig.MinVersion) - } - if c.ServerAddr() != fmt.Sprintf("%s:%d", tt.host, c.port) { - t.Errorf("failed to create new client. c.ServerAddr() expected: %s, got: %s", - fmt.Sprintf("%s:%d", tt.host, c.port), c.ServerAddr()) - } - }) - } + hostname, err := os.Hostname() + if err != nil { + t.Fatalf("failed to get hostname: %s", err) + } + if client.helo != hostname { + t.Errorf("new Client failed. Expected helo: %s, got: %s", hostname, client.helo) + } + }) + t.Run("NewClient with empty hostname should fail", func(t *testing.T) { + _, err := NewClient("") + if err == nil { + t.Fatalf("NewClient with empty hostname should fail") + } + if !errors.Is(err, ErrNoHostname) { + t.Errorf("NewClient with empty hostname should fail with error: %s, got: %s", ErrNoHostname, err) + } + }) + t.Run("NewClient with option", func(t *testing.T) { + hostname := "mail.example.com" + tests := []struct { + name string + option Option + expectFunc func(c *Client) error + shouldfail bool + expectErr *error + }{ + {"nil option", nil, nil, true, nil}, + { + "WithPort", WithPort(465), + func(c *Client) error { + if c.port != 465 { + return fmt.Errorf("failed to set custom port. Want: %d, got: %d", 465, c.port) + } + return nil + }, + false, nil, + }, + { + "WithPort but too high port number", WithPort(100000), nil, true, + &ErrInvalidPort, + }, + { + "WithTimeout", WithTimeout(time.Second * 100), + func(c *Client) error { + if c.connTimeout != time.Second*100 { + return fmt.Errorf("failed to set custom timeout. Want: %d, got: %d", time.Second*100, + c.connTimeout) + } + return nil + }, + false, nil, + }, + { + "WithTimeout but invalid timeout", WithTimeout(-10), nil, true, + &ErrInvalidTimeout, + }, + { + "WithSSL", WithSSL(), + func(c *Client) error { + if !c.useSSL { + return fmt.Errorf("failed to set useSSL. Want: %t, got: %t", true, c.useSSL) + } + return nil + }, + false, nil, + }, + { + "WithSSLPort with no fallback", WithSSLPort(false), + func(c *Client) error { + if !c.useSSL { + return fmt.Errorf("failed to set useSSL. Want: %t, got: %t", true, c.useSSL) + } + if c.port != 465 { + return fmt.Errorf("failed to set ssl port. Want: %d, got: %d", 465, c.port) + } + if c.fallbackPort != 0 { + return fmt.Errorf("failed to set ssl fallbackport. Want: %d, got: %d", 0, + c.fallbackPort) + } + return nil + }, + false, nil, + }, + { + "WithSSLPort with fallback", WithSSLPort(true), + func(c *Client) error { + if !c.useSSL { + return fmt.Errorf("failed to set useSSL. Want: %t, got: %t", true, c.useSSL) + } + if c.port != 465 { + return fmt.Errorf("failed to set ssl port. Want: %d, got: %d", 465, c.port) + } + if c.fallbackPort != 25 { + return fmt.Errorf("failed to set ssl fallbackport. Want: %d, got: %d", 0, + c.fallbackPort) + } + return nil + }, + false, nil, + }, + { + "WithDebugLog", WithDebugLog(), + func(c *Client) error { + if !c.useDebugLog { + return fmt.Errorf("failed to set enable debug log. Want: %t, got: %t", true, + c.useDebugLog) + } + if c.logAuthData { + return fmt.Errorf("failed to set enable debug log. Want logAuthData: %t, got: %t", true, + c.logAuthData) + } + return nil + }, + false, nil, + }, + { + "WithLogger log.Stdlog", WithLogger(log.New(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.Stdlog" { + return fmt.Errorf("failed to set logger. Want logger type: %s, got: %s", + "*log.Stdlog", loggerType) + } + return nil + }, + false, nil, + }, + { + "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, + }, + { + "WithHELO", WithHELO(hostname), + func(c *Client) error { + if c.helo != hostname { + return fmt.Errorf("failed to set custom HELO. Want: %s, got: %s", hostname, c.helo) + } + return nil + }, + false, nil, + }, + { + "WithHELO fail with empty hostname", WithHELO(""), nil, + true, &ErrInvalidHELO, + }, + { + "WithTLSPolicy TLSMandatory", WithTLSPolicy(TLSMandatory), + func(c *Client) error { + if c.tlspolicy != TLSMandatory { + return fmt.Errorf("failed to set custom TLS policy. Want: %s, got: %s", TLSMandatory, + c.tlspolicy) + } + return nil + }, + false, nil, + }, + { + "WithTLSPolicy TLSOpportunistic", WithTLSPolicy(TLSOpportunistic), + func(c *Client) error { + if c.tlspolicy != TLSOpportunistic { + return fmt.Errorf("failed to set custom TLS policy. Want: %s, got: %s", TLSOpportunistic, + c.tlspolicy) + } + return nil + }, + false, nil, + }, + { + "WithTLSPolicy NoTLS", WithTLSPolicy(NoTLS), + func(c *Client) error { + if c.tlspolicy != NoTLS { + return fmt.Errorf("failed to set custom TLS policy. Want: %s, got: %s", TLSOpportunistic, + c.tlspolicy) + } + return nil + }, + false, nil, + }, + { + "WithTLSPortPolicy TLSMandatory", WithTLSPortPolicy(TLSMandatory), + func(c *Client) error { + if c.tlspolicy != TLSMandatory { + return fmt.Errorf("failed to set custom TLS policy. Want: %s, got: %s", TLSMandatory, + c.tlspolicy) + } + if c.port != 587 { + return fmt.Errorf("failed to set custom TLS policy. Want port: %d, got: %d", 587, + c.port) + } + if c.fallbackPort != 0 { + return fmt.Errorf("failed to set custom TLS policy. Want fallback port: %d, got: %d", + 0, c.fallbackPort) + } + return nil + }, + false, nil, + }, + { + "WithTLSPortPolicy TLSOpportunistic", WithTLSPortPolicy(TLSOpportunistic), + func(c *Client) error { + if c.tlspolicy != TLSOpportunistic { + return fmt.Errorf("failed to set custom TLS policy. Want: %s, got: %s", TLSOpportunistic, + c.tlspolicy) + } + if c.port != 587 { + return fmt.Errorf("failed to set custom TLS policy. Want port: %d, got: %d", 587, + c.port) + } + if c.fallbackPort != 25 { + return fmt.Errorf("failed to set custom TLS policy. Want fallback port: %d, got: %d", + 25, c.fallbackPort) + } + return nil + }, + false, nil, + }, + { + "WithTLSPortPolicy NoTLS", WithTLSPortPolicy(NoTLS), + func(c *Client) error { + if c.tlspolicy != NoTLS { + return fmt.Errorf("failed to set custom TLS policy. Want: %s, got: %s", TLSOpportunistic, + c.tlspolicy) + } + if c.port != 25 { + return fmt.Errorf("failed to set custom TLS policy. Want port: %d, got: %d", 587, + c.port) + } + if c.fallbackPort != 0 { + return fmt.Errorf("failed to set custom TLS policy. Want fallback port: %d, got: %d", + 25, c.fallbackPort) + } + return nil + }, + false, nil, + }, + { + "WithTLSConfig with empty tls.Config", WithTLSConfig(&tls.Config{}), + func(c *Client) error { + if c.tlsconfig == nil { + return errors.New("failed to set custom TLS config. Wanted policy but got nil") + } + return nil + }, + false, nil, + }, + { + "WithTLSConfig with custom tls.Config", WithTLSConfig(&tls.Config{ServerName: hostname}), + func(c *Client) error { + if c.tlsconfig == nil { + return errors.New("failed to set custom TLS config. Wanted policy but got nil") + } + if c.tlsconfig.ServerName != hostname { + return fmt.Errorf("failed to set custom TLS config. Want hostname: %s, got: %s", + hostname, c.tlsconfig.ServerName) + } + return nil + }, + false, nil, + }, + { + "WithTLSConfig with nil", WithTLSConfig(nil), nil, + true, &ErrInvalidTLSConfig, + }, + { + "WithSMTPAuthCustom with PLAIN auth", + WithSMTPAuthCustom(smtp.PlainAuth("", "", "", "", false)), + func(c *Client) error { + if c.smtpAuthType != SMTPAuthCustom { + return fmt.Errorf("failed to set custom SMTP auth method. Want smtp auth type: %s, "+ + "got: %s", SMTPAuthCustom, c.smtpAuthType) + } + if c.smtpAuth == nil { + return errors.New("failed to set custom SMTP auth method. Wanted smtp auth method but" + + " got nil") + } + smtpAuthType := reflect.TypeOf(c.smtpAuth).String() + if smtpAuthType != "*smtp.plainAuth" { + return fmt.Errorf("failed to set custom SMTP auth method. Want smtp auth method of type: %s, "+ + "got: %s", "*smtp.plainAuth", smtpAuthType) + } + return nil + }, + false, nil, + }, + { + "WithSMTPAuthCustom with LOGIN auth", + WithSMTPAuthCustom(smtp.LoginAuth("", "", "", false)), + func(c *Client) error { + if c.smtpAuthType != SMTPAuthCustom { + return fmt.Errorf("failed to set custom SMTP auth method. Want smtp auth type: %s, "+ + "got: %s", SMTPAuthCustom, c.smtpAuthType) + } + if c.smtpAuth == nil { + return errors.New("failed to set custom SMTP auth method. Wanted smtp auth method but" + + " got nil") + } + smtpAuthType := reflect.TypeOf(c.smtpAuth).String() + if smtpAuthType != "*smtp.loginAuth" { + return fmt.Errorf("failed to set custom SMTP auth method. Want smtp auth method of type: %s, "+ + "got: %s", "*smtp.loginAuth", smtpAuthType) + } + return nil + }, + false, nil, + }, + { + "WithSMTPAuthCustom with nil", WithSMTPAuthCustom(nil), nil, + true, &ErrSMTPAuthMethodIsNil, + }, + { + "WithUsername", WithUsername("toni.tester"), + func(c *Client) error { + if c.user != "toni.tester" { + return fmt.Errorf("failed to set username. Want username: %s, got: %s", + "toni.tester", c.user) + } + return nil + }, + false, nil, + }, + { + "WithPassword", WithPassword("sU*p3rS3cr3t"), + func(c *Client) error { + if c.pass != "sU*p3rS3cr3t" { + return fmt.Errorf("failed to set password. Want password: %s, got: %s", + "sU*p3rS3cr3t", c.pass) + } + return nil + }, + false, nil, + }, + { + "WithDSN", WithDSN(), + func(c *Client) error { + if c.requestDSN != true { + return fmt.Errorf("failed to enable DSN. Want requestDSN: %t, got: %t", true, + c.requestDSN) + } + if c.dsnReturnType != DSNMailReturnFull { + return fmt.Errorf("failed to enable DSN. Want dsnReturnType: %s, got: %s", + DSNMailReturnFull, c.dsnReturnType) + } + if len(c.dsnRcptNotifyType) != 2 { + return fmt.Errorf("failed to enable DSN. Want 2 default DSN Rcpt Notify types, got: %d", + len(c.dsnRcptNotifyType)) + } + if c.dsnRcptNotifyType[0] != string(DSNRcptNotifyFailure) { + return fmt.Errorf("failed to enable DSN. Want DSN Rcpt Notify Failure type: %s, got: %s", + string(DSNRcptNotifyFailure), c.dsnRcptNotifyType[0]) + } + if c.dsnRcptNotifyType[1] != string(DSNRcptNotifySuccess) { + return fmt.Errorf("failed to enable DSN. Want DSN Rcpt Notify Success type: %s, got: %s", + string(DSNRcptNotifySuccess), c.dsnRcptNotifyType[1]) + } + 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 && 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) + } + } + }) + } + }) + t.Run("NewClient WithSMTPAuth", func(t *testing.T) { + tests := []struct { + name string + option Option + expected SMTPAuthType + }{ + {"CRAM-MD5", WithSMTPAuth(SMTPAuthCramMD5), SMTPAuthCramMD5}, + {"LOGIN", WithSMTPAuth(SMTPAuthLogin), SMTPAuthLogin}, + {"LOGIN-NOENC", WithSMTPAuth(SMTPAuthLoginNoEnc), SMTPAuthLoginNoEnc}, + {"NOAUTH", WithSMTPAuth(SMTPAuthNoAuth), SMTPAuthNoAuth}, + {"PLAIN", WithSMTPAuth(SMTPAuthPlain), SMTPAuthPlain}, + {"PLAIN-NOENC", WithSMTPAuth(SMTPAuthPlainNoEnc), SMTPAuthPlainNoEnc}, + {"SCRAM-SHA-1", WithSMTPAuth(SMTPAuthSCRAMSHA1), SMTPAuthSCRAMSHA1}, + {"SCRAM-SHA-1-PLUS", WithSMTPAuth(SMTPAuthSCRAMSHA1PLUS), SMTPAuthSCRAMSHA1PLUS}, + {"SCRAM-SHA-256", WithSMTPAuth(SMTPAuthSCRAMSHA256), SMTPAuthSCRAMSHA256}, + {"SCRAM-SHA-256-PLUS", WithSMTPAuth(SMTPAuthSCRAMSHA256PLUS), SMTPAuthSCRAMSHA256PLUS}, + {"XOAUTH2", WithSMTPAuth(SMTPAuthXOAUTH2), SMTPAuthXOAUTH2}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := NewClient(DefaultHost, tt.option) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if client.smtpAuthType != tt.expected { + t.Errorf("failed to set custom SMTP auth type. Want: %s, got: %s", + tt.expected, client.smtpAuthType) + } + }) + } + }) } +/* + // TestNewClient tests the NewClient() method with its custom options func TestNewClientWithOptions(t *testing.T) { host := "mail.example.com" @@ -92,29 +502,6 @@ func TestNewClientWithOptions(t *testing.T) { option Option shouldfail bool }{ - {"nil option", nil, true}, - {"WithPort()", WithPort(465), false}, - {"WithPort(); port is too high", WithPort(100000), true}, - {"WithTimeout()", WithTimeout(time.Second * 5), false}, - {"WithTimeout()", WithTimeout(-10), true}, - {"WithSSL()", WithSSL(), false}, - {"WithSSLPort(false)", WithSSLPort(false), false}, - {"WithSSLPort(true)", WithSSLPort(true), false}, - {"WithHELO()", WithHELO(host), false}, - {"WithHELO(); helo is empty", WithHELO(""), true}, - {"WithTLSPolicy()", WithTLSPolicy(TLSOpportunistic), false}, - {"WithTLSPortPolicy()", WithTLSPortPolicy(TLSOpportunistic), false}, - {"WithTLSConfig()", WithTLSConfig(&tls.Config{}), false}, - {"WithTLSConfig(); config is nil", WithTLSConfig(nil), true}, - {"WithSMTPAuth(NoAuth)", WithSMTPAuth(SMTPAuthNoAuth), false}, - {"WithSMTPAuth()", WithSMTPAuth(SMTPAuthLogin), false}, - { - "WithSMTPAuthCustom()", - WithSMTPAuthCustom(smtp.PlainAuth("", "", "", "", false)), - false, - }, - {"WithUsername()", WithUsername("test"), false}, - {"WithPassword()", WithPassword("test"), false}, {"WithDSN()", WithDSN(), false}, {"WithDSNMailReturnType()", WithDSNMailReturnType(DSNMailReturnFull), false}, {"WithDSNMailReturnType() wrong option", WithDSNMailReturnType("FAIL"), true}, @@ -2806,3 +3193,6 @@ func handleTestServerConnection(connection net.Conn, featureSet string, failRese } } } + + +*/ From 35f92f2ddc325944bb70c0638dab55b5b4e27625 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 13:51:31 +0200 Subject: [PATCH 011/188] 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. --- client.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client.go b/client.go index af36b47..2e48358 100644 --- a/client.go +++ b/client.go @@ -245,6 +245,9 @@ var ( // 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. @@ -677,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 } From ab8fc3e4fcfaf72c3b682a3fc21ce668e5f633eb Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 13:51:48 +0200 Subject: [PATCH 012/188] 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. --- client_test.go | 176 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 171 insertions(+), 5 deletions(-) diff --git a/client_test.go b/client_test.go index e9b927e..1c08dd9 100644 --- a/client_test.go +++ b/client_test.go @@ -8,6 +8,7 @@ import ( "crypto/tls" "errors" "fmt" + "net" "os" "reflect" "testing" @@ -85,6 +86,8 @@ func TestNewClient(t *testing.T) { }) t.Run("NewClient with option", func(t *testing.T) { hostname := "mail.example.com" + netDailer := net.Dialer{} + tlsDailer := tls.Dialer{NetDialer: &netDailer, Config: &tls.Config{}} tests := []struct { name string option Option @@ -438,6 +441,174 @@ func TestNewClient(t *testing.T) { }, false, nil, }, + { + "WithDSNMailReturnType DSNMailReturnHeadersOnly", + WithDSNMailReturnType(DSNMailReturnHeadersOnly), + func(c *Client) error { + if c.requestDSN != true { + return fmt.Errorf("failed to enable DSN. Want requestDSN: %t, got: %t", true, + c.requestDSN) + } + if c.dsnReturnType != DSNMailReturnHeadersOnly { + return fmt.Errorf("failed to enable DSN. Want dsnReturnType: %s, got: %s", + DSNMailReturnHeadersOnly, c.dsnReturnType) + } + return nil + }, + false, nil, + }, + { + "WithDSNMailReturnType DSNMailReturnFull", + WithDSNMailReturnType(DSNMailReturnFull), + func(c *Client) error { + if c.requestDSN != true { + return fmt.Errorf("failed to enable DSN. Want requestDSN: %t, got: %t", true, + c.requestDSN) + } + if c.dsnReturnType != DSNMailReturnFull { + return fmt.Errorf("failed to enable DSN. Want dsnReturnType: %s, got: %s", + DSNMailReturnFull, c.dsnReturnType) + } + return nil + }, + false, nil, + }, + { + "WithDSNMailReturnType invalid", WithDSNMailReturnType("invalid"), nil, + true, &ErrInvalidDSNMailReturnOption, + }, + { + "WithDSNRcptNotifyType DSNRcptNotifyNever", + WithDSNRcptNotifyType(DSNRcptNotifyNever), + func(c *Client) error { + if c.requestDSN != true { + return fmt.Errorf("failed to enable DSN. Want requestDSN: %t, got: %t", true, + c.requestDSN) + } + if len(c.dsnRcptNotifyType) != 1 { + return fmt.Errorf("failed to enable DSN. Want 1 DSN Rcpt Notify type, got: %d", + len(c.dsnRcptNotifyType)) + } + if c.dsnRcptNotifyType[0] != string(DSNRcptNotifyNever) { + return fmt.Errorf("failed to enable DSN. Want DSN Rcpt Notify Never type: %s, got: %s", + string(DSNRcptNotifyNever), c.dsnRcptNotifyType[0]) + } + return nil + }, + false, nil, + }, + { + "WithDSNRcptNotifyType DSNRcptNotifySuccess, DSNRcptNotifyFailure", + WithDSNRcptNotifyType(DSNRcptNotifySuccess, DSNRcptNotifyFailure), + func(c *Client) error { + if c.requestDSN != true { + return fmt.Errorf("failed to enable DSN. Want requestDSN: %t, got: %t", true, + c.requestDSN) + } + if len(c.dsnRcptNotifyType) != 2 { + return fmt.Errorf("failed to enable DSN. Want 2 DSN Rcpt Notify type, got: %d", + len(c.dsnRcptNotifyType)) + } + if c.dsnRcptNotifyType[0] != string(DSNRcptNotifySuccess) { + return fmt.Errorf("failed to enable DSN. Want DSN Rcpt Notify Success type: %s, got: %s", + string(DSNRcptNotifySuccess), c.dsnRcptNotifyType[0]) + } + if c.dsnRcptNotifyType[1] != string(DSNRcptNotifyFailure) { + return fmt.Errorf("failed to enable DSN. Want DSN Rcpt Notify Failure type: %s, got: %s", + string(DSNRcptNotifyFailure), c.dsnRcptNotifyType[1]) + } + return nil + }, + false, nil, + }, + { + "WithDSNRcptNotifyType DSNRcptNotifyDelay", + WithDSNRcptNotifyType(DSNRcptNotifyDelay), + func(c *Client) error { + if c.requestDSN != true { + return fmt.Errorf("failed to enable DSN. Want requestDSN: %t, got: %t", true, + c.requestDSN) + } + if len(c.dsnRcptNotifyType) != 1 { + return fmt.Errorf("failed to enable DSN. Want 1 DSN Rcpt Notify type, got: %d", + len(c.dsnRcptNotifyType)) + } + if c.dsnRcptNotifyType[0] != string(DSNRcptNotifyDelay) { + return fmt.Errorf("failed to enable DSN. Want DSN Rcpt Notify Delay type: %s, got: %s", + string(DSNRcptNotifyDelay), c.dsnRcptNotifyType[0]) + } + return nil + }, + false, nil, + }, + { + "WithDSNRcptNotifyType invalid", WithDSNRcptNotifyType("invalid"), nil, + true, &ErrInvalidDSNRcptNotifyOption, + }, + { + "WithDSNRcptNotifyType mix valid and invalid", + WithDSNRcptNotifyType(DSNRcptNotifyDelay, "invalid"), nil, + true, &ErrInvalidDSNRcptNotifyOption, + }, + { + "WithDSNRcptNotifyType mix NEVER with SUCCESS", + WithDSNRcptNotifyType(DSNRcptNotifyNever, DSNRcptNotifySuccess), nil, + true, &ErrInvalidDSNRcptNotifyCombination, + }, + { + "WithDSNRcptNotifyType mix NEVER with FAIL", + WithDSNRcptNotifyType(DSNRcptNotifyNever, DSNRcptNotifyFailure), nil, + true, &ErrInvalidDSNRcptNotifyCombination, + }, + { + "WithDSNRcptNotifyType mix NEVER with DELAY", + WithDSNRcptNotifyType(DSNRcptNotifyNever, DSNRcptNotifyDelay), nil, + true, &ErrInvalidDSNRcptNotifyCombination, + }, + { + "WithoutNoop", WithoutNoop(), + func(c *Client) error { + if !c.noNoop { + return fmt.Errorf("failed to disable Noop. Want noNoop: %t, got: %t", false, c.noNoop) + } + return nil + }, + false, nil, + }, + { + "WithDialContextFunc with net.Dailer", WithDialContextFunc(netDailer.DialContext), + func(c *Client) error { + if c.dialContextFunc == nil { + return errors.New("failed to set dial context func, got: nil") + } + ctxType := reflect.TypeOf(c.dialContextFunc).String() + if ctxType != "mail.DialContextFunc" { + return fmt.Errorf("failed to set dial context func, want: %s, got: %s", + "mail.DialContextFunc", ctxType) + } + return nil + }, + false, nil, + }, + { + "WithDialContextFunc with tls.Dailer", WithDialContextFunc(tlsDailer.DialContext), + func(c *Client) error { + if c.dialContextFunc == nil { + return errors.New("failed to set dial context func, got: nil") + } + ctxType := reflect.TypeOf(c.dialContextFunc).String() + if ctxType != "mail.DialContextFunc" { + return fmt.Errorf("failed to set dial context func, want: %s, got: %s", + "mail.DialContextFunc", ctxType) + } + return nil + }, + false, nil, + }, + { + "WithDialContextFunc with nil", WithDialContextFunc(nil), nil, + true, &ErrDialContextFuncIsNil, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -502,11 +673,6 @@ func TestNewClientWithOptions(t *testing.T) { option Option shouldfail bool }{ - {"WithDSN()", WithDSN(), false}, - {"WithDSNMailReturnType()", WithDSNMailReturnType(DSNMailReturnFull), false}, - {"WithDSNMailReturnType() wrong option", WithDSNMailReturnType("FAIL"), true}, - {"WithDSNRcptNotifyType()", WithDSNRcptNotifyType(DSNRcptNotifySuccess), false}, - {"WithDSNRcptNotifyType() wrong option", WithDSNRcptNotifyType("FAIL"), true}, {"WithoutNoop()", WithoutNoop(), false}, {"WithDebugLog()", WithDebugLog(), false}, {"WithLogger()", WithLogger(log.New(os.Stderr, log.LevelDebug)), false}, From c5b57543c117fa0b7bc69480febdb590c2f904e9 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 14:08:19 +0200 Subject: [PATCH 013/188] 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. --- client_test.go | 166 ++++++++++++++++--------------------------------- 1 file changed, 55 insertions(+), 111 deletions(-) diff --git a/client_test.go b/client_test.go index 1c08dd9..bfd0253 100644 --- a/client_test.go +++ b/client_test.go @@ -5,6 +5,7 @@ package mail import ( + "context" "crypto/tls" "errors" "fmt" @@ -95,7 +96,7 @@ func TestNewClient(t *testing.T) { shouldfail bool expectErr *error }{ - {"nil option", nil, nil, true, nil}, + {"nil option", nil, nil, false, nil}, { "WithPort", WithPort(465), func(c *Client) error { @@ -307,6 +308,25 @@ func TestNewClient(t *testing.T) { c.tlspolicy) } if c.port != 25 { + return fmt.Errorf("failed to set custom TLS policy. Want port: %d, got: %d", 25, + c.port) + } + if c.fallbackPort != 0 { + return fmt.Errorf("failed to set custom TLS policy. Want fallback port: %d, got: %d", + 0, c.fallbackPort) + } + return nil + }, + false, nil, + }, + { + "WithTLSPortPolicy invalid", WithTLSPortPolicy(-1), + func(c *Client) error { + if c.tlspolicy.String() != "UnknownPolicy" { + return fmt.Errorf("failed to set custom TLS policy. Want: %s, got: %s", "UnknownPolicy", + c.tlspolicy) + } + if c.port != 587 { return fmt.Errorf("failed to set custom TLS policy. Want port: %d, got: %d", 587, c.port) } @@ -605,10 +625,41 @@ func TestNewClient(t *testing.T) { }, false, nil, }, + { + "WithDialContextFunc with custom dialer", + WithDialContextFunc( + func(ctx context.Context, network, address string) (net.Conn, error) { + return nil, nil + }, + ), + func(c *Client) error { + if c.dialContextFunc == nil { + return errors.New("failed to set dial context func, got: nil") + } + ctxType := reflect.TypeOf(c.dialContextFunc).String() + if ctxType != "mail.DialContextFunc" { + return fmt.Errorf("failed to set dial context func, want: %s, got: %s", + "mail.DialContextFunc", ctxType) + } + return nil + }, + false, nil, + }, { "WithDialContextFunc with nil", WithDialContextFunc(nil), nil, true, &ErrDialContextFuncIsNil, }, + { + "WithLogAuthData", WithLogAuthData(), + func(c *Client) error { + if !c.logAuthData { + return fmt.Errorf("failed to enable auth data logging. Want logAuthData: %t, got: %t", + true, c.logAuthData) + } + return nil + }, + false, nil, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -616,6 +667,9 @@ func TestNewClient(t *testing.T) { 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", @@ -665,116 +719,6 @@ func TestNewClient(t *testing.T) { /* -// TestNewClient tests the NewClient() method with its custom options -func TestNewClientWithOptions(t *testing.T) { - host := "mail.example.com" - tests := []struct { - name string - option Option - shouldfail bool - }{ - {"WithoutNoop()", WithoutNoop(), false}, - {"WithDebugLog()", WithDebugLog(), false}, - {"WithLogger()", WithLogger(log.New(os.Stderr, log.LevelDebug)), false}, - {"WithLogger()", WithLogAuthData(), false}, - {"WithDialContextFunc()", WithDialContextFunc(func(ctx context.Context, network, address string) (net.Conn, error) { - return nil, nil - }), false}, - - { - "WithDSNRcptNotifyType() NEVER combination", - WithDSNRcptNotifyType(DSNRcptNotifySuccess, DSNRcptNotifyNever), true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c, err := NewClient(DefaultHost, tt.option, nil) - if err != nil && !tt.shouldfail { - t.Errorf("failed to create new client: %s", err) - return - } - _ = c - }) - } -} - -// TestWithHELO tests the WithHELO() option for the NewClient() method -func TestWithHELO(t *testing.T) { - tests := []struct { - name string - value string - want string - }{ - {"HELO test.de", "test.de", "test.de"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c, err := NewClient(DefaultHost, WithHELO(tt.value)) - if err != nil { - t.Errorf("failed to create new client: %s", err) - return - } - if c.helo != tt.want { - t.Errorf("failed to set custom HELO. Want: %s, got: %s", tt.want, c.helo) - } - }) - } -} - -// TestWithPort tests the WithPort() option for the NewClient() method -func TestWithPort(t *testing.T) { - tests := []struct { - name string - value int - want int - sf bool - }{ - {"set port to 25", 25, 25, false}, - {"set port to 465", 465, 465, false}, - {"set port to 100000", 100000, 25, true}, - {"set port to -10", -10, 25, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c, err := NewClient(DefaultHost, WithPort(tt.value)) - if err != nil && !tt.sf { - t.Errorf("failed to create new client: %s", err) - return - } - if c.port != tt.want { - t.Errorf("failed to set custom port. Want: %d, got: %d", tt.want, c.port) - } - }) - } -} - -// TestWithTimeout tests the WithTimeout() option for the NewClient() method -func TestWithTimeout(t *testing.T) { - tests := []struct { - name string - value time.Duration - want time.Duration - sf bool - }{ - {"set timeout to 5s", time.Second * 5, time.Second * 5, false}, - {"set timeout to 30s", time.Second * 30, time.Second * 30, false}, - {"set timeout to 1m", time.Minute, time.Minute, false}, - {"set timeout to 0", 0, DefaultTimeout, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c, err := NewClient(DefaultHost, WithTimeout(tt.value)) - if err != nil && !tt.sf { - t.Errorf("failed to create new client: %s", err) - return - } - if c.connTimeout != tt.want { - t.Errorf("failed to set custom timeout. Want: %d, got: %d", tt.want, c.connTimeout) - } - }) - } -} - // TestWithTLSPolicy tests the WithTLSPolicy() option for the NewClient() method func TestWithTLSPolicy(t *testing.T) { tests := []struct { From 3efd2b529f5c2262e4442127e02c8d5a543a1e1c Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 14:39:28 +0200 Subject: [PATCH 014/188] 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. --- client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/client.go b/client.go index 2e48358..f65766d 100644 --- a/client.go +++ b/client.go @@ -751,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 From 68bc5dde724c1b411ccc251c900a0de44927e9a1 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 15:15:59 +0200 Subject: [PATCH 015/188] 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. --- client_test.go | 636 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 483 insertions(+), 153 deletions(-) diff --git a/client_test.go b/client_test.go index bfd0253..f3603e5 100644 --- a/client_test.go +++ b/client_test.go @@ -5,6 +5,8 @@ package mail import ( + "bufio" + "bytes" "context" "crypto/tls" "errors" @@ -12,6 +14,8 @@ import ( "net" "os" "reflect" + "strings" + "sync/atomic" "testing" "time" @@ -32,6 +36,9 @@ const ( TestServerPortBase = 2025 ) +// PortAdder is an atomic counter used to increment port numbers for the test SMTP server instances. +var PortAdder atomic.Int32 + func TestNewClient(t *testing.T) { t.Run("create new Client", func(t *testing.T) { client, err := NewClient(DefaultHost) @@ -717,98 +724,483 @@ func TestNewClient(t *testing.T) { }) } +func TestClient_TLSPolicy(t *testing.T) { + t.Run("WithTLSPolicy fmt.Stringer interface", func(t *testing.T) {}) + tests := []struct { + name string + value TLSPolicy + want string + }{ + {"TLSMandatory", TLSMandatory, "TLSMandatory"}, + {"TLSOpportunistic", TLSOpportunistic, "TLSOpportunistic"}, + {"NoTLS", NoTLS, "NoTLS"}, + {"Invalid", -1, "UnknownPolicy"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := NewClient(DefaultHost, WithTLSPolicy(tt.value)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + got := client.TLSPolicy() + if !strings.EqualFold(got, tt.want) { + t.Errorf("failed to get expected TLS policy string. Want: %s, got: %s", tt.want, got) + } + }) + } +} + +func TestClient_ServerAddr(t *testing.T) { + t.Run("ServerAddr of default client", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + got := client.ServerAddr() + expected := fmt.Sprintf("%s:%d", DefaultHost, DefaultPort) + if !strings.EqualFold(expected, got) { + t.Errorf("failed to get expected server address. Want: %s, got: %s", expected, got) + } + }) + t.Run("ServerAddr of with custom port", func(t *testing.T) { + client, err := NewClient(DefaultHost, WithPort(587)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + got := client.ServerAddr() + expected := fmt.Sprintf("%s:%d", DefaultHost, 587) + if !strings.EqualFold(expected, got) { + t.Errorf("failed to get expected server address. Want: %s, got: %s", expected, got) + } + }) + t.Run("ServerAddr of with port policy TLSMandatory", func(t *testing.T) { + client, err := NewClient(DefaultHost, WithTLSPortPolicy(TLSMandatory)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + got := client.ServerAddr() + expected := fmt.Sprintf("%s:%d", DefaultHost, 587) + if !strings.EqualFold(expected, got) { + t.Errorf("failed to get expected server address. Want: %s, got: %s", expected, got) + } + }) + t.Run("ServerAddr of with port policy TLSOpportunistic", func(t *testing.T) { + client, err := NewClient(DefaultHost, WithTLSPortPolicy(TLSOpportunistic)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + got := client.ServerAddr() + expected := fmt.Sprintf("%s:%d", DefaultHost, 587) + if !strings.EqualFold(expected, got) { + t.Errorf("failed to get expected server address. Want: %s, got: %s", expected, got) + } + }) + t.Run("ServerAddr of with port policy NoTLS", func(t *testing.T) { + client, err := NewClient(DefaultHost, WithTLSPortPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + got := client.ServerAddr() + expected := fmt.Sprintf("%s:%d", DefaultHost, 25) + if !strings.EqualFold(expected, got) { + t.Errorf("failed to get expected server address. Want: %s, got: %s", expected, got) + } + }) + t.Run("ServerAddr of with SSL", func(t *testing.T) { + client, err := NewClient(DefaultHost, WithSSLPort(false)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + got := client.ServerAddr() + expected := fmt.Sprintf("%s:%d", DefaultHost, 465) + if !strings.EqualFold(expected, got) { + t.Errorf("failed to get expected server address. Want: %s, got: %s", expected, got) + } + }) +} + +func TestClient_SetTLSPolicy(t *testing.T) { + t.Run("SetTLSPolicy TLSMandatory", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetTLSPolicy(TLSMandatory) + if client.tlspolicy != TLSMandatory { + t.Errorf("failed to set expected TLS policy. Want: %s, got: %s", + TLSMandatory, client.tlspolicy) + } + }) + t.Run("SetTLSPolicy TLSOpportunistic", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetTLSPolicy(TLSOpportunistic) + if client.tlspolicy != TLSOpportunistic { + t.Errorf("failed to set expected TLS policy. Want: %s, got: %s", + TLSOpportunistic, client.tlspolicy) + } + }) + t.Run("SetTLSPolicy NoTLS", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetTLSPolicy(NoTLS) + if client.tlspolicy != NoTLS { + t.Errorf("failed to set expected TLS policy. Want: %s, got: %s", + NoTLS, client.tlspolicy) + } + }) + t.Run("SetTLSPolicy to override WithTLSPolicy", func(t *testing.T) { + client, err := NewClient(DefaultHost, WithTLSPolicy(TLSOpportunistic)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetTLSPolicy(TLSMandatory) + if client.tlspolicy != TLSMandatory { + t.Errorf("failed to set expected TLS policy. Want: %s, got: %s", + TLSMandatory, client.tlspolicy) + } + }) +} + +func TestClient_SetTLSPortPolicy(t *testing.T) { + t.Run("SetTLSPortPolicy TLSMandatory", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetTLSPortPolicy(TLSMandatory) + if client.tlspolicy != TLSMandatory { + t.Errorf("failed to set expected TLS policy. Want policy: %s, got: %s", + TLSMandatory, client.tlspolicy) + } + if client.port != 587 { + t.Errorf("failed to set expected TLS policy. Want port: %d, got: %d", 587, client.port) + } + if client.fallbackPort != 0 { + t.Errorf("failed to set expected TLS policy. Want fallback port: %d, got: %d", 0, + client.fallbackPort) + } + }) + t.Run("SetTLSPortPolicy TLSOpportunistic", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetTLSPortPolicy(TLSOpportunistic) + if client.tlspolicy != TLSOpportunistic { + t.Errorf("failed to set expected TLS policy. Want policy: %s, got: %s", + TLSOpportunistic, client.tlspolicy) + } + if client.port != 587 { + t.Errorf("failed to set expected TLS policy. Want port: %d, got: %d", 587, client.port) + } + if client.fallbackPort != 25 { + t.Errorf("failed to set expected TLS policy. Want fallback port: %d, got: %d", 25, + client.fallbackPort) + } + }) + t.Run("SetTLSPortPolicy NoTLS", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetTLSPortPolicy(NoTLS) + if client.tlspolicy != NoTLS { + t.Errorf("failed to set expected TLS policy. Want policy: %s, got: %s", + NoTLS, client.tlspolicy) + } + if client.port != 25 { + t.Errorf("failed to set expected TLS policy. Want port: %d, got: %d", 25, client.port) + } + if client.fallbackPort != 0 { + t.Errorf("failed to set expected TLS policy. Want fallback port: %d, got: %d", 0, + client.fallbackPort) + } + }) + t.Run("SetTLSPortPolicy to override WithTLSPortPolicy", func(t *testing.T) { + client, err := NewClient(DefaultHost, WithTLSPortPolicy(TLSOpportunistic), WithPort(25)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetTLSPortPolicy(TLSMandatory) + if client.tlspolicy != TLSMandatory { + t.Errorf("failed to set expected TLS policy. Want policy: %s, got: %s", + TLSMandatory, client.tlspolicy) + } + if client.port != 587 { + t.Errorf("failed to set expected TLS policy. Want port: %d, got: %d", 587, client.port) + } + if client.fallbackPort != 0 { + t.Errorf("failed to set expected TLS policy. Want fallback port: %d, got: %d", 0, + client.fallbackPort) + } + }) +} + +func TestClient_SetSSL(t *testing.T) { + t.Run("SetSSL true", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetSSL(true) + if !client.useSSL { + t.Errorf("failed to set expected useSSL: %t", true) + } + }) + t.Run("SetSSL false", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetSSL(false) + if client.useSSL { + t.Errorf("failed to set expected useSSL: %t", false) + } + }) + t.Run("SetSSL to override WithSSL", func(t *testing.T) { + client, err := NewClient(DefaultHost, WithSSL()) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetSSL(false) + if client.useSSL { + t.Errorf("failed to set expected useSSL: %t", false) + } + }) +} + +func TestClient_SetSSLPort(t *testing.T) { + t.Run("SetSSLPort true no fallback", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetSSLPort(true, false) + if !client.useSSL { + t.Errorf("failed to set expected useSSL: %t", true) + } + if client.port != 465 { + t.Errorf("failed to set expected port: %d, got: %d", 465, client.port) + } + if client.fallbackPort != 0 { + t.Errorf("failed to set expected fallback: %d, got: %d", 0, client.fallbackPort) + } + }) + t.Run("SetSSLPort true with fallback", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetSSLPort(true, true) + if !client.useSSL { + t.Errorf("failed to set expected useSSL: %t", true) + } + if client.port != 465 { + t.Errorf("failed to set expected port: %d, got: %d", 465, client.port) + } + if client.fallbackPort != 25 { + t.Errorf("failed to set expected fallback: %d, got: %d", 25, client.fallbackPort) + } + }) + t.Run("SetSSLPort false no fallback", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetSSLPort(false, false) + if client.useSSL { + t.Errorf("failed to set expected useSSL: %t", false) + } + if client.port != 25 { + t.Errorf("failed to set expected port: %d, got: %d", 25, client.port) + } + if client.fallbackPort != 0 { + t.Errorf("failed to set expected fallback: %d, got: %d", 0, client.fallbackPort) + } + }) + t.Run("SetSSLPort false with fallback (makes no sense)", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetSSLPort(false, true) + if client.useSSL { + t.Errorf("failed to set expected useSSL: %t", false) + } + if client.port != 25 { + t.Errorf("failed to set expected port: %d, got: %d", 25, client.port) + } + if client.fallbackPort != 25 { + t.Errorf("failed to set expected fallback: %d, got: %d", 25, client.fallbackPort) + } + }) + t.Run("SetSSLPort to override WithSSL", func(t *testing.T) { + client, err := NewClient(DefaultHost, WithSSL()) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetSSLPort(false, false) + if client.useSSL { + t.Errorf("failed to set expected useSSL: %t", false) + } + if client.port != 25 { + t.Errorf("failed to set expected port: %d, got: %d", 25, client.port) + } + if client.fallbackPort != 0 { + t.Errorf("failed to set expected fallback: %d, got: %d", 0, client.fallbackPort) + } + }) + t.Run("SetSSLPort with custom port", func(t *testing.T) { + client, err := NewClient(DefaultHost, WithPort(123)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetSSLPort(false, false) + if client.useSSL { + t.Errorf("failed to set expected useSSL: %t", false) + } + if client.port != 123 { + t.Errorf("failed to set expected port: %d, got: %d", 123, client.port) + } + if client.fallbackPort != 0 { + t.Errorf("failed to set expected fallback: %d, got: %d", 0, client.fallbackPort) + } + }) +} + +func TestClient_SetDebugLog(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, featureSet, false, serverPort); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + t.Run("SetDebugLog true", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetDebugLog(true) + if !client.useDebugLog { + t.Errorf("failed to set expected useDebugLog: %t", true) + } + }) + t.Run("SetDebugLog false", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetDebugLog(false) + if client.useDebugLog { + t.Errorf("failed to set expected useDebugLog: %t", false) + } + }) + t.Run("SetDebugLog true with active SMTP client", func(t *testing.T) { + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + buffer := &bytes.Buffer{} + client.SetLogger(log.New(buffer, log.LevelDebug)) + client.SetDebugLog(true) + + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("failed to close client to test server: %s", err) + } + }) + + if !client.useDebugLog { + t.Errorf("failed to set expected useDebugLog: %t", true) + } + if !strings.Contains(buffer.String(), "DEBUG: C --> S: EHLO") { + t.Errorf("failed to enable debug log. Expected string: %s in log buffer but didn't find it. "+ + "Buffer: %s", "DEBUG: C --> S: EHLO", buffer.String()) + } + }) + t.Run("SetDebugLog false to override WithDebugLog", func(t *testing.T) { + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS), WithDebugLog()) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + buffer := &bytes.Buffer{} + client.SetLogger(log.New(buffer, log.LevelDebug)) + client.SetDebugLog(false) + + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("failed to close client to test server: %s", err) + } + }) + + if client.useDebugLog { + t.Errorf("failed to set expected useDebugLog: %t", false) + } + if buffer.Len() > 0 { + t.Errorf("failed to disable debug logger. Expected buffer to be empty but got: %d", buffer.Len()) + } + }) + t.Run("SetDebugLog true active SMTP client after dial", func(t *testing.T) { + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("failed to close client to test server: %s", err) + } + }) + + buffer := &bytes.Buffer{} + client.SetLogger(log.New(buffer, log.LevelDebug)) + client.SetDebugLog(true) + if err = client.smtpClient.Noop(); err != nil { + t.Errorf("failed to send NOOP command: %s", err) + } + + if !client.useDebugLog { + t.Errorf("failed to set expected useDebugLog: %t", true) + } + if !strings.Contains(buffer.String(), "DEBUG: C --> S: NOOP") { + t.Errorf("failed to enable debug log. Expected string: %s in log buffer but didn't find it. "+ + "Buffer: %s", "DEBUG: C --> S: NOOP", buffer.String()) + } + }) +} + /* -// TestWithTLSPolicy tests the WithTLSPolicy() option for the NewClient() method -func TestWithTLSPolicy(t *testing.T) { - tests := []struct { - name string - value TLSPolicy - want string - sf bool - }{ - {"Policy: TLSMandatory", TLSMandatory, TLSMandatory.String(), false}, - {"Policy: TLSOpportunistic", TLSOpportunistic, TLSOpportunistic.String(), false}, - {"Policy: NoTLS", NoTLS, NoTLS.String(), false}, - {"Policy: Invalid", -1, "UnknownPolicy", true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c, err := NewClient(DefaultHost, WithTLSPolicy(tt.value)) - if err != nil && !tt.sf { - t.Errorf("failed to create new client: %s", err) - return - } - if c.tlspolicy.String() != tt.want { - t.Errorf("failed to set TLSPolicy. Want: %s, got: %s", tt.want, c.tlspolicy) - } - }) - } -} - -// TestWithTLSPortPolicy tests the WithTLSPortPolicy() option for the NewClient() method -func TestWithTLSPortPolicy(t *testing.T) { - tests := []struct { - name string - value TLSPolicy - want string - wantPort int - fbPort int - sf bool - }{ - {"Policy: TLSMandatory", TLSMandatory, TLSMandatory.String(), 587, 0, false}, - {"Policy: TLSOpportunistic", TLSOpportunistic, TLSOpportunistic.String(), 587, 25, false}, - {"Policy: NoTLS", NoTLS, NoTLS.String(), 25, 0, false}, - {"Policy: Invalid", -1, "UnknownPolicy", 587, 0, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c, err := NewClient(DefaultHost, WithTLSPortPolicy(tt.value)) - if err != nil && !tt.sf { - t.Errorf("failed to create new client: %s", err) - return - } - if c.tlspolicy.String() != tt.want { - t.Errorf("failed to set TLSPortPolicy. Want: %s, got: %s", tt.want, c.tlspolicy) - } - if c.port != tt.wantPort { - t.Errorf("failed to set TLSPortPolicy, wanted port: %d, got: %d", tt.wantPort, c.port) - } - if c.fallbackPort != tt.fbPort { - t.Errorf("failed to set TLSPortPolicy, wanted fallbakc port: %d, got: %d", tt.fbPort, - c.fallbackPort) - } - }) - } -} - -// TestSetTLSPolicy tests the SetTLSPolicy() method for the Client object -func TestSetTLSPolicy(t *testing.T) { - tests := []struct { - name string - value TLSPolicy - want string - sf bool - }{ - {"Policy: TLSMandatory", TLSMandatory, TLSMandatory.String(), false}, - {"Policy: TLSOpportunistic", TLSOpportunistic, TLSOpportunistic.String(), false}, - {"Policy: NoTLS", NoTLS, NoTLS.String(), false}, - {"Policy: Invalid", -1, "UnknownPolicy", true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c, err := NewClient(DefaultHost, WithTLSPolicy(NoTLS)) - if err != nil { - t.Errorf("failed to create new client: %s", err) - return - } - c.SetTLSPolicy(tt.value) - if c.tlspolicy.String() != tt.want { - t.Errorf("failed to set TLSPolicy. Want: %s, got: %s", tt.want, c.tlspolicy) - } - }) - } -} // TestSetTLSConfig tests the SetTLSConfig() method for the Client object func TestSetTLSConfig(t *testing.T) { @@ -835,66 +1227,6 @@ func TestSetTLSConfig(t *testing.T) { } } -// TestSetSSL tests the SetSSL() method for the Client object -func TestSetSSL(t *testing.T) { - tests := []struct { - name string - value bool - }{ - {"SSL: on", true}, - {"SSL: off", false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c, err := NewClient(DefaultHost) - if err != nil { - t.Errorf("failed to create new client: %s", err) - return - } - c.SetSSL(tt.value) - if c.useSSL != tt.value { - t.Errorf("failed to set SSL setting. Got: %t, want: %t", c.useSSL, tt.value) - } - }) - } -} - -// TestSetSSLPort tests the Client.SetSSLPort method -func TestClient_SetSSLPort(t *testing.T) { - tests := []struct { - name string - value bool - fb bool - port int - fbPort int - }{ - {"SSL: on, fb: off", true, false, 465, 0}, - {"SSL: on, fb: on", true, true, 465, 25}, - {"SSL: off, fb: off", false, false, 25, 0}, - {"SSL: off, fb: on", false, true, 25, 25}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c, err := NewClient(DefaultHost) - if err != nil { - t.Errorf("failed to create new client: %s", err) - return - } - c.SetSSLPort(tt.value, tt.fb) - if c.useSSL != tt.value { - t.Errorf("failed to set SSL setting. Got: %t, want: %t", c.useSSL, tt.value) - } - if c.port != tt.port { - t.Errorf("failed to set SSLPort, wanted port: %d, got: %d", c.port, tt.port) - } - if c.fallbackPort != tt.fbPort { - t.Errorf("failed to set SSLPort, wanted fallback port: %d, got: %d", c.fallbackPort, - tt.fbPort) - } - }) - } -} - // TestSetUsername tests the SetUsername method for the Client object func TestSetUsername(t *testing.T) { tests := []struct { @@ -3104,6 +3436,7 @@ func (f faker) RemoteAddr() net.Addr { return nil } func (f faker) SetDeadline(time.Time) error { return nil } func (f faker) SetReadDeadline(time.Time) error { return nil } func (f faker) SetWriteDeadline(time.Time) error { return nil } +*/ // simpleSMTPServer starts a simple TCP server that resonds to SMTP commands. // The provided featureSet represents in what the server responds to EHLO command @@ -3303,6 +3636,3 @@ func handleTestServerConnection(connection net.Conn, featureSet string, failRese } } } - - -*/ From c946f74ad262d9cebaeebf85b7cea8a50e6ff462 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 15:31:00 +0200 Subject: [PATCH 016/188] 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. --- client_test.go | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/client_test.go b/client_test.go index f3603e5..ad1b483 100644 --- a/client_test.go +++ b/client_test.go @@ -1199,6 +1199,96 @@ func TestClient_SetDebugLog(t *testing.T) { }) } +func TestClient_SetTLSConfig(t *testing.T) { + t.Run("SetTLSConfig with &tls.Config", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.SetTLSConfig(&tls.Config{}); err != nil { + t.Errorf("failed to set expected TLSConfig: %s", err) + } + if client.tlsconfig == nil { + t.Fatalf("failed to set expected TLSConfig. TLSConfig is nil") + } + }) + t.Run("SetTLSConfig with InsecureSkipVerify", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}); err != nil { + t.Errorf("failed to set expected TLSConfig: %s", err) + } + if client.tlsconfig == nil { + t.Fatalf("failed to set expected TLSConfig. TLSConfig is nil") + } + if !client.tlsconfig.InsecureSkipVerify { + t.Errorf("failed to set expected TLSConfig. Expected InsecureSkipVerify: %t, got: %t", true, + client.tlsconfig.InsecureSkipVerify) + } + }) + t.Run("SetTLSConfig with nil should fail", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + err = client.SetTLSConfig(nil) + if err == nil { + t.Errorf("SetTLSConfig with nil should fail") + } + if !errors.Is(err, ErrInvalidTLSConfig) { + t.Errorf("SetTLSConfig was expected to fail with %s, got: %s", ErrInvalidTLSConfig, err) + } + }) +} + +func TestClient_SetUsername(t *testing.T) { + t.Run("SetUsername", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetUsername("toni.tester") + if client.user != "toni.tester" { + t.Errorf("failed to set expected username, want: %s, got: %s", "toni.tester", client.user) + } + }) + t.Run("SetUsername to override WithUsername", func(t *testing.T) { + client, err := NewClient(DefaultHost, WithUsername("toni.tester")) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetUsername("tina.tester") + if client.user != "tina.tester" { + t.Errorf("failed to set expected username, want: %s, got: %s", "tina.tester", client.user) + } + }) +} + +func TestClient_SetPassword(t *testing.T) { + t.Run("SetPassword", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetPassword("sU*perS3crEt") + if client.pass != "sU*perS3crEt" { + t.Errorf("failed to set expected password, want: %s, got: %s", "sU*perS3crEt", client.pass) + } + }) + t.Run("SetPassword to override WithPassword", func(t *testing.T) { + client, err := NewClient(DefaultHost, WithPassword("sU*perS3crEt")) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetPassword("Su%perS3crEt") + if client.pass != "Su%perS3crEt" { + t.Errorf("failed to set expected password, want: %s, got: %s", "Su%perS3crEt", client.pass) + } + }) +} + /* From 17cb590a455f27cb5409df03ef9f112d9096a7f2 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 15:44:21 +0200 Subject: [PATCH 017/188] 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. --- client_test.go | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/client_test.go b/client_test.go index ad1b483..adeeec5 100644 --- a/client_test.go +++ b/client_test.go @@ -1289,6 +1289,96 @@ func TestClient_SetPassword(t *testing.T) { }) } +func TestClient_SetSMTPAuth(t *testing.T) { + t.Run("SetSMTPAuth", func(t *testing.T) { + tests := []struct { + name string + auth SMTPAuthType + expected SMTPAuthType + }{ + {"CRAM-MD5", SMTPAuthCramMD5, SMTPAuthCramMD5}, + {"LOGIN", SMTPAuthLogin, SMTPAuthLogin}, + {"LOGIN-NOENC", SMTPAuthLoginNoEnc, SMTPAuthLoginNoEnc}, + {"NOAUTH", SMTPAuthNoAuth, SMTPAuthNoAuth}, + {"PLAIN", SMTPAuthPlain, SMTPAuthPlain}, + {"PLAIN-NOENC", SMTPAuthPlainNoEnc, SMTPAuthPlainNoEnc}, + {"SCRAM-SHA-1", SMTPAuthSCRAMSHA1, SMTPAuthSCRAMSHA1}, + {"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS, SMTPAuthSCRAMSHA1PLUS}, + {"SCRAM-SHA-256", SMTPAuthSCRAMSHA256, SMTPAuthSCRAMSHA256}, + {"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS, SMTPAuthSCRAMSHA256PLUS}, + {"XOAUTH2", SMTPAuthXOAUTH2, SMTPAuthXOAUTH2}, + } + + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client.SetSMTPAuth(tt.auth) + if client.smtpAuthType != tt.expected { + t.Errorf("failed to set expected SMTPAuthType, want: %s, got: %s", tt.expected, + client.smtpAuthType) + } + }) + } + }) + t.Run("SetSMTPAuth to override WithSMTPAuth", func(t *testing.T) { + tests := []struct { + name string + auth SMTPAuthType + expected SMTPAuthType + }{ + {"CRAM-MD5", SMTPAuthCramMD5, SMTPAuthCramMD5}, + {"LOGIN", SMTPAuthLogin, SMTPAuthLogin}, + {"LOGIN-NOENC", SMTPAuthLoginNoEnc, SMTPAuthLoginNoEnc}, + {"NOAUTH", SMTPAuthNoAuth, SMTPAuthNoAuth}, + {"PLAIN", SMTPAuthPlain, SMTPAuthPlain}, + {"PLAIN-NOENC", SMTPAuthPlainNoEnc, SMTPAuthPlainNoEnc}, + {"SCRAM-SHA-1", SMTPAuthSCRAMSHA1, SMTPAuthSCRAMSHA1}, + {"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS, SMTPAuthSCRAMSHA1PLUS}, + {"SCRAM-SHA-256", SMTPAuthSCRAMSHA256, SMTPAuthSCRAMSHA256}, + {"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS, SMTPAuthSCRAMSHA256PLUS}, + {"XOAUTH2", SMTPAuthXOAUTH2, SMTPAuthXOAUTH2}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := NewClient(DefaultHost, WithSMTPAuth(SMTPAuthLogin)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if client.smtpAuthType != SMTPAuthLogin { + t.Fatalf("failed to create client with LOGIN auth, got: %s", client.smtpAuthType) + } + client.SetSMTPAuth(tt.auth) + if client.smtpAuthType != tt.expected { + t.Errorf("failed to set expected SMTPAuthType, want: %s, got: %s", tt.expected, + client.smtpAuthType) + } + }) + } + }) + t.Run("SetSMTPAuth override custom auth", func(t *testing.T) { + client, err := NewClient(DefaultHost, + WithSMTPAuthCustom(smtp.LoginAuth("", "", "", false))) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if client.smtpAuthType != SMTPAuthCustom { + t.Fatalf("failed to create client with Custom auth, got: %s", client.smtpAuthType) + } + client.SetSMTPAuth(SMTPAuthSCRAMSHA256) + if client.smtpAuthType != SMTPAuthSCRAMSHA256 { + t.Errorf("failed to set expected SMTPAuthType, want: %s, got: %s", SMTPAuthSCRAMSHA256, + client.smtpAuthType) + } + if client.smtpAuth != nil { + t.Errorf("failed to set expected SMTPAuth, want: nil, got: %s", client.smtpAuth) + } + }) +} + /* From d4dc212dd3e30e275aed443b5c1574e8cf476aa3 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 16:14:05 +0200 Subject: [PATCH 018/188] 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. --- client_test.go | 128 ++++++++++--------------------------------------- 1 file changed, 24 insertions(+), 104 deletions(-) diff --git a/client_test.go b/client_test.go index adeeec5..0c762a8 100644 --- a/client_test.go +++ b/client_test.go @@ -1379,113 +1379,33 @@ func TestClient_SetSMTPAuth(t *testing.T) { }) } +func TestClient_SetSMTPAuthCustom(t *testing.T) { + t.Run("SetSMTPAuthCustom with PLAIN auth", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetSMTPAuthCustom( + smtp.PlainAuth("", "", "", "", false), + ) + if client.smtpAuth == nil { + t.Errorf("failed to set custom SMTP auth, expected auth method but got nil") + } + if client.smtpAuthType != SMTPAuthCustom { + t.Errorf("failed to set custom SMTP auth, want auth type: %s, got: %s", SMTPAuthCustom, + client.smtpAuthType) + } + authType := reflect.TypeOf(client.smtpAuth).String() + if authType != "*smtp.plainAuth" { + t.Errorf("failed to set custom SMTP auth, expected auth method type: %s, got: %s", + "*smtp.plainAuth", authType) + } + }) +} + /* -// TestSetTLSConfig tests the SetTLSConfig() method for the Client object -func TestSetTLSConfig(t *testing.T) { - tests := []struct { - name string - value *tls.Config - sf bool - }{ - {"default config", &tls.Config{}, false}, - {"nil config", nil, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c, err := NewClient(DefaultHost) - if err != nil { - t.Errorf("failed to create new client: %s", err) - return - } - if err := c.SetTLSConfig(tt.value); err != nil && !tt.sf { - t.Errorf("failed to set TLSConfig: %s", err) - return - } - }) - } -} - -// TestSetUsername tests the SetUsername method for the Client object -func TestSetUsername(t *testing.T) { - tests := []struct { - name string - value string - want string - sf bool - }{ - {"normal username", "testuser", "testuser", false}, - {"empty username", "", "", false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c, err := NewClient(DefaultHost) - if err != nil { - t.Errorf("failed to create new client: %s", err) - return - } - c.SetUsername(tt.value) - if c.user != tt.want { - t.Errorf("failed to set username. Expected %s, got: %s", tt.want, c.user) - } - }) - } -} - -// TestSetPassword tests the SetPassword method for the Client object -func TestSetPassword(t *testing.T) { - tests := []struct { - name string - value string - want string - sf bool - }{ - {"normal password", "testpass", "testpass", false}, - {"empty password", "", "", false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c, err := NewClient(DefaultHost) - if err != nil { - t.Errorf("failed to create new client: %s", err) - return - } - c.SetPassword(tt.value) - if c.pass != tt.want { - t.Errorf("failed to set password. Expected %s, got: %s", tt.want, c.pass) - } - }) - } -} - -// TestSetSMTPAuth tests the SetSMTPAuth method for the Client object -func TestSetSMTPAuth(t *testing.T) { - tests := []struct { - name string - value SMTPAuthType - want string - sf bool - }{ - {"SMTPAuth: LOGIN", SMTPAuthLogin, "LOGIN", false}, - {"SMTPAuth: PLAIN", SMTPAuthPlain, "PLAIN", false}, - {"SMTPAuth: CRAM-MD5", SMTPAuthCramMD5, "CRAM-MD5", false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c, err := NewClient(DefaultHost) - if err != nil { - t.Errorf("failed to create new client: %s", err) - return - } - c.SetSMTPAuth(tt.value) - if string(c.smtpAuthType) != tt.want { - t.Errorf("failed to set SMTP auth type. Expected %s, got: %s", tt.want, string(c.smtpAuthType)) - } - }) - } -} - // TestWithDSN tests the WithDSN method for the Client object func TestWithDSN(t *testing.T) { c, err := NewClient(DefaultHost, WithDSN()) From ae7160ddba871dd682c37035501b338c8c78f95b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 16:46:18 +0200 Subject: [PATCH 019/188] 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. --- client_test.go | 61 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/client_test.go b/client_test.go index 0c762a8..208d68d 100644 --- a/client_test.go +++ b/client_test.go @@ -1380,25 +1380,50 @@ func TestClient_SetSMTPAuth(t *testing.T) { } func TestClient_SetSMTPAuthCustom(t *testing.T) { - t.Run("SetSMTPAuthCustom with PLAIN auth", func(t *testing.T) { - client, err := NewClient(DefaultHost) - if err != nil { - t.Fatalf("failed to create new client: %s", err) + t.Run("SetSMTPAuthCustom", func(t *testing.T) { + tests := []struct { + name string + authFunc smtp.Auth + want string + }{ + {"CRAM-MD5", smtp.CRAMMD5Auth("", ""), "*smtp.cramMD5Auth"}, + {"LOGIN", smtp.LoginAuth("", "", "", false), + "*smtp.loginAuth"}, + {"LOGIN-NOENC", smtp.LoginAuth("", "", "", true), + "*smtp.loginAuth"}, + {"PLAIN", smtp.PlainAuth("", "", "", "", false), + "*smtp.plainAuth"}, + {"PLAIN-NOENC", smtp.PlainAuth("", "", "", "", true), + "*smtp.plainAuth"}, + {"SCRAM-SHA-1", smtp.ScramSHA1Auth("", ""), "*smtp.scramAuth"}, + {"SCRAM-SHA-1-PLUS", smtp.ScramSHA1PlusAuth("", "", nil), + "*smtp.scramAuth"}, + {"SCRAM-SHA-256", smtp.ScramSHA256Auth("", ""), "*smtp.scramAuth"}, + {"SCRAM-SHA-256-PLUS", smtp.ScramSHA256PlusAuth("", "", nil), + "*smtp.scramAuth"}, + {"XOAUTH2", smtp.XOAuth2Auth("", ""), "*smtp.xoauth2Auth"}, } - client.SetSMTPAuthCustom( - smtp.PlainAuth("", "", "", "", false), - ) - if client.smtpAuth == nil { - t.Errorf("failed to set custom SMTP auth, expected auth method but got nil") - } - if client.smtpAuthType != SMTPAuthCustom { - t.Errorf("failed to set custom SMTP auth, want auth type: %s, got: %s", SMTPAuthCustom, - client.smtpAuthType) - } - authType := reflect.TypeOf(client.smtpAuth).String() - if authType != "*smtp.plainAuth" { - t.Errorf("failed to set custom SMTP auth, expected auth method type: %s, got: %s", - "*smtp.plainAuth", authType) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetSMTPAuthCustom(tt.authFunc) + if client.smtpAuth == nil { + t.Errorf("failed to set custom SMTP auth, expected auth method but got nil") + } + if client.smtpAuthType != SMTPAuthCustom { + t.Errorf("failed to set custom SMTP auth, want auth type: %s, got: %s", SMTPAuthCustom, + client.smtpAuthType) + } + authType := reflect.TypeOf(client.smtpAuth).String() + if authType != tt.want { + t.Errorf("failed to set custom SMTP auth, expected auth method type: %s, got: %s", + tt.want, authType) + } + + }) } }) } From 8a6cd2b448b4047532f6efaa4e24aacaf47dd097 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 17:34:23 +0200 Subject: [PATCH 020/188] 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. --- client_test.go | 433 ++++++++++++++++++++++++++++--------------------- 1 file changed, 250 insertions(+), 183 deletions(-) diff --git a/client_test.go b/client_test.go index 208d68d..4c7ceb3 100644 --- a/client_test.go +++ b/client_test.go @@ -9,6 +9,7 @@ import ( "bytes" "context" "crypto/tls" + "encoding/json" "errors" "fmt" "net" @@ -34,11 +35,30 @@ const ( TestServerAddr = "127.0.0.1" // TestServerPortBase is the base port for the simple SMTP test server TestServerPortBase = 2025 + // TestPasswordValid is the password that the test server accepts as valid for SMTP auth + TestPasswordValid = "V3ryS3cr3t+" + // TestUserValid is the username that the test server accepts as valid for SMTP auth + TestUserValid = "toni@tester.com" ) // PortAdder is an atomic counter used to increment port numbers for the test SMTP server instances. var PortAdder atomic.Int32 +// logLine represents a log entry with time, level, message, and direction details. +type logLine struct { + Time time.Time `json:"time"` + Level string `json:"level"` + Message string `json:"msg"` + Direction struct { + From string `json:"from"` + To string `json:"to"` + } `json:"direction"` +} + +type logData struct { + Lines []logLine `json:"lines"` +} + func TestNewClient(t *testing.T) { t.Run("create new Client", func(t *testing.T) { client, err := NewClient(DefaultHost) @@ -1428,192 +1448,210 @@ func TestClient_SetSMTPAuthCustom(t *testing.T) { }) } +func TestClient_SetLogAuthData(t *testing.T) { + t.Run("SetLogAuthData true", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetLogAuthData(true) + if !client.logAuthData { + t.Errorf("failed to set logAuthData, want: true, got: %t", client.logAuthData) + } + }) + t.Run("SetLogAuthData false", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetLogAuthData(false) + if client.logAuthData { + t.Errorf("failed to set logAuthData, want: false, got: %t", client.logAuthData) + } + }) + t.Run("SetLogAuthData override WithLogAuthData", func(t *testing.T) { + client, err := NewClient(DefaultHost, WithLogAuthData()) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.SetLogAuthData(false) + if client.logAuthData { + t.Errorf("failed to set logAuthData, want: false, got: %t", client.logAuthData) + } + }) +} + +func TestClient_Close(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, featureSet, false, serverPort); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + t.Run("connect and close the Client", func(t *testing.T) { + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to the test server: %s", err) + } + if !client.smtpClient.HasConnection() { + t.Fatalf("client has no connection") + } + if err = client.Close(); err != nil { + t.Errorf("failed to close the client: %s", err) + } + }) + t.Run("connect and double close the Client", func(t *testing.T) { + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to the test server: %s", err) + } + if !client.smtpClient.HasConnection() { + t.Fatalf("client has no connection") + } + if err = client.Close(); err != nil { + t.Errorf("failed to close the client: %s", err) + } + if err = client.Close(); err != nil { + t.Errorf("failed to close the client: %s", err) + } + }) +} + +func TestClient_DialWithContext(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, featureSet, false, serverPort); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + t.Run("connect and check connection", func(t *testing.T) { + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialWithContext(ctxDial); err != nil { + 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) + } + }) + if !client.smtpClient.HasConnection() { + t.Fatalf("client has no connection") + } + }) + t.Run("fail on base port use fallback", func(t *testing.T) { + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.port = 12345 + client.fallbackPort = serverPort + + if err = client.DialWithContext(ctxDial); err != nil { + 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) + } + }) + if !client.smtpClient.HasConnection() { + t.Fatalf("client has no connection") + } + }) + t.Run("fail on base port and fallback", func(t *testing.T) { + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.port = 12345 + client.fallbackPort = 12346 + + if err = client.DialWithContext(ctxDial); err == nil { + t.Fatalf("connection was supposed to fail, but didn't") + } + if client.smtpClient != nil { + t.Fatalf("client has connection") + } + }) + 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(TestUserValid), WithPassword(TestPasswordValid)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + + if err = client.DialWithContext(ctxDial); err != nil { + 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 AHRvbmlAdGVzdGVyLmNvbQBWM3J5UzNjcjN0Kw==") && + logline.Direction.From == "client" && logline.Direction.To == "server" { + authFound = true + } + } + if !authFound { + t.Errorf("logAuthData not working, no authentication info found in logs") + } + }) +} + /* -// TestWithDSN tests the WithDSN method for the Client object -func TestWithDSN(t *testing.T) { - c, err := NewClient(DefaultHost, WithDSN()) - if err != nil { - t.Errorf("failed to create new client: %s", err) - return - } - if !c.requestDSN { - t.Errorf("WithDSN failed. c.requestDSN expected to be: %t, got: %t", true, c.requestDSN) - } - if c.dsnReturnType != DSNMailReturnFull { - t.Errorf("WithDSN failed. c.dsnReturnType expected to be: %s, got: %s", DSNMailReturnFull, - c.dsnReturnType) - } - if c.dsnRcptNotifyType[0] != string(DSNRcptNotifyFailure) { - t.Errorf("WithDSN failed. c.dsnRcptNotifyType[0] expected to be: %s, got: %s", DSNRcptNotifyFailure, - c.dsnRcptNotifyType[0]) - } - if c.dsnRcptNotifyType[1] != string(DSNRcptNotifySuccess) { - t.Errorf("WithDSN failed. c.dsnRcptNotifyType[1] expected to be: %s, got: %s", DSNRcptNotifySuccess, - c.dsnRcptNotifyType[1]) - } -} - -// TestWithDSNMailReturnType tests the WithDSNMailReturnType method for the Client object -func TestWithDSNMailReturnType(t *testing.T) { - tests := []struct { - name string - value DSNMailReturnOption - want string - sf bool - }{ - {"WithDSNMailReturnType: FULL", DSNMailReturnFull, "FULL", false}, - {"WithDSNMailReturnType: HDRS", DSNMailReturnHeadersOnly, "HDRS", false}, - {"WithDSNMailReturnType: INVALID", "INVALID", "", true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c, err := NewClient(DefaultHost, WithDSNMailReturnType(tt.value)) - if err != nil && !tt.sf { - t.Errorf("failed to create new client: %s", err) - return - } - if string(c.dsnReturnType) != tt.want { - t.Errorf("WithDSNMailReturnType failed. Expected %s, got: %s", tt.want, string(c.dsnReturnType)) - } - }) - } -} - -// TestWithDSNRcptNotifyType tests the WithDSNRcptNotifyType method for the Client object -func TestWithDSNRcptNotifyType(t *testing.T) { - tests := []struct { - name string - value DSNRcptNotifyOption - want string - sf bool - }{ - {"WithDSNRcptNotifyType: NEVER", DSNRcptNotifyNever, "NEVER", false}, - {"WithDSNRcptNotifyType: SUCCESS", DSNRcptNotifySuccess, "SUCCESS", false}, - {"WithDSNRcptNotifyType: FAILURE", DSNRcptNotifyFailure, "FAILURE", false}, - {"WithDSNRcptNotifyType: DELAY", DSNRcptNotifyDelay, "DELAY", false}, - {"WithDSNRcptNotifyType: INVALID", "INVALID", "", true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c, err := NewClient(DefaultHost, WithDSNRcptNotifyType(tt.value)) - if err != nil && !tt.sf { - t.Errorf("failed to create new client: %s", err) - return - } - if len(c.dsnRcptNotifyType) <= 0 && !tt.sf { - t.Errorf("WithDSNRcptNotifyType failed. Expected at least one DSNRNType but got none") - } - if !tt.sf && c.dsnRcptNotifyType[0] != tt.want { - t.Errorf("WithDSNRcptNotifyType failed. Expected %s, got: %s", tt.want, c.dsnRcptNotifyType[0]) - } - }) - } -} - -// TestWithoutNoop tests the WithoutNoop method for the Client object -func TestWithoutNoop(t *testing.T) { - c, err := NewClient(DefaultHost, WithoutNoop()) - if err != nil { - t.Errorf("failed to create new client: %s", err) - return - } - if !c.noNoop { - t.Errorf("WithoutNoop failed. c.noNoop expected to be: %t, got: %t", true, c.noNoop) - } - - c, err = NewClient(DefaultHost) - if err != nil { - t.Errorf("failed to create new client: %s", err) - return - } - if c.noNoop { - t.Errorf("WithoutNoop failed. c.noNoop expected to be: %t, got: %t", false, c.noNoop) - } -} - -func TestClient_SetLogAuthData(t *testing.T) { - c, err := NewClient(DefaultHost, WithLogAuthData()) - if err != nil { - t.Errorf("failed to create new client: %s", err) - return - } - if !c.logAuthData { - t.Errorf("WithLogAuthData failed. c.logAuthData expected to be: %t, got: %t", true, - c.logAuthData) - } - c.SetLogAuthData(false) - if c.logAuthData { - t.Errorf("SetLogAuthData failed. c.logAuthData expected to be: %t, got: %t", false, - c.logAuthData) - } -} - -// TestSetSMTPAuthCustom tests the SetSMTPAuthCustom method for the Client object -func TestSetSMTPAuthCustom(t *testing.T) { - tests := []struct { - name string - value smtp.Auth - want string - sf bool - }{ - {"SMTPAuth: CRAM-MD5", smtp.CRAMMD5Auth("", ""), "CRAM-MD5", false}, - {"SMTPAuth: LOGIN", smtp.LoginAuth("", "", "", false), "LOGIN", false}, - {"SMTPAuth: PLAIN", smtp.PlainAuth("", "", "", "", false), "PLAIN", false}, - } - si := smtp.ServerInfo{TLS: true} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c, err := NewClient(DefaultHost) - if err != nil { - t.Errorf("failed to create new client: %s", err) - return - } - c.SetSMTPAuthCustom(tt.value) - if c.smtpAuth == nil { - t.Errorf("failed to set custom SMTP auth method. SMTP Auth method is empty") - } - if c.smtpAuthType != SMTPAuthCustom { - t.Errorf("failed to set custom SMTP auth method. SMTP Auth type is not custom: %s", - c.smtpAuthType) - } - p, _, err := c.smtpAuth.Start(&si) - if err != nil { - t.Errorf("SMTP Auth Start() method returned error: %s", err) - } - if p != tt.want { - t.Errorf("SMTP Auth Start() method is returned proto: %s, expected: %s", p, tt.want) - } - }) - } -} - -// TestClient_Close_double tests if a close on an already closed connection causes an error. -func TestClient_Close_double(t *testing.T) { - c, err := getTestConnection(true) - if err != nil { - t.Skipf("failed to create test client: %s. Skipping tests", err) - } - ctx := context.Background() - if err = c.DialWithContext(ctx); err != nil { - t.Errorf("failed to dial with context: %s", err) - return - } - if c.smtpClient == nil { - t.Errorf("DialWithContext didn't fail but no SMTP client found.") - return - } - if !c.smtpClient.HasConnection() { - t.Errorf("DialWithContext didn't fail but no connection found.") - } - if err = c.Close(); err != nil { - t.Errorf("failed to close connection: %s", err) - } - if err = c.Close(); err != nil { - t.Errorf("failed 2nd close connection: %s", err) - } -} // TestClient_DialWithContext tests the DialWithContext method for the Client object func TestClient_DialWithContext(t *testing.T) { @@ -3563,6 +3601,35 @@ func (f faker) SetReadDeadline(time.Time) error { return nil } func (f faker) SetWriteDeadline(time.Time) error { return nil } */ +// parseJSONLog parses a JSON encoded log from the provided buffer and returns a slice of logLine structs. +// In case of a decode error, it reports the error to the testing framework. +func parseJSONLog(t *testing.T, buf *bytes.Buffer) logData { + t.Helper() + + builder := strings.Builder{} + builder.WriteString(`{"lines":[`) + lines := strings.Split(buf.String(), "\n") + for i, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + builder.WriteString(line) + if i < len(lines)-2 { + builder.WriteString(`,`) + } + } + builder.WriteString("]}") + + var logdata logData + readBuffer := bytes.NewBuffer(nil) + readBuffer.WriteString(builder.String()) + if err := json.NewDecoder(readBuffer).Decode(&logdata); err != nil { + t.Errorf("failed to decode json log: %s", err) + } + return logdata +} + // simpleSMTPServer starts a simple TCP server that resonds to SMTP commands. // The provided featureSet represents in what the server responds to EHLO command // failReset controls if a RSET succeeds From 12695385e82981279f7d0bea8e7d88adcee11346 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 17:55:31 +0200 Subject: [PATCH 021/188] 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. --- client_test.go | 118 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 93 insertions(+), 25 deletions(-) diff --git a/client_test.go b/client_test.go index 4c7ceb3..e288262 100644 --- a/client_test.go +++ b/client_test.go @@ -1100,7 +1100,7 @@ func TestClient_SetDebugLog(t *testing.T) { serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil { + if err := simpleSMTPServer(ctx, &serverProps{FeatureSet: featureSet, ListenPort: serverPort}); err != nil { t.Errorf("failed to start test server: %s", err) return } @@ -1482,20 +1482,23 @@ func TestClient_SetLogAuthData(t *testing.T) { } func TestClient_Close(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, featureSet, false, serverPort); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 300) - t.Run("connect and close the Client", func(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, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -1514,6 +1517,22 @@ func TestClient_Close(t *testing.T) { } }) t.Run("connect and double close the Client", func(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, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -1534,6 +1553,41 @@ func TestClient_Close(t *testing.T) { t.Errorf("failed to close the client: %s", err) } }) + t.Run("test server will let close fail", func(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, &serverProps{ + FailOnQuit: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to the test server: %s", err) + } + if !client.smtpClient.HasConnection() { + t.Fatalf("client has no connection") + } + if err = client.Close(); err == nil { + t.Errorf("close was supposed to fail, but didn't") + } + }) } func TestClient_DialWithContext(t *testing.T) { @@ -1543,7 +1597,7 @@ func TestClient_DialWithContext(t *testing.T) { serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil { + if err := simpleSMTPServer(ctx, &serverProps{FeatureSet: featureSet, ListenPort: serverPort}); err != nil { t.Errorf("failed to start test server: %s", err) return } @@ -3630,11 +3684,21 @@ func parseJSONLog(t *testing.T, buf *bytes.Buffer) logData { return logdata } +type serverProps struct { + FailOnReset bool + FailOnQuit bool + FeatureSet string + ListenPort int +} + // simpleSMTPServer starts a simple TCP server that resonds to SMTP commands. // The provided featureSet represents in what the server responds to EHLO command // failReset controls if a RSET succeeds -func simpleSMTPServer(ctx context.Context, featureSet string, failReset bool, port int) error { - listener, err := net.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, port)) +func simpleSMTPServer(ctx context.Context, props *serverProps) error { + if props == nil { + return fmt.Errorf("no server properties provided") + } + listener, err := net.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, props.ListenPort)) if err != nil { return fmt.Errorf("unable to listen on %s://%s: %w", TestServerProto, TestServerAddr, err) } @@ -3659,12 +3723,12 @@ func simpleSMTPServer(ctx context.Context, featureSet string, failReset bool, po } return fmt.Errorf("unable to accept connection: %w", err) } - handleTestServerConnection(connection, featureSet, failReset) + handleTestServerConnection(connection, props) } } } -func handleTestServerConnection(connection net.Conn, featureSet string, failReset bool) { +func handleTestServerConnection(connection net.Conn, props *serverProps) { defer func() { if err := connection.Close(); err != nil { fmt.Printf("unable to close connection: %s\n", err) @@ -3698,7 +3762,7 @@ func handleTestServerConnection(connection net.Conn, featureSet string, failRese fmt.Printf("expected EHLO, got %q", data) return } - if err = writeLine("250-localhost.localdomain\r\n" + featureSet); err != nil { + if err = writeLine("250-localhost.localdomain\r\n" + props.FeatureSet); err != nil { return } @@ -3748,19 +3812,19 @@ func handleTestServerConnection(connection net.Conn, featureSet string, failRese var username, password string userResp := "VXNlcm5hbWU6" passResp := "UGFzc3dvcmQ6" - if strings.Contains(featureSet, "250-X-MOX-LOGIN") { + if strings.Contains(props.FeatureSet, "250-X-MOX-LOGIN") { userResp = "" passResp = "UGFzc3dvcmQ=" } - if strings.Contains(featureSet, "250-X-NULLBYTE-LOGIN") { + if strings.Contains(props.FeatureSet, "250-X-NULLBYTE-LOGIN") { userResp = "VXNlciBuYW1lAA==" passResp = "UGFzc3dvcmQA" } - if strings.Contains(featureSet, "250-X-BOGUS-LOGIN") { + if strings.Contains(props.FeatureSet, "250-X-BOGUS-LOGIN") { userResp = "Qm9ndXM=" passResp = "Qm9ndXM=" } - if strings.Contains(featureSet, "250-X-EMPTY-LOGIN") { + if strings.Contains(props.FeatureSet, "250-X-EMPTY-LOGIN") { userResp = "" passResp = "" } @@ -3816,12 +3880,16 @@ func handleTestServerConnection(connection net.Conn, featureSet string, failRese strings.EqualFold(data, "vrfy"): writeOK() case strings.EqualFold(data, "rset"): - if failReset { + if props.FailOnReset { _ = writeLine("500 5.1.2 Error: reset failed") break } writeOK() case strings.EqualFold(data, "quit"): + if props.FailOnQuit { + _ = writeLine("500 5.1.2 Error: quit failed") + break + } _ = writeLine("221 2.0.0 Bye") default: _ = writeLine("500 5.5.2 Error: bad syntax") From 21184e60b94178e3040b3fa07ea04b09e4b491d7 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 17:57:09 +0200 Subject: [PATCH 022/188] 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. --- b64linebreaker_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/b64linebreaker_test.go b/b64linebreaker_test.go index 9f88c7e..dc08471 100644 --- a/b64linebreaker_test.go +++ b/b64linebreaker_test.go @@ -21,7 +21,7 @@ var ( func TestBase64LineBreaker(t *testing.T) { t.Run("write, copy and close", func(t *testing.T) { - logoWriter := &bytes.Buffer{} + logoWriter := bytes.NewBuffer(nil) lineBreaker := &Base64LineBreaker{out: logoWriter} t.Cleanup(func() { if err := lineBreaker.Close(); err != nil { @@ -43,7 +43,7 @@ func TestBase64LineBreaker(t *testing.T) { } }) - logoWriter := &bytes.Buffer{} + logoWriter := bytes.NewBuffer(nil) lineBreaker := &Base64LineBreaker{out: logoWriter} t.Cleanup(func() { if err := lineBreaker.Close(); err != nil { @@ -237,9 +237,9 @@ func FuzzBase64LineBreaker(f *testing.F) { } f.Fuzz(func(t *testing.T, data []byte) { - var buffer bytes.Buffer + buffer := bytes.NewBuffer(nil) lineBreaker := &Base64LineBreaker{ - out: &buffer, + out: buffer, } base64Encoder := base64.NewEncoder(base64.StdEncoding, lineBreaker) From 0db1383940742f847d6c23b7c9fd04787cad059b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 17:58:08 +0200 Subject: [PATCH 023/188] 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. --- client_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client_test.go b/client_test.go index e288262..c009acf 100644 --- a/client_test.go +++ b/client_test.go @@ -1135,7 +1135,7 @@ func TestClient_SetDebugLog(t *testing.T) { if err != nil { t.Fatalf("failed to create new client: %s", err) } - buffer := &bytes.Buffer{} + buffer := bytes.NewBuffer(nil) client.SetLogger(log.New(buffer, log.LevelDebug)) client.SetDebugLog(true) @@ -1164,7 +1164,7 @@ func TestClient_SetDebugLog(t *testing.T) { if err != nil { t.Fatalf("failed to create new client: %s", err) } - buffer := &bytes.Buffer{} + buffer := bytes.NewBuffer(nil) client.SetLogger(log.New(buffer, log.LevelDebug)) client.SetDebugLog(false) @@ -1202,7 +1202,7 @@ func TestClient_SetDebugLog(t *testing.T) { } }) - buffer := &bytes.Buffer{} + buffer := bytes.NewBuffer(nil) client.SetLogger(log.New(buffer, log.LevelDebug)) client.SetDebugLog(true) if err = client.smtpClient.Noop(); err != nil { From d281f838d4fa8bc8838f5912f69b3c7e68cdf170 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 18:08:30 +0200 Subject: [PATCH 024/188] 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. --- client_test.go | 167 +++++++++++++++++-------------------------------- 1 file changed, 59 insertions(+), 108 deletions(-) diff --git a/client_test.go b/client_test.go index c009acf..41ed1e6 100644 --- a/client_test.go +++ b/client_test.go @@ -1647,6 +1647,23 @@ func TestClient_DialWithContext(t *testing.T) { t.Fatalf("client has no connection") } }) + t.Run("fail on invalid host", func(t *testing.T) { + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.host = "invalid.addr" + + if err = client.DialWithContext(ctxDial); err == nil { + t.Errorf("client with invalid host should fail") + } + if client.smtpClient != nil { + t.Errorf("client with invalid host should not have a smtp client") + } + }) t.Run("fail on base port and fallback", func(t *testing.T) { ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -1701,119 +1718,48 @@ func TestClient_DialWithContext(t *testing.T) { t.Errorf("logAuthData not working, no authentication info found in logs") } }) + t.Run("connect should fail on HELO", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + failServerPort := int(TestServerPortBase + PortAdder.Load()) + failFeatureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, &serverProps{ + FailOnHelo: true, + FeatureSet: failFeatureSet, + ListenPort: failServerPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(failServerPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialWithContext(ctxDial); err == nil { + t.Fatalf("connection was supposed to fail, but didn't") + } + if client.smtpClient == nil { + t.Fatalf("client has no smtp client") + } + if !client.smtpClient.HasConnection() { + t.Errorf("client has no connection") + } + }) + // TODO: Implement tests for TLS/SSL and custom DialCtxFunc } /* -// TestClient_DialWithContext tests the DialWithContext method for the Client object -func TestClient_DialWithContext(t *testing.T) { - c, err := getTestConnection(true) - if err != nil { - t.Skipf("failed to create test client: %s. Skipping tests", err) - } - ctx := context.Background() - if err = c.DialWithContext(ctx); err != nil { - t.Errorf("failed to dial with context: %s", err) - return - } - if c.smtpClient == nil { - t.Errorf("DialWithContext didn't fail but no SMTP client found.") - return - } - if !c.smtpClient.HasConnection() { - t.Errorf("DialWithContext didn't fail but no connection found.") - } - if err := c.Close(); err != nil { - t.Errorf("failed to close connection: %s", err) - } -} - -// TestClient_DialWithContext_Fallback tests the Client.DialWithContext method with the fallback -// port functionality -func TestClient_DialWithContext_Fallback(t *testing.T) { - c, err := getTestConnectionNoTestPort(true) - if err != nil { - t.Skipf("failed to create test client: %s. Skipping tests", err) - } - c.SetTLSPortPolicy(TLSOpportunistic) - c.port = 999 - ctx := context.Background() - if err = c.DialWithContext(ctx); err != nil { - t.Errorf("failed to dial with context: %s", err) - return - } - if c.smtpClient == nil { - t.Errorf("DialWithContext didn't fail but no SMTP client found.") - return - } - if !c.smtpClient.HasConnection() { - t.Errorf("DialWithContext didn't fail but no connection found.") - } - if err = c.Close(); err != nil { - t.Errorf("failed to close connection: %s", err) - } - - c.port = 999 - c.fallbackPort = 999 - if err = c.DialWithContext(ctx); err == nil { - t.Error("dial with context was supposed to fail, but didn't") - return - } -} - -// TestClient_DialWithContext_Debug tests the DialWithContext method for the Client object with debug -// logging enabled on the SMTP client -func TestClient_DialWithContext_Debug(t *testing.T) { - c, err := getTestClient(true) - if err != nil { - t.Skipf("failed to create test client: %s. Skipping tests", err) - } - ctx := context.Background() - if err = c.DialWithContext(ctx); err != nil { - t.Errorf("failed to dial with context: %s", err) - return - } - if c.smtpClient == nil { - t.Errorf("DialWithContext didn't fail but no SMTP client found.") - return - } - if !c.smtpClient.HasConnection() { - t.Errorf("DialWithContext didn't fail but no connection found.") - } - c.SetDebugLog(true) - if err = c.Close(); err != nil { - t.Errorf("failed to close connection: %s", err) - } -} - -// TestClient_DialWithContext_Debug_custom tests the DialWithContext method for the Client -// object with debug logging enabled and a custom logger on the SMTP client -func TestClient_DialWithContext_Debug_custom(t *testing.T) { - c, err := getTestClient(true) - if err != nil { - t.Skipf("failed to create test client: %s. Skipping tests", err) - } - ctx := context.Background() - if err = c.DialWithContext(ctx); err != nil { - t.Errorf("failed to dial with context: %s", err) - return - } - if c.smtpClient == nil { - t.Errorf("DialWithContext didn't fail but no SMTP client found.") - return - } - if !c.smtpClient.HasConnection() { - t.Errorf("DialWithContext didn't fail but no connection found.") - } - c.SetDebugLog(true) - c.SetLogger(log.New(os.Stderr, log.LevelDebug)) - if err = c.Close(); err != nil { - t.Errorf("failed to close connection: %s", err) - } -} - // TestClient_DialWithContextInvalidHost tests the DialWithContext method with intentional breaking // for the Client object func TestClient_DialWithContextInvalidHost(t *testing.T) { @@ -3685,8 +3631,9 @@ func parseJSONLog(t *testing.T, buf *bytes.Buffer) logData { } type serverProps struct { - FailOnReset bool + FailOnHelo bool FailOnQuit bool + FailOnReset bool FeatureSet string ListenPort int } @@ -3762,6 +3709,10 @@ func handleTestServerConnection(connection net.Conn, props *serverProps) { fmt.Printf("expected EHLO, got %q", data) return } + if props.FailOnHelo { + _ = writeLine("500 5.5.2 Error: fail on HELO") + return + } if err = writeLine("250-localhost.localdomain\r\n" + props.FeatureSet); err != nil { return } From 572751ac10678c1ce09b4a10e5f496dbcfe1181a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 18:20:52 +0200 Subject: [PATCH 025/188] 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. --- client_test.go | 69 +++++++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/client_test.go b/client_test.go index 41ed1e6..ff48c23 100644 --- a/client_test.go +++ b/client_test.go @@ -1664,6 +1664,31 @@ func TestClient_DialWithContext(t *testing.T) { t.Errorf("client with invalid host should not have a smtp client") } }) + t.Run("fail on invalid HELO", func(t *testing.T) { + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + client.helo = "" + + if err = client.DialWithContext(ctxDial); err == nil { + t.Errorf("client with invalid HELO should fail") + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("failed to close the client: %s", err) + } + }) + if client.smtpClient == nil { + t.Errorf("client with invalid HELO should still have a smtp client, got nil") + } + if !client.smtpClient.HasConnection() { + t.Errorf("client with invalid HELO should still have a smtp client connection, got nil") + } + }) t.Run("fail on base port and fallback", func(t *testing.T) { ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -1760,20 +1785,6 @@ func TestClient_DialWithContext(t *testing.T) { -// TestClient_DialWithContextInvalidHost tests the DialWithContext method with intentional breaking -// for the Client object -func TestClient_DialWithContextInvalidHost(t *testing.T) { - c, err := getTestConnection(true) - if err != nil { - t.Skipf("failed to create test client: %s. Skipping tests", err) - } - c.host = "invalid.addr" - ctx := context.Background() - if err = c.DialWithContext(ctx); err == nil { - t.Errorf("dial succeeded but was supposed to fail") - return - } -} // TestClient_DialWithContextInvalidHELO tests the DialWithContext method with intentional breaking // for the Client object @@ -3701,24 +3712,8 @@ func handleTestServerConnection(connection net.Conn, props *serverProps) { return } - data, err := reader.ReadString('\n') - if err != nil { - return - } - if !strings.HasPrefix(data, "EHLO") && !strings.HasPrefix(data, "HELO") { - fmt.Printf("expected EHLO, got %q", data) - return - } - if props.FailOnHelo { - _ = writeLine("500 5.5.2 Error: fail on HELO") - return - } - if err = writeLine("250-localhost.localdomain\r\n" + props.FeatureSet); err != nil { - return - } - for { - data, err = reader.ReadString('\n') + data, err := reader.ReadString('\n') if err != nil { break } @@ -3727,6 +3722,18 @@ func handleTestServerConnection(connection net.Conn, props *serverProps) { var datastring string data = strings.TrimSpace(data) switch { + case strings.HasPrefix(data, "EHLO"), strings.HasPrefix(data, "HELO"): + if len(strings.Split(data, " ")) != 2 { + _ = writeLine("501 Syntax: EHLO hostname") + return + } + if props.FailOnHelo { + _ = writeLine("500 5.5.2 Error: fail on HELO") + return + } + if err = writeLine("250-localhost.localdomain\r\n" + props.FeatureSet); err != nil { + return + } case strings.HasPrefix(data, "MAIL FROM:"): from := strings.TrimPrefix(data, "MAIL FROM:") from = strings.ReplaceAll(from, "BODY=8BITMIME", "") From 74fa3f6f62728cab1597bca5deea6ee758157e24 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 21:54:06 +0200 Subject: [PATCH 026/188] 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. --- smtp/smtp.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/smtp/smtp.go b/smtp/smtp.go index 77b5fb0..208677f 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -554,8 +554,19 @@ 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 + // If we did already tried to send a EHLO/HELO but it failed, we still need to be able to send + // a QUIT to close the connection. + // c.hello() will return the global helloErr of the Client, which will always be set if the HELO + // failed before. Therefore if we already sent a HELO and the error is not nil, we skip another + // EHLO/HELO try + c.mutex.RLock() + didHello := c.didHello + helloErr := c.helloError + c.mutex.RUnlock() + if !didHello || helloErr == nil { + if err := c.hello(); err != nil { + return err + } } _, _, err := c.cmd(221, "QUIT") if err != nil { From ea57644a8e17f449991f5f76ac15c5eb3380f44d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 22:20:32 +0200 Subject: [PATCH 027/188] 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. --- client_test.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client_test.go b/client_test.go index ff48c23..fa16e4b 100644 --- a/client_test.go +++ b/client_test.go @@ -1668,7 +1668,7 @@ func TestClient_DialWithContext(t *testing.T) { ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) - client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS), WithDebugLog()) if err != nil { t.Fatalf("failed to create new client: %s", err) } @@ -3725,14 +3725,14 @@ func handleTestServerConnection(connection net.Conn, props *serverProps) { case strings.HasPrefix(data, "EHLO"), strings.HasPrefix(data, "HELO"): if len(strings.Split(data, " ")) != 2 { _ = writeLine("501 Syntax: EHLO hostname") - return + break } if props.FailOnHelo { _ = writeLine("500 5.5.2 Error: fail on HELO") - return + break } if err = writeLine("250-localhost.localdomain\r\n" + props.FeatureSet); err != nil { - return + break } case strings.HasPrefix(data, "MAIL FROM:"): from := strings.TrimPrefix(data, "MAIL FROM:") @@ -3849,6 +3849,7 @@ func handleTestServerConnection(connection net.Conn, props *serverProps) { break } _ = writeLine("221 2.0.0 Bye") + return default: _ = writeLine("500 5.5.2 Error: bad syntax") } From 563ccbab4a9ac13d37b64128ebe4c5ec2b6ae7ab Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 22:26:08 +0200 Subject: [PATCH 028/188] 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. --- smtp/smtp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smtp/smtp.go b/smtp/smtp.go index 208677f..63504fe 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -554,7 +554,7 @@ func (c *Client) Noop() error { // Quit sends the QUIT command and closes the connection to the server. func (c *Client) Quit() error { - // If we did already tried to send a EHLO/HELO but it failed, we still need to be able to send + // If we already tried to send a EHLO/HELO but it failed, we still need to be able to send // a QUIT to close the connection. // c.hello() will return the global helloErr of the Client, which will always be set if the HELO // failed before. Therefore if we already sent a HELO and the error is not nil, we skip another From c63b8b124e0534f4cae94af5eb8d83dee1cd08fb Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 23:33:33 +0200 Subject: [PATCH 029/188] 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. --- client_test.go | 214 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 198 insertions(+), 16 deletions(-) diff --git a/client_test.go b/client_test.go index fa16e4b..288156c 100644 --- a/client_test.go +++ b/client_test.go @@ -34,7 +34,7 @@ const ( // TestServerAddr is the address the simple SMTP test server listens on TestServerAddr = "127.0.0.1" // TestServerPortBase is the base port for the simple SMTP test server - TestServerPortBase = 2025 + TestServerPortBase = 12025 // TestPasswordValid is the password that the test server accepts as valid for SMTP auth TestPasswordValid = "V3ryS3cr3t+" // TestUserValid is the username that the test server accepts as valid for SMTP auth @@ -44,6 +44,44 @@ const ( // PortAdder is an atomic counter used to increment port numbers for the test SMTP server instances. var PortAdder atomic.Int32 +// localhostCert is a PEM-encoded TLS cert generated from src/crypto/tls: +// +// go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com \ +// --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h +var localhostCert = []byte(` +-----BEGIN CERTIFICATE----- +MIICFDCCAX2gAwIBAgIRAK0xjnaPuNDSreeXb+z+0u4wDQYJKoZIhvcNAQELBQAw +EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2 +MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw +gYkCgYEA0nFbQQuOWsjbGtejcpWz153OlziZM4bVjJ9jYruNw5n2Ry6uYQAffhqa +JOInCmmcVe2siJglsyH9aRh6vKiobBbIUXXUU1ABd56ebAzlt0LobLlx7pZEMy30 +LqIi9E6zmL3YvdGzpYlkFRnRrqwEtWYbGBf3znO250S56CCWH2UCAwEAAaNoMGYw +DgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQF +MAMBAf8wLgYDVR0RBCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAAAAAAAAAA +AAAAAAEwDQYJKoZIhvcNAQELBQADgYEAbZtDS2dVuBYvb+MnolWnCNqvw1w5Gtgi +NmvQQPOMgM3m+oQSCPRTNGSg25e1Qbo7bgQDv8ZTnq8FgOJ/rbkyERw2JckkHpD4 +n4qcK27WkEDBtQFlPihIM8hLIuzWoi/9wygiElTy/tVL3y7fGCvY2/k1KBthtZGF +tN8URjVmyEo= +-----END CERTIFICATE-----`) + +// localhostKey is the private key for localhostCert. +var localhostKey = []byte(testingKey(` +-----BEGIN RSA TESTING KEY----- +MIICXgIBAAKBgQDScVtBC45ayNsa16NylbPXnc6XOJkzhtWMn2Niu43DmfZHLq5h +AB9+Gpok4icKaZxV7ayImCWzIf1pGHq8qKhsFshRddRTUAF3np5sDOW3QuhsuXHu +lkQzLfQuoiL0TrOYvdi90bOliWQVGdGurAS1ZhsYF/fOc7bnRLnoIJYfZQIDAQAB +AoGBAMst7OgpKyFV6c3JwyI/jWqxDySL3caU+RuTTBaodKAUx2ZEmNJIlx9eudLA +kucHvoxsM/eRxlxkhdFxdBcwU6J+zqooTnhu/FE3jhrT1lPrbhfGhyKnUrB0KKMM +VY3IQZyiehpxaeXAwoAou6TbWoTpl9t8ImAqAMY8hlULCUqlAkEA+9+Ry5FSYK/m +542LujIcCaIGoG1/Te6Sxr3hsPagKC2rH20rDLqXwEedSFOpSS0vpzlPAzy/6Rbb +PHTJUhNdwwJBANXkA+TkMdbJI5do9/mn//U0LfrCR9NkcoYohxfKz8JuhgRQxzF2 +6jpo3q7CdTuuRixLWVfeJzcrAyNrVcBq87cCQFkTCtOMNC7fZnCTPUv+9q1tcJyB +vNjJu3yvoEZeIeuzouX9TJE21/33FaeDdsXbRhQEj23cqR38qFHsF1qAYNMCQQDP +QXLEiJoClkR2orAmqjPLVhR3t2oB3INcnEjLNSq8LHyQEfXyaFfu4U9l5+fRPL2i +jiC0k/9L5dHUsF0XZothAkEA23ddgRs+Id/HxtojqqUT27B8MT/IGNrYsp4DvS/c +qgkeluku4GjxRlDMBuXk94xOBEinUs+p/hwP1Alll80Tpg== +-----END RSA TESTING KEY-----`)) + // logLine represents a log entry with time, level, message, and direction details. type logLine struct { Time time.Time `json:"time"` @@ -1668,7 +1706,7 @@ func TestClient_DialWithContext(t *testing.T) { ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) - client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS), WithDebugLog()) + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) if err != nil { t.Fatalf("failed to create new client: %s", err) } @@ -1744,13 +1782,13 @@ func TestClient_DialWithContext(t *testing.T) { } }) t.Run("connect should fail on HELO", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctxFail, cancelFail := context.WithCancel(context.Background()) + defer cancelFail() PortAdder.Add(1) failServerPort := int(TestServerPortBase + PortAdder.Load()) failFeatureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctx, &serverProps{ + if err := simpleSMTPServer(ctxFail, &serverProps{ FailOnHelo: true, FeatureSet: failFeatureSet, ListenPort: failServerPort, @@ -1778,7 +1816,113 @@ func TestClient_DialWithContext(t *testing.T) { t.Errorf("client has no connection") } }) - // TODO: Implement tests for TLS/SSL and custom DialCtxFunc + t.Run("connect with failing auth", func(t *testing.T) { + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS), + WithSMTPAuth(SMTPAuthPlain), WithUsername("invalid"), WithPassword("invalid")) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialWithContext(ctxDial); err == nil { + t.Fatalf("connection was supposed to fail, but didn't") + } + }) + t.Run("connect with STARTTLS", func(t *testing.T) { + ctxTLS, cancelTLS := context.WithCancel(context.Background()) + defer cancelTLS() + PortAdder.Add(1) + tlsServerPort := int(TestServerPortBase + PortAdder.Load()) + tlsFeatureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250-STARTTLS\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctxTLS, &serverProps{ + FeatureSet: tlsFeatureSet, + ListenPort: tlsServerPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + tlsConfig := &tls.Config{InsecureSkipVerify: true} + client, err := NewClient(DefaultHost, WithPort(tlsServerPort), WithTLSPolicy(TLSMandatory), + WithTLSConfig(tlsConfig), WithSMTPAuth(SMTPAuthPlain), WithUsername(TestUserValid), + WithPassword(TestPasswordValid)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to the test server: %s", err) + } + }) + t.Run("want STARTTLS, but server does not support it", func(t *testing.T) { + ctxTLS, cancelTLS := context.WithCancel(context.Background()) + defer cancelTLS() + PortAdder.Add(1) + tlsServerPort := int(TestServerPortBase + PortAdder.Load()) + tlsFeatureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctxTLS, &serverProps{ + FeatureSet: tlsFeatureSet, + ListenPort: tlsServerPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + tlsConfig := &tls.Config{InsecureSkipVerify: true} + client, err := NewClient(DefaultHost, WithPort(tlsServerPort), WithTLSPolicy(TLSMandatory), + WithTLSConfig(tlsConfig), WithSMTPAuth(SMTPAuthPlain), WithUsername(TestUserValid), + WithPassword(TestPasswordValid)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialWithContext(ctxDial); err == nil { + t.Fatalf("connection was supposed to fail, but didn't") + } + }) + t.Run("connect with SSL", func(t *testing.T) { + ctxSSL, cancelSSL := context.WithCancel(context.Background()) + defer cancelSSL() + PortAdder.Add(1) + sslServerPort := int(TestServerPortBase + PortAdder.Load()) + sslFeatureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctxSSL, &serverProps{ + SSLListener: true, + FeatureSet: sslFeatureSet, + ListenPort: sslServerPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + tlsConfig := &tls.Config{InsecureSkipVerify: true} + client, err := NewClient(DefaultHost, WithPort(sslServerPort), WithSSL(), + WithTLSConfig(tlsConfig), WithSMTPAuth(SMTPAuthPlain), WithUsername(TestUserValid), + WithPassword(TestPasswordValid)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to the test server: %s", err) + } + if err := client.Close(); err != nil { + t.Fatalf("failed to close client: %s", err) + } + }) } /* @@ -3641,12 +3785,19 @@ func parseJSONLog(t *testing.T, buf *bytes.Buffer) logData { return logdata } +// testingKey replaces the substring "TESTING KEY" with "PRIVATE KEY" in the given string s. +func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") } + +// serverProps represents the configuration properties for the SMTP server. type serverProps struct { - FailOnHelo bool - FailOnQuit bool - FailOnReset bool - FeatureSet string - ListenPort int + FailOnHelo bool + FailOnQuit bool + FailOnReset bool + FailOnSTARTTLS bool + FeatureSet string + ListenPort int + SSLListener bool + IsTLS bool } // simpleSMTPServer starts a simple TCP server that resonds to SMTP commands. @@ -3656,9 +3807,23 @@ func simpleSMTPServer(ctx context.Context, props *serverProps) error { if props == nil { return fmt.Errorf("no server properties provided") } - listener, err := net.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, props.ListenPort)) + + var listener net.Listener + var err error + if props.SSLListener { + keypair, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + return fmt.Errorf("failed to read TLS keypair: %s", err) + } + tlsConfig := &tls.Config{Certificates: []tls.Certificate{keypair}} + listener, err = tls.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, props.ListenPort), + tlsConfig) + } else { + listener, err = net.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, props.ListenPort)) + } if err != nil { - return fmt.Errorf("unable to listen on %s://%s: %w", TestServerProto, TestServerAddr, err) + return fmt.Errorf("unable to listen on %s://%s: %w (SSL: %t)", TestServerProto, TestServerAddr, err, + props.SSLListener) } defer func() { @@ -3707,9 +3872,11 @@ func handleTestServerConnection(connection net.Conn, props *serverProps) { _ = writeLine("250 2.0.0 OK") } - if err := writeLine("220 go-mail test server ready ESMTP"); err != nil { - fmt.Printf("unable to write to client: %s\n", err) - return + if !props.IsTLS { + if err := writeLine("220 go-mail test server ready ESMTP"); err != nil { + fmt.Printf("unable to write to client: %s\n", err) + return + } } for { @@ -3850,6 +4017,21 @@ func handleTestServerConnection(connection net.Conn, props *serverProps) { } _ = writeLine("221 2.0.0 Bye") return + case strings.EqualFold(data, "starttls"): + if props.FailOnSTARTTLS { + _ = writeLine("500 5.1.2 Error: starttls failed") + break + } + keypair, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + _ = writeLine("500 5.1.2 Error: starttls failed - " + err.Error()) + break + } + _ = writeLine("220 Ready to start TLS") + tlsConfig := &tls.Config{Certificates: []tls.Certificate{keypair}} + connection = tls.Server(connection, tlsConfig) + props.IsTLS = true + handleTestServerConnection(connection, props) default: _ = writeLine("500 5.5.2 Error: bad syntax") } From cf1246d9eafb860c592b2a46dc4a9b41b57e3ada Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 23 Oct 2024 23:34:13 +0200 Subject: [PATCH 030/188] 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. --- client_test.go | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/client_test.go b/client_test.go index 288156c..4725310 100644 --- a/client_test.go +++ b/client_test.go @@ -1930,38 +1930,6 @@ func TestClient_DialWithContext(t *testing.T) { -// TestClient_DialWithContextInvalidHELO tests the DialWithContext method with intentional breaking -// for the Client object -func TestClient_DialWithContextInvalidHELO(t *testing.T) { - c, err := getTestConnection(true) - if err != nil { - t.Skipf("failed to create test client: %s. Skipping tests", err) - } - c.helo = "" - ctx := context.Background() - if err = c.DialWithContext(ctx); err == nil { - t.Errorf("dial succeeded but was supposed to fail") - return - } -} - -// TestClient_DialWithContextInvalidAuth tests the DialWithContext method with intentional breaking -// for the Client object -func TestClient_DialWithContextInvalidAuth(t *testing.T) { - c, err := getTestConnection(true) - if err != nil { - t.Skipf("failed to create test client: %s. Skipping tests", err) - } - c.user = "invalid" - c.pass = "invalid" - c.SetSMTPAuthCustom(smtp.LoginAuth("invalid", "invalid", "invalid", false)) - ctx := context.Background() - if err = c.DialWithContext(ctx); err == nil { - t.Errorf("dial succeeded but was supposed to fail") - return - } -} - // TestClient_checkConn tests the checkConn method with intentional breaking for the Client object func TestClient_checkConn(t *testing.T) { c, err := getTestConnection(true) From 2710250baa317097d1732b160f4c134104946fd8 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 24 Oct 2024 00:11:58 +0200 Subject: [PATCH 031/188] 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. --- client_test.go | 105 ++++++++++++++++++++++++------------------------- 1 file changed, 52 insertions(+), 53 deletions(-) diff --git a/client_test.go b/client_test.go index 4725310..c08ae0a 100644 --- a/client_test.go +++ b/client_test.go @@ -1138,7 +1138,7 @@ func TestClient_SetDebugLog(t *testing.T) { serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctx, &serverProps{FeatureSet: featureSet, ListenPort: serverPort}); err != nil { + if err := simpleSMTPServer(ctx, t, &serverProps{FeatureSet: featureSet, ListenPort: serverPort}); err != nil { t.Errorf("failed to start test server: %s", err) return } @@ -1527,7 +1527,7 @@ func TestClient_Close(t *testing.T) { serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctx, &serverProps{ + if err := simpleSMTPServer(ctx, t, &serverProps{ FeatureSet: featureSet, ListenPort: serverPort, }); err != nil { @@ -1561,7 +1561,7 @@ func TestClient_Close(t *testing.T) { serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctx, &serverProps{ + if err := simpleSMTPServer(ctx, t, &serverProps{ FeatureSet: featureSet, ListenPort: serverPort, }); err != nil { @@ -1598,7 +1598,7 @@ func TestClient_Close(t *testing.T) { serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctx, &serverProps{ + if err := simpleSMTPServer(ctx, t, &serverProps{ FailOnQuit: true, FeatureSet: featureSet, ListenPort: serverPort, @@ -1635,7 +1635,7 @@ func TestClient_DialWithContext(t *testing.T) { serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctx, &serverProps{FeatureSet: featureSet, ListenPort: serverPort}); err != nil { + if err := simpleSMTPServer(ctx, t, &serverProps{FeatureSet: featureSet, ListenPort: serverPort}); err != nil { t.Errorf("failed to start test server: %s", err) return } @@ -1788,7 +1788,7 @@ func TestClient_DialWithContext(t *testing.T) { failServerPort := int(TestServerPortBase + PortAdder.Load()) failFeatureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctxFail, &serverProps{ + if err := simpleSMTPServer(ctxFail, t, &serverProps{ FailOnHelo: true, FeatureSet: failFeatureSet, ListenPort: failServerPort, @@ -1836,7 +1836,7 @@ func TestClient_DialWithContext(t *testing.T) { tlsServerPort := int(TestServerPortBase + PortAdder.Load()) tlsFeatureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250-STARTTLS\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctxTLS, &serverProps{ + if err := simpleSMTPServer(ctxTLS, t, &serverProps{ FeatureSet: tlsFeatureSet, ListenPort: tlsServerPort, }); err != nil { @@ -1866,7 +1866,7 @@ func TestClient_DialWithContext(t *testing.T) { tlsServerPort := int(TestServerPortBase + PortAdder.Load()) tlsFeatureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctxTLS, &serverProps{ + if err := simpleSMTPServer(ctxTLS, t, &serverProps{ FeatureSet: tlsFeatureSet, ListenPort: tlsServerPort, }); err != nil { @@ -1896,7 +1896,7 @@ func TestClient_DialWithContext(t *testing.T) { sslServerPort := int(TestServerPortBase + PortAdder.Load()) sslFeatureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { - if err := simpleSMTPServer(ctxSSL, &serverProps{ + if err := simpleSMTPServer(ctxSSL, t, &serverProps{ SSLListener: true, FeatureSet: sslFeatureSet, ListenPort: sslServerPort, @@ -3771,7 +3771,8 @@ type serverProps struct { // simpleSMTPServer starts a simple TCP server that resonds to SMTP commands. // The provided featureSet represents in what the server responds to EHLO command // failReset controls if a RSET succeeds -func simpleSMTPServer(ctx context.Context, props *serverProps) error { +func simpleSMTPServer(ctx context.Context, t *testing.T, props *serverProps) error { + t.Helper() if props == nil { return fmt.Errorf("no server properties provided") } @@ -3796,8 +3797,7 @@ func simpleSMTPServer(ctx context.Context, props *serverProps) error { defer func() { if err := listener.Close(); err != nil { - fmt.Printf("unable to close listener: %s\n", err) - os.Exit(1) + t.Logf("failed to close listener: %s", err) } }() @@ -3814,37 +3814,37 @@ func simpleSMTPServer(ctx context.Context, props *serverProps) error { } return fmt.Errorf("unable to accept connection: %w", err) } - handleTestServerConnection(connection, props) + handleTestServerConnection(connection, t, props) } } } -func handleTestServerConnection(connection net.Conn, props *serverProps) { - defer func() { +func handleTestServerConnection(connection net.Conn, t *testing.T, props *serverProps) { + t.Helper() + t.Cleanup(func() { if err := connection.Close(); err != nil { - fmt.Printf("unable to close connection: %s\n", err) + t.Logf("failed to close connection: %s", err) } - }() + }) reader := bufio.NewReader(connection) writer := bufio.NewWriter(connection) - writeLine := func(data string) error { + writeLine := func(data string) { _, err := writer.WriteString(data + "\r\n") if err != nil { - return fmt.Errorf("unable to write line: %w", err) + t.Logf("failed to write line: %s", err) + } + if err = writer.Flush(); err != nil { + t.Logf("failed to flush writer: %s", err) } - return writer.Flush() } writeOK := func() { - _ = writeLine("250 2.0.0 OK") + writeLine("250 2.0.0 OK") } if !props.IsTLS { - if err := writeLine("220 go-mail test server ready ESMTP"); err != nil { - fmt.Printf("unable to write to client: %s\n", err) - return - } + writeLine("220 go-mail test server ready ESMTP") } for { @@ -3859,23 +3859,22 @@ func handleTestServerConnection(connection net.Conn, props *serverProps) { switch { case strings.HasPrefix(data, "EHLO"), strings.HasPrefix(data, "HELO"): if len(strings.Split(data, " ")) != 2 { - _ = writeLine("501 Syntax: EHLO hostname") + writeLine("501 Syntax: EHLO hostname") break } if props.FailOnHelo { - _ = writeLine("500 5.5.2 Error: fail on HELO") - break - } - if err = writeLine("250-localhost.localdomain\r\n" + props.FeatureSet); err != nil { + writeLine("500 5.5.2 Error: fail on HELO") break } + writeLine("250-localhost.localdomain\r\n" + props.FeatureSet) + break case strings.HasPrefix(data, "MAIL FROM:"): from := strings.TrimPrefix(data, "MAIL FROM:") from = strings.ReplaceAll(from, "BODY=8BITMIME", "") from = strings.ReplaceAll(from, "SMTPUTF8", "") from = strings.TrimSpace(from) if !strings.EqualFold(from, "") { - _ = writeLine(fmt.Sprintf("503 5.1.2 Invalid from: %s", from)) + writeLine(fmt.Sprintf("503 5.1.2 Invalid from: %s", from)) break } writeOK() @@ -3883,24 +3882,24 @@ func handleTestServerConnection(connection net.Conn, props *serverProps) { to := strings.TrimPrefix(data, "RCPT TO:") to = strings.TrimSpace(to) if !strings.EqualFold(to, "") { - _ = writeLine(fmt.Sprintf("500 5.1.2 Invalid to: %s", to)) + writeLine(fmt.Sprintf("500 5.1.2 Invalid to: %s", to)) break } writeOK() case strings.HasPrefix(data, "AUTH XOAUTH2"): auth := strings.TrimPrefix(data, "AUTH XOAUTH2 ") if !strings.EqualFold(auth, "dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=") { - _ = writeLine("535 5.7.8 Error: authentication failed") + writeLine("535 5.7.8 Error: authentication failed") break } - _ = writeLine("235 2.7.0 Authentication successful") + writeLine("235 2.7.0 Authentication successful") case strings.HasPrefix(data, "AUTH PLAIN"): auth := strings.TrimPrefix(data, "AUTH PLAIN ") if !strings.EqualFold(auth, "AHRvbmlAdGVzdGVyLmNvbQBWM3J5UzNjcjN0Kw==") { - _ = writeLine("535 5.7.8 Error: authentication failed") + writeLine("535 5.7.8 Error: authentication failed") break } - _ = writeLine("235 2.7.0 Authentication successful") + writeLine("235 2.7.0 Authentication successful") case strings.HasPrefix(data, "AUTH LOGIN"): var username, password string userResp := "VXNlcm5hbWU6" @@ -3921,7 +3920,7 @@ func handleTestServerConnection(connection net.Conn, props *serverProps) { userResp = "" passResp = "" } - _ = writeLine("334 " + userResp) + writeLine("334 " + userResp) ddata, derr := reader.ReadString('\n') if derr != nil { @@ -3930,7 +3929,7 @@ func handleTestServerConnection(connection net.Conn, props *serverProps) { } ddata = strings.TrimSpace(ddata) username = ddata - _ = writeLine("334 " + passResp) + writeLine("334 " + passResp) ddata, derr = reader.ReadString('\n') if derr != nil { @@ -3942,29 +3941,29 @@ func handleTestServerConnection(connection net.Conn, props *serverProps) { if !strings.EqualFold(username, "dG9uaUB0ZXN0ZXIuY29t") || !strings.EqualFold(password, "VjNyeVMzY3IzdCs=") { - _ = writeLine("535 5.7.8 Error: authentication failed") + writeLine("535 5.7.8 Error: authentication failed") break } - _ = writeLine("235 2.7.0 Authentication successful") + writeLine("235 2.7.0 Authentication successful") case strings.EqualFold(data, "DATA"): - _ = writeLine("354 End data with .") + writeLine("354 End data with .") for { ddata, derr := reader.ReadString('\n') if derr != nil { - fmt.Printf("failed to read DATA data from connection: %s\n", derr) + t.Logf("failed to read data from connection: %s", derr) break } ddata = strings.TrimSpace(ddata) if strings.EqualFold(ddata, "DATA write should fail") { - _ = writeLine("500 5.0.0 Error during DATA transmission") + writeLine("500 5.0.0 Error during DATA transmission") break } if ddata == "." { if strings.Contains(datastring, "DATA close should fail") { - _ = writeLine("500 5.0.0 Error during DATA closing") + writeLine("500 5.0.0 Error during DATA closing") break } - _ = writeLine("250 2.0.0 Ok: queued as 1234567890") + writeLine("250 2.0.0 Ok: queued as 1234567890") break } datastring += ddata + "\n" @@ -3974,34 +3973,34 @@ func handleTestServerConnection(connection net.Conn, props *serverProps) { writeOK() case strings.EqualFold(data, "rset"): if props.FailOnReset { - _ = writeLine("500 5.1.2 Error: reset failed") + writeLine("500 5.1.2 Error: reset failed") break } writeOK() case strings.EqualFold(data, "quit"): if props.FailOnQuit { - _ = writeLine("500 5.1.2 Error: quit failed") + writeLine("500 5.1.2 Error: quit failed") break } - _ = writeLine("221 2.0.0 Bye") + writeLine("221 2.0.0 Bye") return case strings.EqualFold(data, "starttls"): if props.FailOnSTARTTLS { - _ = writeLine("500 5.1.2 Error: starttls failed") + writeLine("500 5.1.2 Error: starttls failed") break } keypair, err := tls.X509KeyPair(localhostCert, localhostKey) if err != nil { - _ = writeLine("500 5.1.2 Error: starttls failed - " + err.Error()) + writeLine("500 5.1.2 Error: starttls failed - " + err.Error()) break } - _ = writeLine("220 Ready to start TLS") + writeLine("220 Ready to start TLS") tlsConfig := &tls.Config{Certificates: []tls.Certificate{keypair}} connection = tls.Server(connection, tlsConfig) props.IsTLS = true - handleTestServerConnection(connection, props) + handleTestServerConnection(connection, t, props) default: - _ = writeLine("500 5.5.2 Error: bad syntax") + writeLine("500 5.5.2 Error: bad syntax") } } } From 06f6fd369207dba587ee53a834734aa819d353d7 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 24 Oct 2024 00:22:33 +0200 Subject: [PATCH 032/188] 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. --- client_test.go | 104 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/client_test.go b/client_test.go index c08ae0a..7535177 100644 --- a/client_test.go +++ b/client_test.go @@ -1925,6 +1925,110 @@ func TestClient_DialWithContext(t *testing.T) { }) } +func TestClient_Reset(t *testing.T) { + t.Run("reset client", func(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 * 300) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithTLSPolicy(NoTLS), WithPort(serverPort)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to the test server: %s", err) + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Fatalf("failed to close client: %s", err) + } + }) + if err = client.Reset(); err != nil { + t.Errorf("failed to reset client: %s", err) + } + }) + t.Run("reset should fail on disconnected client", func(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 * 300) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithTLSPolicy(NoTLS), WithPort(serverPort)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to the test server: %s", err) + } + if err = client.Close(); err != nil { + t.Fatalf("failed to close client: %s", err) + } + if err = client.Reset(); err == nil { + t.Errorf("reset on disconnected client should fail") + } + }) + t.Run("reset with server failure", func(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{ + FailOnReset: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithTLSPolicy(NoTLS), WithPort(serverPort)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to the test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Fatalf("failed to close client: %s", err) + } + }) + if err = client.Reset(); err == nil { + t.Errorf("reset on disconnected client should fail") + } + }) +} + /* From 84ca70083a1fc51c5e87df54baa6490b3c61b436 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 24 Oct 2024 00:30:57 +0200 Subject: [PATCH 033/188] 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. --- client_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/client_test.go b/client_test.go index 7535177..07e3f4b 100644 --- a/client_test.go +++ b/client_test.go @@ -39,6 +39,10 @@ const ( TestPasswordValid = "V3ryS3cr3t+" // TestUserValid is the username that the test server accepts as valid for SMTP auth TestUserValid = "toni@tester.com" + // TestSenderValid is a test sender email address considered valid for sending test emails. + TestSenderValid = "valid-from@domain.tld" + // TestRcptValid is a test recipient email address considered valid for sending test emails. + TestRcptValid = "valid-to@domain.tld" ) // PortAdder is an atomic counter used to increment port numbers for the test SMTP server instances. @@ -2029,6 +2033,44 @@ func TestClient_Reset(t *testing.T) { }) } +func TestClient_DialAndSend(t *testing.T) { + t.Run("dial and send", func(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 * 300) + + message := NewMsg() + if err := message.From(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") + + client, err := NewClient(DefaultHost, WithTLSPolicy(NoTLS), WithPort(serverPort)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialAndSend(message); err != nil { + t.Fatalf("failed to dial and send: %s", err) + } + }) +} + /* From 28dc629674d86a0618064503dbbdd02c0baffac8 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 24 Oct 2024 00:50:16 +0200 Subject: [PATCH 034/188] 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. --- client_test.go | 150 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 131 insertions(+), 19 deletions(-) diff --git a/client_test.go b/client_test.go index 07e3f4b..2007541 100644 --- a/client_test.go +++ b/client_test.go @@ -2033,8 +2033,18 @@ func TestClient_Reset(t *testing.T) { }) } -func TestClient_DialAndSend(t *testing.T) { - t.Run("dial and send", func(t *testing.T) { +func TestClient_DialAndSendWithContext(t *testing.T) { + message := NewMsg() + if err := message.From(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") + + t.Run("DialAndSend", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() PortAdder.Add(1) @@ -2051,16 +2061,6 @@ func TestClient_DialAndSend(t *testing.T) { }() time.Sleep(time.Millisecond * 300) - message := NewMsg() - if err := message.From(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") - client, err := NewClient(DefaultHost, WithTLSPolicy(NoTLS), WithPort(serverPort)) if err != nil { t.Fatalf("failed to create new client: %s", err) @@ -2069,6 +2069,121 @@ func TestClient_DialAndSend(t *testing.T) { t.Fatalf("failed to dial and send: %s", err) } }) + t.Run("DialAndSendWithContext", func(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 * 300) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithTLSPolicy(NoTLS), WithPort(serverPort)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialAndSendWithContext(ctxDial, message); err != nil { + t.Fatalf("failed to dial and send: %s", err) + } + }) + t.Run("DialAndSendWithContext fail on dial", func(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{ + FailOnHelo: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithTLSPolicy(NoTLS), WithPort(serverPort)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialAndSendWithContext(ctxDial, message); err == nil { + t.Errorf("client was supposed to fail on dial") + } + }) + t.Run("DialAndSendWithContext fail on close", func(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{ + FailOnQuit: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithTLSPolicy(NoTLS), WithPort(serverPort)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialAndSendWithContext(ctxDial, message); err == nil { + t.Errorf("client was supposed to fail on dial") + } + }) + t.Run("DialAndSendWithContext fail on send", func(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{ + FailOnData: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithTLSPolicy(NoTLS), WithPort(serverPort)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialAndSendWithContext(ctxDial, message); err == nil { + t.Errorf("client was supposed to fail on dial") + } + }) } /* @@ -3904,6 +4019,7 @@ func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", " // serverProps represents the configuration properties for the SMTP server. type serverProps struct { + FailOnData bool FailOnHelo bool FailOnQuit bool FailOnReset bool @@ -3982,7 +4098,7 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server t.Logf("failed to write line: %s", err) } if err = writer.Flush(); err != nil { - t.Logf("failed to flush writer: %s", err) + t.Logf("failed to flush line: %s", err) } } writeOK := func() { @@ -4100,13 +4216,9 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server break } ddata = strings.TrimSpace(ddata) - if strings.EqualFold(ddata, "DATA write should fail") { - writeLine("500 5.0.0 Error during DATA transmission") - break - } if ddata == "." { - if strings.Contains(datastring, "DATA close should fail") { - writeLine("500 5.0.0 Error during DATA closing") + if props.FailOnData { + writeLine("500 5.0.0 Error during DATA transmission") break } writeLine("250 2.0.0 Ok: queued as 1234567890") From e442419c18748530c5f9e1371239cc3271f02a03 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 24 Oct 2024 09:19:08 +0200 Subject: [PATCH 035/188] 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. --- client.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/client.go b/client.go index f65766d..af56afc 100644 --- a/client.go +++ b/client.go @@ -1360,9 +1360,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") From 2a2176d7009846167c7e13aca746687c28153e53 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 24 Oct 2024 09:59:31 +0200 Subject: [PATCH 036/188] 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. --- client_test.go | 320 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 207 insertions(+), 113 deletions(-) diff --git a/client_test.go b/client_test.go index 2007541..e396176 100644 --- a/client_test.go +++ b/client_test.go @@ -1821,10 +1821,27 @@ func TestClient_DialWithContext(t *testing.T) { } }) t.Run("connect with failing auth", func(t *testing.T) { + ctxAuth, cancelAuth := context.WithCancel(context.Background()) + defer cancelAuth() + PortAdder.Add(1) + authServerPort := int(TestServerPortBase + PortAdder.Load()) + authFeatureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250-STARTTLS\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctxAuth, t, &serverProps{ + FailOnAuth: true, + FeatureSet: authFeatureSet, + ListenPort: authServerPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) - client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS), + client, err := NewClient(DefaultHost, WithPort(authServerPort), WithTLSPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), WithUsername("invalid"), WithPassword("invalid")) if err != nil { t.Fatalf("failed to create new client: %s", err) @@ -1863,6 +1880,67 @@ func TestClient_DialWithContext(t *testing.T) { t.Fatalf("failed to connect to the test server: %s", err) } }) + t.Run("connect with STARTTLS Opportunisticly", func(t *testing.T) { + ctxTLS, cancelTLS := context.WithCancel(context.Background()) + defer cancelTLS() + PortAdder.Add(1) + tlsServerPort := int(TestServerPortBase + PortAdder.Load()) + tlsFeatureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250-STARTTLS\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctxTLS, t, &serverProps{ + FeatureSet: tlsFeatureSet, + ListenPort: tlsServerPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + tlsConfig := &tls.Config{InsecureSkipVerify: true} + client, err := NewClient(DefaultHost, WithPort(tlsServerPort), WithTLSPolicy(TLSOpportunistic), + WithTLSConfig(tlsConfig), WithSMTPAuth(SMTPAuthPlain), WithUsername(TestUserValid), + WithPassword(TestPasswordValid)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to the test server: %s", err) + } + }) + t.Run("connect with STARTTLS but fail", func(t *testing.T) { + ctxTLS, cancelTLS := context.WithCancel(context.Background()) + defer cancelTLS() + PortAdder.Add(1) + tlsServerPort := int(TestServerPortBase + PortAdder.Load()) + tlsFeatureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250-STARTTLS\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctxTLS, t, &serverProps{ + FailOnSTARTTLS: true, + FeatureSet: tlsFeatureSet, + ListenPort: tlsServerPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + tlsConfig := &tls.Config{InsecureSkipVerify: true} + client, err := NewClient(DefaultHost, WithPort(tlsServerPort), WithTLSPolicy(TLSMandatory), + WithTLSConfig(tlsConfig), WithSMTPAuth(SMTPAuthPlain), WithUsername(TestUserValid), + WithPassword(TestPasswordValid)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialWithContext(ctxDial); err == nil { + t.Fatalf("connection was supposed to fail, but didn't") + } + }) t.Run("want STARTTLS, but server does not support it", func(t *testing.T) { ctxTLS, cancelTLS := context.WithCancel(context.Background()) defer cancelTLS() @@ -2186,6 +2264,123 @@ func TestClient_DialAndSendWithContext(t *testing.T) { }) } +func TestClient_auth(t *testing.T) { + tests := []struct { + name string + authType SMTPAuthType + }{ + {"CRAM-MD5", SMTPAuthCramMD5}, + {"LOGIN", SMTPAuthLogin}, + {"LOGIN-NOENC", SMTPAuthLoginNoEnc}, + {"PLAIN", SMTPAuthPlain}, + {"PLAIN-NOENC", SMTPAuthPlainNoEnc}, + {"SCRAM-SHA-1", SMTPAuthSCRAMSHA1}, + {"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS}, + {"SCRAM-SHA-256", SMTPAuthSCRAMSHA256}, + {"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS}, + {"XOAUTH2", SMTPAuthXOAUTH2}, + } + + tlsConfig := tls.Config{InsecureSkipVerify: true} + for _, tt := range tests { + t.Run(tt.name+" should succeed", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH " + tt.name + "\r\n250-STARTTLS\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 * 300) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithTLSPolicy(NoTLS), WithPort(serverPort), + WithTLSPolicy(TLSMandatory), WithSMTPAuth(tt.authType), WithTLSConfig(&tlsConfig), + WithUsername("test"), WithPassword("password")) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to test service: %s", err) + } + if err := client.Close(); err != nil { + t.Errorf("failed to close client connection: %s", err) + } + + }) + t.Run(tt.name+" should fail", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH " + tt.name + "\r\n250-STARTTLS\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnAuth: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 300) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithTLSPolicy(NoTLS), WithPort(serverPort), + WithTLSPolicy(TLSMandatory), WithSMTPAuth(tt.authType), WithTLSConfig(&tlsConfig), + WithUsername("test"), WithPassword("password")) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialWithContext(ctxDial); err == nil { + t.Fatalf("client should have failed to connect") + } + }) + t.Run(tt.name+" should fail as unspported", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH UNKNOWN\r\n250-8BITMIME\r\n250-STARTTLS\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 * 300) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithTLSPolicy(NoTLS), WithPort(serverPort), + WithTLSPolicy(TLSMandatory), WithSMTPAuth(tt.authType), WithTLSConfig(&tlsConfig), + WithUsername("test"), WithPassword("password")) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialWithContext(ctxDial); err == nil { + t.Fatalf("client should have failed to connect") + } + }) + } +} + /* @@ -2202,55 +2397,6 @@ func TestClient_checkConn(t *testing.T) { } } -// TestClient_DiealWithContextOptions tests the DialWithContext method plus different options -// for the Client object -func TestClient_DialWithContextOptions(t *testing.T) { - tests := []struct { - name string - wantssl bool - wanttls TLSPolicy - sf bool - }{ - {"Want SSL (should fail)", true, NoTLS, true}, - {"Want Mandatory TLS", false, TLSMandatory, false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c, err := getTestConnection(true) - if err != nil { - t.Skipf("failed to create test client: %s. Skipping tests", err) - } - if tt.wantssl { - c.SetSSL(true) - } - if tt.wanttls != NoTLS { - c.SetTLSPolicy(tt.wanttls) - } - - ctx := context.Background() - if err = c.DialWithContext(ctx); err != nil && !tt.sf { - t.Errorf("failed to dial with context: %s", err) - return - } - if !tt.sf { - if c.smtpClient == nil && !tt.sf { - t.Errorf("DialWithContext didn't fail but no SMTP client found.") - return - } - if !c.smtpClient.HasConnection() && !tt.sf { - t.Errorf("DialWithContext didn't fail but no connection found.") - return - } - if err = c.Reset(); err != nil { - t.Errorf("failed to reset connection: %s", err) - } - if err = c.Close(); err != nil { - t.Errorf("failed to close connection: %s", err) - } - } - }) - } -} // TestClient_DialWithContextOptionDialContextFunc tests the DialWithContext method plus // use dialContextFunc option for the Client object @@ -4019,6 +4165,7 @@ func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", " // serverProps represents the configuration properties for the SMTP server. type serverProps struct { + FailOnAuth bool FailOnData bool FailOnHelo bool FailOnQuit bool @@ -4083,11 +4230,13 @@ func simpleSMTPServer(ctx context.Context, t *testing.T, props *serverProps) err func handleTestServerConnection(connection net.Conn, t *testing.T, props *serverProps) { t.Helper() - t.Cleanup(func() { - if err := connection.Close(); err != nil { - t.Logf("failed to close connection: %s", err) - } - }) + if !props.IsTLS { + t.Cleanup(func() { + if err := connection.Close(); err != nil { + t.Logf("failed to close connection: %s", err) + } + }) + } reader := bufio.NewReader(connection) writer := bufio.NewWriter(connection) @@ -4097,9 +4246,7 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server if err != nil { t.Logf("failed to write line: %s", err) } - if err = writer.Flush(); err != nil { - t.Logf("failed to flush line: %s", err) - } + _ = writer.Flush() } writeOK := func() { writeLine("250 2.0.0 OK") @@ -4148,61 +4295,8 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server break } writeOK() - case strings.HasPrefix(data, "AUTH XOAUTH2"): - auth := strings.TrimPrefix(data, "AUTH XOAUTH2 ") - if !strings.EqualFold(auth, "dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=") { - writeLine("535 5.7.8 Error: authentication failed") - break - } - writeLine("235 2.7.0 Authentication successful") - case strings.HasPrefix(data, "AUTH PLAIN"): - auth := strings.TrimPrefix(data, "AUTH PLAIN ") - if !strings.EqualFold(auth, "AHRvbmlAdGVzdGVyLmNvbQBWM3J5UzNjcjN0Kw==") { - writeLine("535 5.7.8 Error: authentication failed") - break - } - writeLine("235 2.7.0 Authentication successful") - case strings.HasPrefix(data, "AUTH LOGIN"): - var username, password string - userResp := "VXNlcm5hbWU6" - passResp := "UGFzc3dvcmQ6" - if strings.Contains(props.FeatureSet, "250-X-MOX-LOGIN") { - userResp = "" - passResp = "UGFzc3dvcmQ=" - } - if strings.Contains(props.FeatureSet, "250-X-NULLBYTE-LOGIN") { - userResp = "VXNlciBuYW1lAA==" - passResp = "UGFzc3dvcmQA" - } - if strings.Contains(props.FeatureSet, "250-X-BOGUS-LOGIN") { - userResp = "Qm9ndXM=" - passResp = "Qm9ndXM=" - } - if strings.Contains(props.FeatureSet, "250-X-EMPTY-LOGIN") { - userResp = "" - passResp = "" - } - writeLine("334 " + userResp) - - ddata, derr := reader.ReadString('\n') - if derr != nil { - fmt.Printf("failed to read username data from connection: %s\n", derr) - break - } - ddata = strings.TrimSpace(ddata) - username = ddata - writeLine("334 " + passResp) - - ddata, derr = reader.ReadString('\n') - if derr != nil { - fmt.Printf("failed to read password data from connection: %s\n", derr) - break - } - ddata = strings.TrimSpace(ddata) - password = ddata - - if !strings.EqualFold(username, "dG9uaUB0ZXN0ZXIuY29t") || - !strings.EqualFold(password, "VjNyeVMzY3IzdCs=") { + case strings.HasPrefix(data, "AUTH"): + if props.FailOnAuth { writeLine("535 5.7.8 Error: authentication failed") break } From 040289cea4e695be277898389be5168f38194eb3 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 24 Oct 2024 10:12:43 +0200 Subject: [PATCH 037/188] 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. --- client_test.go | 128 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 109 insertions(+), 19 deletions(-) diff --git a/client_test.go b/client_test.go index e396176..9d8bcd9 100644 --- a/client_test.go +++ b/client_test.go @@ -35,10 +35,6 @@ const ( TestServerAddr = "127.0.0.1" // TestServerPortBase is the base port for the simple SMTP test server TestServerPortBase = 12025 - // TestPasswordValid is the password that the test server accepts as valid for SMTP auth - TestPasswordValid = "V3ryS3cr3t+" - // TestUserValid is the username that the test server accepts as valid for SMTP auth - TestUserValid = "toni@tester.com" // TestSenderValid is a test sender email address considered valid for sending test emails. TestSenderValid = "valid-from@domain.tld" // TestRcptValid is a test recipient email address considered valid for sending test emails. @@ -1756,7 +1752,7 @@ func TestClient_DialWithContext(t *testing.T) { logBuffer := bytes.NewBuffer(nil) client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS), WithDebugLog(), WithLogAuthData(), WithLogger(log.NewJSON(logBuffer, log.LevelDebug)), - WithSMTPAuth(SMTPAuthPlain), WithUsername(TestUserValid), WithPassword(TestPasswordValid)) + WithSMTPAuth(SMTPAuthPlain), WithUsername("test"), WithPassword("password")) if err != nil { t.Fatalf("failed to create new client: %s", err) } @@ -1776,7 +1772,7 @@ func TestClient_DialWithContext(t *testing.T) { } authFound := false for _, logline := range logs.Lines { - if strings.EqualFold(logline.Message, "AUTH PLAIN AHRvbmlAdGVzdGVyLmNvbQBWM3J5UzNjcjN0Kw==") && + if strings.EqualFold(logline.Message, "AUTH PLAIN AHRlc3QAcGFzc3dvcmQ=") && logline.Direction.From == "client" && logline.Direction.To == "server" { authFound = true } @@ -1871,8 +1867,8 @@ func TestClient_DialWithContext(t *testing.T) { tlsConfig := &tls.Config{InsecureSkipVerify: true} client, err := NewClient(DefaultHost, WithPort(tlsServerPort), WithTLSPolicy(TLSMandatory), - WithTLSConfig(tlsConfig), WithSMTPAuth(SMTPAuthPlain), WithUsername(TestUserValid), - WithPassword(TestPasswordValid)) + WithTLSConfig(tlsConfig), WithSMTPAuth(SMTPAuthPlain), WithUsername("test"), + WithPassword("password")) if err != nil { t.Fatalf("failed to create new client: %s", err) } @@ -1901,8 +1897,8 @@ func TestClient_DialWithContext(t *testing.T) { tlsConfig := &tls.Config{InsecureSkipVerify: true} client, err := NewClient(DefaultHost, WithPort(tlsServerPort), WithTLSPolicy(TLSOpportunistic), - WithTLSConfig(tlsConfig), WithSMTPAuth(SMTPAuthPlain), WithUsername(TestUserValid), - WithPassword(TestPasswordValid)) + WithTLSConfig(tlsConfig), WithSMTPAuth(SMTPAuthPlain), WithUsername("test"), + WithPassword("password")) if err != nil { t.Fatalf("failed to create new client: %s", err) } @@ -1932,8 +1928,8 @@ func TestClient_DialWithContext(t *testing.T) { tlsConfig := &tls.Config{InsecureSkipVerify: true} client, err := NewClient(DefaultHost, WithPort(tlsServerPort), WithTLSPolicy(TLSMandatory), - WithTLSConfig(tlsConfig), WithSMTPAuth(SMTPAuthPlain), WithUsername(TestUserValid), - WithPassword(TestPasswordValid)) + WithTLSConfig(tlsConfig), WithSMTPAuth(SMTPAuthPlain), WithUsername("test"), + WithPassword("password")) if err != nil { t.Fatalf("failed to create new client: %s", err) } @@ -1962,8 +1958,8 @@ func TestClient_DialWithContext(t *testing.T) { tlsConfig := &tls.Config{InsecureSkipVerify: true} client, err := NewClient(DefaultHost, WithPort(tlsServerPort), WithTLSPolicy(TLSMandatory), - WithTLSConfig(tlsConfig), WithSMTPAuth(SMTPAuthPlain), WithUsername(TestUserValid), - WithPassword(TestPasswordValid)) + WithTLSConfig(tlsConfig), WithSMTPAuth(SMTPAuthPlain), WithUsername("test"), + WithPassword("password")) if err != nil { t.Fatalf("failed to create new client: %s", err) } @@ -1993,8 +1989,8 @@ func TestClient_DialWithContext(t *testing.T) { tlsConfig := &tls.Config{InsecureSkipVerify: true} client, err := NewClient(DefaultHost, WithPort(sslServerPort), WithSSL(), - WithTLSConfig(tlsConfig), WithSMTPAuth(SMTPAuthPlain), WithUsername(TestUserValid), - WithPassword(TestPasswordValid)) + WithTLSConfig(tlsConfig), WithSMTPAuth(SMTPAuthPlain), WithUsername("test"), + WithPassword("password")) if err != nil { t.Fatalf("failed to create new client: %s", err) } @@ -2303,7 +2299,7 @@ func TestClient_auth(t *testing.T) { ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) - client, err := NewClient(DefaultHost, WithTLSPolicy(NoTLS), WithPort(serverPort), + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(TLSMandatory), WithSMTPAuth(tt.authType), WithTLSConfig(&tlsConfig), WithUsername("test"), WithPassword("password")) if err != nil { @@ -2338,7 +2334,7 @@ func TestClient_auth(t *testing.T) { ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) - client, err := NewClient(DefaultHost, WithTLSPolicy(NoTLS), WithPort(serverPort), + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(TLSMandatory), WithSMTPAuth(tt.authType), WithTLSConfig(&tlsConfig), WithUsername("test"), WithPassword("password")) if err != nil { @@ -2368,7 +2364,7 @@ func TestClient_auth(t *testing.T) { ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) - client, err := NewClient(DefaultHost, WithTLSPolicy(NoTLS), WithPort(serverPort), + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(TLSMandatory), WithSMTPAuth(tt.authType), WithTLSConfig(&tlsConfig), WithUsername("test"), WithPassword("password")) if err != nil { @@ -2379,6 +2375,100 @@ func TestClient_auth(t *testing.T) { } }) } + t.Run("auth is not supported at all", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-STARTTLS\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 * 300) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), + WithTLSPolicy(TLSMandatory), WithSMTPAuth(SMTPAuthPlain), WithTLSConfig(&tlsConfig), + WithUsername("test"), WithPassword("password")) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialWithContext(ctxDial); err == nil { + t.Fatalf("client should have failed to connect") + } + }) + t.Run("SCRAM-X-PLUS on non TLS connection should fail", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH SCRAM-SHA-256-PLUS\r\n250-8BITMIME\r\n250-STARTTLS\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 * 300) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithTLSPolicy(NoTLS), WithPort(serverPort), + WithSMTPAuth(SMTPAuthSCRAMSHA256PLUS), WithTLSConfig(&tlsConfig), + WithUsername("test"), WithPassword("password")) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialWithContext(ctxDial); err == nil { + t.Fatalf("client should have failed to connect") + } + }) + t.Run("unknown auth type should fail", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH UNKNOWN\r\n250-8BITMIME\r\n250-STARTTLS\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 * 300) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), + WithTLSPolicy(TLSMandatory), WithSMTPAuth("UNKNOWN"), WithTLSConfig(&tlsConfig), + WithUsername("test"), WithPassword("password")) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialWithContext(ctxDial); err == nil { + t.Fatalf("client should have failed to connect") + } + }) +} + +func TestClient_Send(t *testing.T) { + t.Run("send email", func(t *testing.T) {}) } /* From 5e3ebcc1a6f5f882af6eb5d074e35147f0895f6a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 24 Oct 2024 10:12:57 +0200 Subject: [PATCH 038/188] 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. --- client.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client.go b/client.go index af56afc..55280b6 100644 --- a/client.go +++ b/client.go @@ -1094,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 { From 4a8ac76636d2ae894f6bffbc7c89b83c36caa90f Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 24 Oct 2024 10:44:40 +0200 Subject: [PATCH 039/188] 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. --- client.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client.go b/client.go index 55280b6..3e26e9f 100644 --- a/client.go +++ b/client.go @@ -1298,6 +1298,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 } From 3bf1992cab19015c598b3998bb2fcd5bf9d1a195 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 24 Oct 2024 10:45:05 +0200 Subject: [PATCH 040/188] 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. --- client_test.go | 411 +++++++++++++++---------------------------------- 1 file changed, 122 insertions(+), 289 deletions(-) diff --git a/client_test.go b/client_test.go index 9d8bcd9..e912730 100644 --- a/client_test.go +++ b/client_test.go @@ -16,6 +16,7 @@ import ( "os" "reflect" "strings" + "sync" "sync/atomic" "testing" "time" @@ -2108,16 +2109,7 @@ func TestClient_Reset(t *testing.T) { } func TestClient_DialAndSendWithContext(t *testing.T) { - message := NewMsg() - if err := message.From(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") - + message := testMessage(t) t.Run("DialAndSend", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -2468,7 +2460,111 @@ func TestClient_auth(t *testing.T) { } func TestClient_Send(t *testing.T) { - t.Run("send email", func(t *testing.T) {}) + message := testMessage(t) + t.Run("connect and send email", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-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 * 300) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.Send(message); err != nil { + t.Errorf("failed to send email: %s", err) + } + }) + t.Run("send with no connection should fail", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err = client.Send(message); err == nil { + t.Errorf("client should have failed to send email with no connection") + } + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Fatalf("expected SendError, got %T", err) + } + if sendErr.Reason != ErrConnCheck { + t.Errorf("expected ErrConnCheck, got %s", sendErr.Reason) + } + }) + t.Run("concurrent sending on a single client connection", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-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 * 300) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + + var messages []*Msg + for i := 0; i < 50; i++ { + curMessage := testMessage(t) + curMessage.SetMessageIDWithValue("this.is.a.message.id") + messages = append(messages, curMessage) + } + + wg := sync.WaitGroup{} + for id, curMessage := range messages { + wg.Add(1) + go func(curMsg *Msg, curID int) { + defer wg.Done() + if goroutineErr := client.Send(curMsg); err != nil { + t.Errorf("failed to send message with ID %d: %s", curID, goroutineErr) + } + }(curMessage, id) + } + wg.Wait() + }) +} + +// TestClient_onlinetests will perform some additional tests on a actual live mail server. These tests are only +// meant for the CI/CD pipeline and are usually skipped. They can be activated by setting PERFORM_ONLINE_TEST=true +// in the ENV. The normal test suite should provide all the tests needed to cover the full functionality. +func TestClient_onlinetests(t *testing.T) { + if os.Getenv("PERFORM_ONLINE_TEST") != "true" { + t.Skip(`"PERFORM_ONLINE_TEST" env variable is not set to "true". Skipping online tests.`) + } } /* @@ -2487,129 +2583,6 @@ func TestClient_checkConn(t *testing.T) { } } - -// TestClient_DialWithContextOptionDialContextFunc tests the DialWithContext method plus -// use dialContextFunc option for the Client object -func TestClient_DialWithContextOptionDialContextFunc(t *testing.T) { - c, err := getTestConnection(true) - if err != nil { - t.Skipf("failed to create test client: %s. Skipping tests", err) - } - - called := false - c.dialContextFunc = func(ctx context.Context, network, address string) (net.Conn, error) { - called = true - return (&net.Dialer{}).DialContext(ctx, network, address) - } - - ctx := context.Background() - if err := c.DialWithContext(ctx); err != nil { - t.Errorf("failed to dial with context: %s", err) - return - } - - if called == false { - t.Errorf("dialContextFunc supposed to be called but not called") - } -} - -// TestClient_DialSendClose tests the Dial(), Send() and Close() method of Client -func TestClient_DialSendClose(t *testing.T) { - if os.Getenv("TEST_ALLOW_SEND") == "" { - t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test") - } - m := NewMsg() - _ = m.FromFormat("go-mail Test Mailer", os.Getenv("TEST_FROM")) - _ = m.To(TestRcpt) - m.Subject(fmt.Sprintf("This is a test mail from go-mail/v%s", VERSION)) - m.SetBulk() - m.SetDate() - m.SetMessageID() - m.SetBodyString(TypeTextPlain, "This is a test mail from the go-mail library") - - c, err := getTestConnection(true) - if err != nil { - t.Skipf("failed to create test client: %s. Skipping tests", err) - } - - ctx, cfn := context.WithTimeout(context.Background(), time.Second*10) - defer cfn() - if err := c.DialWithContext(ctx); err != nil { - t.Errorf("Dial() failed: %s", err) - } - if err := c.Send(m); err != nil { - t.Errorf("Send() failed: %s", err) - } - if err := c.Close(); err != nil { - t.Errorf("Close() failed: %s", err) - } - if !m.IsDelivered() { - t.Errorf("message should be delivered but is indicated no to") - } -} - -// TestClient_DialAndSendWithContext tests the DialAndSendWithContext() method of Client -func TestClient_DialAndSendWithContext(t *testing.T) { - if os.Getenv("TEST_ALLOW_SEND") == "" { - t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test") - } - m := NewMsg() - _ = m.FromFormat("go-mail Test Mailer", os.Getenv("TEST_FROM")) - _ = m.To(TestRcpt) - m.Subject(fmt.Sprintf("This is a test mail from go-mail/v%s", VERSION)) - m.SetBulk() - m.SetDate() - m.SetMessageID() - m.SetBodyString(TypeTextPlain, "This is a test mail from the go-mail library") - - tests := []struct { - name string - to time.Duration - sf bool - }{ - {"Timeout: 100s", time.Second * 100, false}, - {"Timeout: 100ms", time.Millisecond * 100, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c, err := getTestConnection(true) - if err != nil { - t.Skipf("failed to create test client: %s. Skipping tests", err) - } - - ctx, cfn := context.WithTimeout(context.Background(), tt.to) - defer cfn() - if err := c.DialAndSendWithContext(ctx, m); err != nil && !tt.sf { - t.Errorf("DialAndSendWithContext() failed: %s", err) - } - }) - } -} - -// TestClient_DialAndSend tests the DialAndSend() method of Client -func TestClient_DialAndSend(t *testing.T) { - if os.Getenv("TEST_ALLOW_SEND") == "" { - t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test") - } - m := NewMsg() - _ = m.FromFormat("go-mail Test Mailer", os.Getenv("TEST_FROM")) - _ = m.To(TestRcpt) - m.Subject(fmt.Sprintf("This is a test mail from go-mail/v%s", VERSION)) - m.SetBulk() - m.SetDate() - m.SetMessageID() - m.SetBodyString(TypeTextPlain, "This is a test mail from the go-mail library") - - c, err := getTestConnection(true) - if err != nil { - t.Skipf("failed to create test client: %s. Skipping tests", err) - } - - if err := c.DialAndSend(m); err != nil { - t.Errorf("DialAndSend() failed: %s", err) - } -} - // TestClient_DialAndSendWithDSN tests the DialAndSend() method of Client with DSN enabled func TestClient_DialAndSendWithDSN(t *testing.T) { if os.Getenv("TEST_ALLOW_SEND") == "" { @@ -2749,49 +2722,6 @@ func TestClient_DialSendCloseBrokenWithDSN(t *testing.T) { } } -// TestClient_Send_withBrokenRecipient tests the Send() method of Client with a broken and a working recipient -func TestClient_Send_withBrokenRecipient(t *testing.T) { - if os.Getenv("TEST_ALLOW_SEND") == "" { - t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test") - } - var msgs []*Msg - rcpts := []string{"invalid@domain.tld", TestRcpt, "invalid@address.invalid"} - for _, rcpt := range rcpts { - m := NewMsg() - _ = m.FromFormat("go-mail Test Mailer", os.Getenv("TEST_FROM")) - _ = m.To(rcpt) - m.Subject(fmt.Sprintf("This is a test mail from go-mail/v%s", VERSION)) - m.SetBulk() - m.SetDate() - m.SetMessageID() - m.SetBodyString(TypeTextPlain, "This is a test mail from the go-mail library") - msgs = append(msgs, m) - } - - c, err := getTestConnection(true) - if err != nil { - t.Skipf("failed to create test client: %s. Skipping tests", err) - } - - ctx, cfn := context.WithTimeout(context.Background(), DefaultTimeout) - defer cfn() - if err := c.DialWithContext(ctx); err != nil { - t.Errorf("failed to dial to sending server: %s", err) - } - if err := c.Send(msgs...); err != nil { - if !strings.Contains(err.Error(), "invalid@domain.tld") || - !strings.Contains(err.Error(), "invalid@address.invalid") { - t.Errorf("sending mails to invalid addresses was supposed to fail but didn't") - } - if strings.Contains(err.Error(), TestRcpt) { - t.Errorf("sending mail to valid addresses failed: %s", err) - } - } - if err := c.Close(); err != nil { - t.Errorf("failed to close client connection: %s", err) - } -} - func TestClient_DialWithContext_switchAuth(t *testing.T) { if os.Getenv("TEST_ALLOW_SEND") == "" { t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test") @@ -2867,44 +2797,6 @@ func TestClient_DialWithContext_switchAuth(t *testing.T) { } } -// TestClient_auth tests the Dial(), Send() and Close() method of Client with broken settings -func TestClient_auth(t *testing.T) { - tests := []struct { - name string - auth SMTPAuthType - sf bool - }{ - {"SMTP AUTH: PLAIN", SMTPAuthPlain, false}, - {"SMTP AUTH: LOGIN", SMTPAuthLogin, false}, - {"SMTP AUTH: CRAM-MD5", SMTPAuthCramMD5, true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c, err := getTestConnection(false) - if err != nil { - t.Skipf("failed to create test client: %s. Skipping tests", err) - } - - ctx, cfn := context.WithTimeout(context.Background(), time.Second*5) - defer cfn() - if err := c.DialWithContext(ctx); err != nil { - t.Errorf("auth() failed: could not Dial() => %s", err) - return - } - c.SetSMTPAuth(tt.auth) - c.SetUsername(os.Getenv("TEST_SMTPAUTH_USER")) - c.SetPassword(os.Getenv("TEST_SMTPAUTH_PASS")) - if err := c.auth(); err != nil && !tt.sf { - t.Errorf("auth() failed: %s", err) - } - if err := c.Close(); err != nil { - t.Errorf("auth() failed: could not Close() => %s", err) - } - }) - } -} - // TestClient_Send_MsgSendError tests the Client.Send method with a broken recipient and verifies // that the SendError type works properly func TestClient_Send_MsgSendError(t *testing.T) { @@ -3127,80 +3019,6 @@ func TestClient_SendErrorMailFrom(t *testing.T) { } } -func TestClient_SendErrorMailFromReset(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - serverPort := TestServerPortBase + 3 - featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" - go func() { - if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 300) - - message := NewMsg() - if err := message.From("invalid-from@domain.tld"); err != nil { - t.Errorf("failed to set FROM address: %s", err) - return - } - if err := message.To("valid-to@domain.tld"); err != nil { - t.Errorf("failed to set TO address: %s", err) - return - } - message.Subject("Test subject") - message.SetBodyString(TypeTextPlain, "Test body") - message.SetMessageIDWithValue("this.is.a.message.id") - - client, err := NewClient(TestServerAddr, WithPort(serverPort), - WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), - WithUsername("toni@tester.com"), - WithPassword("V3ryS3cr3t+")) - if err != nil { - t.Errorf("unable to create new client: %s", err) - } - if err = client.DialWithContext(context.Background()); err != nil { - t.Errorf("failed to dial to test server: %s", err) - } - if err = client.Send(message); err == nil { - t.Error("expected Send() to fail but didn't") - } - - var sendErr *SendError - if !errors.As(err, &sendErr) { - t.Errorf("expected *SendError type as returned error, but got %T", sendErr) - } - if errors.As(err, &sendErr) { - if sendErr.IsTemp() { - t.Errorf("expected permanent error but IsTemp() returned true") - } - if sendErr.Reason != ErrSMTPMailFrom { - t.Errorf("expected ErrSMTPMailFrom error, but got %s", sendErr.Reason) - } - if !strings.EqualFold(sendErr.MessageID(), "") { - t.Errorf("expected message ID: %q, but got %q", "", - sendErr.MessageID()) - } - if len(sendErr.errlist) != 2 { - t.Errorf("expected 2 errors, but got %d", len(sendErr.errlist)) - return - } - if !strings.EqualFold(sendErr.errlist[0].Error(), "503 5.1.2 Invalid from: ") { - t.Errorf("expected error: %q, but got %q", - "503 5.1.2 Invalid from: ", sendErr.errlist[0].Error()) - } - if !strings.EqualFold(sendErr.errlist[1].Error(), "500 5.1.2 Error: reset failed") { - t.Errorf("expected error: %q, but got %q", - "500 5.1.2 Error: reset failed", sendErr.errlist[1].Error()) - } - } - - if err = client.Close(); err != nil { - t.Errorf("failed to close server connection: %s", err) - } -} func TestClient_SendErrorToReset(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) @@ -4250,6 +4068,21 @@ func parseJSONLog(t *testing.T, buf *bytes.Buffer) logData { return logdata } +// testMessage configures and returns a new email message for testing, initializing it with valid sender and recipient. +func testMessage(t *testing.T) *Msg { + t.Helper() + message := NewMsg() + if err := message.From(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") + return message +} + // testingKey replaces the substring "TESTING KEY" with "PRIVATE KEY" in the given string s. func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") } From 1519522e5d6f4eefaf0c2f779cb7ee0435afefa2 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 24 Oct 2024 10:50:17 +0200 Subject: [PATCH 041/188] 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. --- client_test.go | 93 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 65 insertions(+), 28 deletions(-) diff --git a/client_test.go b/client_test.go index e912730..f9f4e41 100644 --- a/client_test.go +++ b/client_test.go @@ -1144,7 +1144,7 @@ func TestClient_SetDebugLog(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) t.Run("SetDebugLog true", func(t *testing.T) { client, err := NewClient(DefaultHost) @@ -1536,7 +1536,7 @@ func TestClient_Close(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -1570,7 +1570,7 @@ func TestClient_Close(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -1608,7 +1608,7 @@ func TestClient_Close(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -1641,7 +1641,7 @@ func TestClient_DialWithContext(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) t.Run("connect and check connection", func(t *testing.T) { ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) @@ -1798,7 +1798,7 @@ func TestClient_DialWithContext(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -1833,7 +1833,7 @@ func TestClient_DialWithContext(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -1862,7 +1862,7 @@ func TestClient_DialWithContext(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -1892,7 +1892,7 @@ func TestClient_DialWithContext(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -1923,7 +1923,7 @@ func TestClient_DialWithContext(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -1953,7 +1953,7 @@ func TestClient_DialWithContext(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -1984,7 +1984,7 @@ func TestClient_DialWithContext(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -2017,7 +2017,7 @@ func TestClient_Reset(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -2050,7 +2050,7 @@ func TestClient_Reset(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -2085,7 +2085,7 @@ func TestClient_Reset(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -2125,7 +2125,7 @@ func TestClient_DialAndSendWithContext(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) client, err := NewClient(DefaultHost, WithTLSPolicy(NoTLS), WithPort(serverPort)) if err != nil { @@ -2150,7 +2150,7 @@ func TestClient_DialAndSendWithContext(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -2179,7 +2179,7 @@ func TestClient_DialAndSendWithContext(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -2208,7 +2208,7 @@ func TestClient_DialAndSendWithContext(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -2237,7 +2237,7 @@ func TestClient_DialAndSendWithContext(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -2286,7 +2286,7 @@ func TestClient_auth(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -2321,7 +2321,7 @@ func TestClient_auth(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -2351,7 +2351,7 @@ func TestClient_auth(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -2382,7 +2382,7 @@ func TestClient_auth(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -2412,7 +2412,7 @@ func TestClient_auth(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -2442,7 +2442,7 @@ func TestClient_auth(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -2476,7 +2476,7 @@ func TestClient_Send(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -2522,7 +2522,7 @@ func TestClient_Send(t *testing.T) { return } }() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 30) ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -2558,6 +2558,43 @@ func TestClient_Send(t *testing.T) { }) } +func TestClient_sendSingleMsg(t *testing.T) { + message := testMessage(t) + t.Run("connect and send email", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-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) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.sendSingleMsg(message); err != nil { + t.Errorf("failed to send message: %s", err) + } + }) +} + // TestClient_onlinetests will perform some additional tests on a actual live mail server. These tests are only // meant for the CI/CD pipeline and are usually skipped. They can be activated by setting PERFORM_ONLINE_TEST=true // in the ENV. The normal test suite should provide all the tests needed to cover the full functionality. From 45ebcb95b3b81075c91277ca44a6b8dec482f910 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 24 Oct 2024 12:02:57 +0200 Subject: [PATCH 042/188] 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. --- client.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/client.go b/client.go index 3e26e9f..50d2f28 100644 --- a/client.go +++ b/client.go @@ -1276,12 +1276,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 } From 1399a3331ac8919d929da08ca9bbb68d8da1466c Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 24 Oct 2024 12:03:56 +0200 Subject: [PATCH 043/188] 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. --- client_test.go | 469 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 454 insertions(+), 15 deletions(-) diff --git a/client_test.go b/client_test.go index f9f4e41..5a65943 100644 --- a/client_test.go +++ b/client_test.go @@ -13,6 +13,7 @@ import ( "errors" "fmt" "net" + "net/mail" "os" "reflect" "strings" @@ -2229,9 +2230,9 @@ func TestClient_DialAndSendWithContext(t *testing.T) { featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" go func() { if err := simpleSMTPServer(ctx, t, &serverProps{ - FailOnData: true, - FeatureSet: featureSet, - ListenPort: serverPort, + FailOnDataClose: true, + FeatureSet: featureSet, + ListenPort: serverPort, }); err != nil { t.Errorf("failed to start test server: %s", err) return @@ -2559,7 +2560,6 @@ func TestClient_Send(t *testing.T) { } func TestClient_sendSingleMsg(t *testing.T) { - message := testMessage(t) t.Run("connect and send email", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -2577,6 +2577,8 @@ func TestClient_sendSingleMsg(t *testing.T) { }() time.Sleep(time.Millisecond * 30) + message := testMessage(t) + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) t.Cleanup(cancelDial) @@ -2593,6 +2595,426 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Errorf("failed to send message: %s", err) } }) + t.Run("server does not support 8BITMIME", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-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) + + message := testMessage(t) + message.SetEncoding(NoEncoding) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.sendSingleMsg(message); err == nil { + t.Errorf("client should have failed to send message") + } + }) + t.Run("fail on invalid sender address", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-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) + + message := testMessage(t) + message.addrHeader["From"] = []*mail.Address{ + {Name: "invalid", Address: "invalid"}, + } + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.sendSingleMsg(message); err == nil { + t.Errorf("client should have failed to send message") + } + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Errorf("expected SendError, got %T", err) + } + if sendErr.Reason != ErrSMTPMailFrom { + t.Errorf("expected ErrSMTPMailFrom, got %s", sendErr.Reason) + } + }) + t.Run("fail with no sender address", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-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) + + message := testMessage(t) + message.addrHeader["From"] = nil + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.sendSingleMsg(message); err == nil { + t.Errorf("client should have failed to send message") + } + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Errorf("expected SendError, got %T", err) + } + if sendErr.Reason != ErrGetSender { + t.Errorf("expected ErrGetSender, got %s", sendErr.Reason) + } + }) + t.Run("fail with no recepient addresses", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-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) + + message := testMessage(t) + message.addrHeader["To"] = nil + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.sendSingleMsg(message); err == nil { + t.Errorf("client should have failed to send message") + } + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Errorf("expected SendError, got %T", err) + } + if sendErr.Reason != ErrGetRcpts { + t.Errorf("expected ErrGetRcpts, got %s", sendErr.Reason) + } + }) + t.Run("connect and send email with DSN", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + SupportDSN: true, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + message := testMessage(t) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS), WithDSN()) + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.sendSingleMsg(message); err != nil { + t.Errorf("failed to send message: %s", err) + } + }) + t.Run("connect and send email but fail on reset", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnReset: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + message := testMessage(t) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.sendSingleMsg(message); err == nil { + t.Errorf("client should have failed to send message") + } + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Errorf("expected SendError, got %T", err) + } + if sendErr.Reason != ErrSMTPReset { + t.Errorf("expected ErrSMTPReset, got %s", sendErr.Reason) + } + }) + t.Run("connect and send email but with mix of valid and invalid rcpts", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnReset: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + message := testMessage(t) + message.addrHeader["To"] = append(message.addrHeader["To"], &mail.Address{Name: "invalid", Address: "invalid"}) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.sendSingleMsg(message); err == nil { + t.Errorf("client should have failed to send message") + } + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Errorf("expected SendError, got %T", err) + } + if sendErr.Reason != ErrSMTPRcptTo { + t.Errorf("expected ErrSMTPRcptTo, got %s", sendErr.Reason) + } + }) + t.Run("connect and send email but fail on mail to and reset", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnMailFrom: true, + FailOnReset: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + message := testMessage(t) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.sendSingleMsg(message); err == nil { + t.Errorf("client should have failed to send message") + } + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Errorf("expected SendError, got %T", err) + } + if sendErr.Reason != ErrSMTPMailFrom { + t.Errorf("expected ErrSMTPMailFrom, got %s", sendErr.Reason) + } + }) + t.Run("connect and send email but fail on data init", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnDataInit: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + message := testMessage(t) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.sendSingleMsg(message); err == nil { + t.Errorf("client should have failed to send message") + } + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Errorf("expected SendError, got %T", err) + } + if sendErr.Reason != ErrSMTPData { + t.Errorf("expected ErrSMTPData, got %s", sendErr.Reason) + } + }) + t.Run("connect and send email but fail on data close", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnDataClose: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + message := testMessage(t) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.sendSingleMsg(message); err == nil { + t.Errorf("client should have failed to send message") + } + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Errorf("expected SendError, got %T", err) + } + if sendErr.Reason != ErrSMTPDataClose { + t.Errorf("expected ErrSMTPDataClose, got %s", sendErr.Reason) + } + }) } // TestClient_onlinetests will perform some additional tests on a actual live mail server. These tests are only @@ -4125,16 +4547,19 @@ func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", " // serverProps represents the configuration properties for the SMTP server. type serverProps struct { - FailOnAuth bool - FailOnData bool - FailOnHelo bool - FailOnQuit bool - FailOnReset bool - FailOnSTARTTLS bool - FeatureSet string - ListenPort int - SSLListener bool - IsTLS bool + FailOnAuth bool + FailOnDataInit bool + FailOnDataClose bool + FailOnHelo bool + FailOnMailFrom bool + FailOnQuit bool + FailOnReset bool + FailOnSTARTTLS bool + FeatureSet string + ListenPort int + SSLListener bool + IsTLS bool + SupportDSN bool } // simpleSMTPServer starts a simple TCP server that resonds to SMTP commands. @@ -4238,9 +4663,16 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server writeLine("250-localhost.localdomain\r\n" + props.FeatureSet) break case strings.HasPrefix(data, "MAIL FROM:"): + if props.FailOnMailFrom { + writeLine("500 5.5.2 Error: fail on MAIL FROM") + break + } from := strings.TrimPrefix(data, "MAIL FROM:") from = strings.ReplaceAll(from, "BODY=8BITMIME", "") from = strings.ReplaceAll(from, "SMTPUTF8", "") + if props.SupportDSN { + from = strings.ReplaceAll(from, "RET=FULL", "") + } from = strings.TrimSpace(from) if !strings.EqualFold(from, "") { writeLine(fmt.Sprintf("503 5.1.2 Invalid from: %s", from)) @@ -4249,6 +4681,9 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server writeOK() case strings.HasPrefix(data, "RCPT TO:"): to := strings.TrimPrefix(data, "RCPT TO:") + if props.SupportDSN { + to = strings.ReplaceAll(to, "NOTIFY=FAILURE,SUCCESS", "") + } to = strings.TrimSpace(to) if !strings.EqualFold(to, "") { writeLine(fmt.Sprintf("500 5.1.2 Invalid to: %s", to)) @@ -4262,6 +4697,10 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server } writeLine("235 2.7.0 Authentication successful") case strings.EqualFold(data, "DATA"): + if props.FailOnDataInit { + writeLine("503 5.5.1 Error: fail on DATA init") + break + } writeLine("354 End data with .") for { ddata, derr := reader.ReadString('\n') @@ -4271,7 +4710,7 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server } ddata = strings.TrimSpace(ddata) if ddata == "." { - if props.FailOnData { + if props.FailOnDataClose { writeLine("500 5.0.0 Error during DATA transmission") break } From 0310527eb591d84e5f375d49da25d78bad944241 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 24 Oct 2024 12:25:13 +0200 Subject: [PATCH 044/188] Completed client.go tests We've now covered 96% of all code. Everything else is not testable for us at this point. --- client_test.go | 110 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 99 insertions(+), 11 deletions(-) diff --git a/client_test.go b/client_test.go index 5a65943..cde9f11 100644 --- a/client_test.go +++ b/client_test.go @@ -2502,7 +2502,7 @@ func TestClient_Send(t *testing.T) { } var sendErr *SendError if !errors.As(err, &sendErr) { - t.Fatalf("expected SendError, got %T", err) + t.Fatalf("expected SendError, got %s", err) } if sendErr.Reason != ErrConnCheck { t.Errorf("expected ErrConnCheck, got %s", sendErr.Reason) @@ -2670,7 +2670,7 @@ func TestClient_sendSingleMsg(t *testing.T) { } var sendErr *SendError if !errors.As(err, &sendErr) { - t.Errorf("expected SendError, got %T", err) + t.Errorf("expected SendError, got %s", err) } if sendErr.Reason != ErrSMTPMailFrom { t.Errorf("expected ErrSMTPMailFrom, got %s", sendErr.Reason) @@ -2713,7 +2713,7 @@ func TestClient_sendSingleMsg(t *testing.T) { } var sendErr *SendError if !errors.As(err, &sendErr) { - t.Errorf("expected SendError, got %T", err) + t.Errorf("expected SendError, got %s", err) } if sendErr.Reason != ErrGetSender { t.Errorf("expected ErrGetSender, got %s", sendErr.Reason) @@ -2756,7 +2756,7 @@ func TestClient_sendSingleMsg(t *testing.T) { } var sendErr *SendError if !errors.As(err, &sendErr) { - t.Errorf("expected SendError, got %T", err) + t.Errorf("expected SendError, got %s", err) } if sendErr.Reason != ErrGetRcpts { t.Errorf("expected ErrGetRcpts, got %s", sendErr.Reason) @@ -2835,7 +2835,7 @@ func TestClient_sendSingleMsg(t *testing.T) { } var sendErr *SendError if !errors.As(err, &sendErr) { - t.Errorf("expected SendError, got %T", err) + t.Errorf("expected SendError, got %s", err) } if sendErr.Reason != ErrSMTPReset { t.Errorf("expected ErrSMTPReset, got %s", sendErr.Reason) @@ -2879,7 +2879,7 @@ func TestClient_sendSingleMsg(t *testing.T) { } var sendErr *SendError if !errors.As(err, &sendErr) { - t.Errorf("expected SendError, got %T", err) + t.Errorf("expected SendError, got %s", err) } if sendErr.Reason != ErrSMTPRcptTo { t.Errorf("expected ErrSMTPRcptTo, got %s", sendErr.Reason) @@ -2923,7 +2923,7 @@ func TestClient_sendSingleMsg(t *testing.T) { } var sendErr *SendError if !errors.As(err, &sendErr) { - t.Errorf("expected SendError, got %T", err) + t.Errorf("expected SendError, got %s", err) } if sendErr.Reason != ErrSMTPMailFrom { t.Errorf("expected ErrSMTPMailFrom, got %s", sendErr.Reason) @@ -2966,7 +2966,7 @@ func TestClient_sendSingleMsg(t *testing.T) { } var sendErr *SendError if !errors.As(err, &sendErr) { - t.Errorf("expected SendError, got %T", err) + t.Errorf("expected SendError, got %s", err) } if sendErr.Reason != ErrSMTPData { t.Errorf("expected ErrSMTPData, got %s", sendErr.Reason) @@ -3009,7 +3009,7 @@ func TestClient_sendSingleMsg(t *testing.T) { } var sendErr *SendError if !errors.As(err, &sendErr) { - t.Errorf("expected SendError, got %T", err) + t.Errorf("expected SendError, got %s", err) } if sendErr.Reason != ErrSMTPDataClose { t.Errorf("expected ErrSMTPDataClose, got %s", sendErr.Reason) @@ -3017,6 +3017,88 @@ func TestClient_sendSingleMsg(t *testing.T) { }) } +func TestClient_checkConn(t *testing.T) { + t.Run("connection is alive", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-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) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.checkConn(); err != nil { + t.Errorf("failed to check connection: %s", err) + } + }) + t.Run("connection should fail on noop", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnNoop: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err = client.DialWithContext(ctxDial); err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.checkConn(); err == nil { + t.Errorf("client should have failed on connection check") + } + if !errors.Is(err, ErrNoActiveConnection) { + t.Errorf("expected ErrNoActiveConnection, got %s", err) + } + }) + t.Run("connection should fail on no connection", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err = client.checkConn(); err == nil { + t.Errorf("client should have failed on connection check") + } + if !errors.Is(err, ErrNoActiveConnection) { + t.Errorf("expected ErrNoActiveConnection, got %s", err) + } + }) +} + // TestClient_onlinetests will perform some additional tests on a actual live mail server. These tests are only // meant for the CI/CD pipeline and are usually skipped. They can be activated by setting PERFORM_ONLINE_TEST=true // in the ENV. The normal test suite should provide all the tests needed to cover the full functionality. @@ -4552,6 +4634,7 @@ type serverProps struct { FailOnDataClose bool FailOnHelo bool FailOnMailFrom bool + FailOnNoop bool FailOnQuit bool FailOnReset bool FailOnSTARTTLS bool @@ -4719,8 +4802,13 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server } datastring += ddata + "\n" } - case strings.EqualFold(data, "noop"), - strings.EqualFold(data, "vrfy"): + case strings.EqualFold(data, "noop"): + if props.FailOnNoop { + writeLine("500 5.0.0 Error: fail on NOOP") + break + } + writeOK() + case strings.EqualFold(data, "vrfy"): writeOK() case strings.EqualFold(data, "rset"): if props.FailOnReset { From 7ed23bf01b0bb2e9b69ee62ded9ce1df4c1d8337 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 24 Oct 2024 12:53:37 +0200 Subject: [PATCH 045/188] 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. --- client_test.go | 1660 ++++++------------------------------------------ 1 file changed, 210 insertions(+), 1450 deletions(-) diff --git a/client_test.go b/client_test.go index cde9f11..c391c20 100644 --- a/client_test.go +++ b/client_test.go @@ -12,6 +12,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net" "net/mail" "os" @@ -3106,1468 +3107,228 @@ func TestClient_onlinetests(t *testing.T) { if os.Getenv("PERFORM_ONLINE_TEST") != "true" { t.Skip(`"PERFORM_ONLINE_TEST" env variable is not set to "true". Skipping online tests.`) } -} - -/* - - - - -// TestClient_checkConn tests the checkConn method with intentional breaking for the Client object -func TestClient_checkConn(t *testing.T) { - c, err := getTestConnection(true) - if err != nil { - t.Skipf("failed to create test client: %s. Skipping tests", err) - } - if err = c.checkConn(); err == nil { - t.Errorf("connCheck() should fail but succeeded") - } -} - -// TestClient_DialAndSendWithDSN tests the DialAndSend() method of Client with DSN enabled -func TestClient_DialAndSendWithDSN(t *testing.T) { - if os.Getenv("TEST_ALLOW_SEND") == "" { - t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test") - } - m := NewMsg() - _ = m.FromFormat("go-mail Test Mailer", os.Getenv("TEST_FROM")) - _ = m.To(TestRcpt) - m.Subject(fmt.Sprintf("This is a test mail from go-mail/v%s", VERSION)) - m.SetBulk() - m.SetDate() - m.SetMessageID() - m.SetBodyString(TypeTextPlain, "This is a test mail from the go-mail library") - - c, err := getTestConnectionWithDSN(true) - if err != nil { - t.Skipf("failed to create test client: %s. Skipping tests", err) - } - - if err := c.DialAndSend(m); err != nil { - t.Errorf("DialAndSend() failed: %s", err) - } -} - -// TestClient_DialSendCloseBroken tests the Dial(), Send() and Close() method of Client with broken settings -func TestClient_DialSendCloseBroken(t *testing.T) { - if os.Getenv("TEST_ALLOW_SEND") == "" { - t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test") - } - tests := []struct { - name string - from string - to string - closestart bool - closeearly bool - sf bool - }{ - {"Invalid FROM", "foo@foo", TestRcpt, false, false, true}, - {"Invalid TO", os.Getenv("TEST_FROM"), "foo@foo", false, false, true}, - {"No FROM", "", TestRcpt, false, false, true}, - {"No TO", os.Getenv("TEST_FROM"), "", false, false, true}, - {"Close early", os.Getenv("TEST_FROM"), TestRcpt, false, true, true}, - {"Close start", os.Getenv("TEST_FROM"), TestRcpt, true, false, true}, - {"Close start/early", os.Getenv("TEST_FROM"), TestRcpt, true, true, true}, - } - - m := NewMsg(WithEncoding(NoEncoding)) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m.SetAddrHeaderIgnoreInvalid(HeaderFrom, tt.from) - m.SetAddrHeaderIgnoreInvalid(HeaderTo, tt.to) - - c, err := getTestConnection(true) - if err != nil { - t.Skipf("failed to create test client: %s. Skipping tests", err) - } - - ctx, cfn := context.WithTimeout(context.Background(), time.Second*10) - defer cfn() - if err := c.DialWithContext(ctx); err != nil && !tt.sf { - t.Errorf("Dail() failed: %s", err) - return - } - if tt.closestart { - _ = c.smtpClient.Close() - } - if err = c.Send(m); err != nil && !tt.sf { - t.Errorf("Send() failed: %s", err) - return - } - if tt.closeearly { - _ = c.smtpClient.Close() - } - if err = c.Close(); err != nil && !tt.sf { - t.Errorf("Close() failed: %s", err) - return - } - }) - } -} - -// TestClient_DialSendCloseBrokenWithDSN tests the Dial(), Send() and Close() method of Client with -// broken settings and DSN enabled -func TestClient_DialSendCloseBrokenWithDSN(t *testing.T) { - if os.Getenv("TEST_ALLOW_SEND") == "" { - t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test") - } - tests := []struct { - name string - from string - to string - closestart bool - closeearly bool - sf bool - }{ - {"Invalid FROM", "foo@foo", TestRcpt, false, false, true}, - {"Invalid TO", os.Getenv("TEST_FROM"), "foo@foo", false, false, true}, - {"No FROM", "", TestRcpt, false, false, true}, - {"No TO", os.Getenv("TEST_FROM"), "", false, false, true}, - {"Close early", os.Getenv("TEST_FROM"), TestRcpt, false, true, true}, - {"Close start", os.Getenv("TEST_FROM"), TestRcpt, true, false, true}, - {"Close start/early", os.Getenv("TEST_FROM"), TestRcpt, true, true, true}, - } - - m := NewMsg(WithEncoding(NoEncoding)) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m.SetAddrHeaderIgnoreInvalid(HeaderFrom, tt.from) - m.SetAddrHeaderIgnoreInvalid(HeaderTo, tt.to) - - c, err := getTestConnectionWithDSN(true) - if err != nil { - t.Skipf("failed to create test client: %s. Skipping tests", err) - } - - ctx, cfn := context.WithTimeout(context.Background(), time.Second*10) - defer cfn() - if err := c.DialWithContext(ctx); err != nil && !tt.sf { - t.Errorf("Dail() failed: %s", err) - return - } - if tt.closestart { - _ = c.smtpClient.Close() - } - if err = c.Send(m); err != nil && !tt.sf { - t.Errorf("Send() failed: %s", err) - return - } - if tt.closeearly { - _ = c.smtpClient.Close() - } - if err = c.Close(); err != nil && !tt.sf { - t.Errorf("Close() failed: %s", err) - return - } - }) - } -} - -func TestClient_DialWithContext_switchAuth(t *testing.T) { - if os.Getenv("TEST_ALLOW_SEND") == "" { - t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test") - } - - // We start with no auth explicitly set - client, err := NewClient( - os.Getenv("TEST_HOST"), - WithTLSPortPolicy(TLSMandatory), - ) - defer func() { - _ = client.Close() - }() - if err != nil { - t.Errorf("failed to create client: %s", err) - return - } - if err = client.DialWithContext(context.Background()); err != nil { - t.Errorf("failed to dial to sending server: %s", err) - } - if err = client.Close(); err != nil { - t.Errorf("failed to close client connection: %s", err) - } - - // We switch to LOGIN auth, which the server supports - client.SetSMTPAuth(SMTPAuthLogin) - client.SetUsername(os.Getenv("TEST_SMTPAUTH_USER")) - client.SetPassword(os.Getenv("TEST_SMTPAUTH_PASS")) - if err = client.DialWithContext(context.Background()); err != nil { - t.Errorf("failed to dial to sending server: %s", err) - } - if err = client.Close(); err != nil { - t.Errorf("failed to close client connection: %s", err) - } - - // We switch to CRAM-MD5, which the server does not support - error expected - client.SetSMTPAuth(SMTPAuthCramMD5) - if err = client.DialWithContext(context.Background()); err == nil { - t.Errorf("expected error when dialing with unsupported auth mechanism, got nil") - return - } - if !errors.Is(err, ErrCramMD5AuthNotSupported) { - t.Errorf("expected dial error: %s, but got: %s", ErrCramMD5AuthNotSupported, err) - } - - // We switch to CUSTOM by providing PLAIN auth as function - the server supports this - client.SetSMTPAuthCustom(smtp.PlainAuth("", os.Getenv("TEST_SMTPAUTH_USER"), - os.Getenv("TEST_SMTPAUTH_PASS"), os.Getenv("TEST_HOST"), false)) - if client.smtpAuthType != SMTPAuthCustom { - t.Errorf("expected auth type to be Custom, got: %s", client.smtpAuthType) - } - if err = client.DialWithContext(context.Background()); err != nil { - t.Errorf("failed to dial to sending server: %s", err) - } - if err = client.Close(); err != nil { - t.Errorf("failed to close client connection: %s", err) - } - - // We switch back to explicit no authenticaiton - client.SetSMTPAuth(SMTPAuthNoAuth) - if err = client.DialWithContext(context.Background()); err != nil { - t.Errorf("failed to dial to sending server: %s", err) - } - if err = client.Close(); err != nil { - t.Errorf("failed to close client connection: %s", err) - } - - // Finally we set an empty string as SMTPAuthType and expect and error. This way we can - // verify that we do not accidentaly skip authentication with an empty string SMTPAuthType - client.SetSMTPAuth("") - if err = client.DialWithContext(context.Background()); err == nil { - t.Errorf("expected error when dialing with empty auth mechanism, got nil") - } -} - -// TestClient_Send_MsgSendError tests the Client.Send method with a broken recipient and verifies -// that the SendError type works properly -func TestClient_Send_MsgSendError(t *testing.T) { - if os.Getenv("TEST_ALLOW_SEND") == "" { - t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test") - } - var msgs []*Msg - rcpts := []string{"invalid@domain.tld", "invalid@address.invalid"} - for _, rcpt := range rcpts { - m := NewMsg() - _ = m.FromFormat("go-mail Test Mailer", os.Getenv("TEST_FROM")) - _ = m.To(rcpt) - m.Subject(fmt.Sprintf("This is a test mail from go-mail/v%s", VERSION)) - m.SetBulk() - m.SetDate() - m.SetMessageID() - m.SetBodyString(TypeTextPlain, "This is a test mail from the go-mail library") - msgs = append(msgs, m) - } - - c, err := getTestConnection(true) - if err != nil { - t.Skipf("failed to create test client: %s. Skipping tests", err) - } - - ctx, cfn := context.WithTimeout(context.Background(), DefaultTimeout) - defer cfn() - if err := c.DialWithContext(ctx); err != nil { - t.Errorf("failed to dial to sending server: %s", err) - } - if err := c.Send(msgs...); err == nil { - t.Errorf("sending messages with broken recipients was supposed to fail but didn't") - } - if err := c.Close(); err != nil { - t.Errorf("failed to close client connection: %s", err) - } - for _, m := range msgs { - if !m.HasSendError() { - t.Errorf("message was expected to have a send error, but didn't") + t.Run("Authentication", func(t *testing.T) { + hostname := os.Getenv("TEST_HOST") + username := os.Getenv("TEST_USER") + password := os.Getenv("TEST_PASS") + tests := []struct { + name string + authtype SMTPAuthType + }{ + {"LOGIN", SMTPAuthLogin}, + {"PLAIN", SMTPAuthPlain}, + {"CRAM-MD5", SMTPAuthCramMD5}, + {"SCRAM-SHA-1", SMTPAuthSCRAMSHA1}, + {"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS}, + {"SCRAM-SHA-256", SMTPAuthSCRAMSHA256}, + {"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS}, } - se := &SendError{Reason: ErrSMTPRcptTo} - if !errors.Is(m.SendError(), se) { - t.Errorf("error mismatch, expected: %s, got: %s", se, m.SendError()) - } - if m.SendErrorIsTemp() { - t.Errorf("message was not expected to be a temporary error, but reported as such") - } - } -} -// TestClient_DialAndSendWithContext_withSendError tests the Client.DialAndSendWithContext method -// with a broken recipient to make sure that the returned error satisfies the Msg.SendError type -func TestClient_DialAndSendWithContext_withSendError(t *testing.T) { - if os.Getenv("TEST_ALLOW_SEND") == "" { - t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test") - } - m := NewMsg() - _ = m.FromFormat("go-mail Test Mailer", os.Getenv("TEST_FROM")) - _ = m.To("invalid@domain.tld") - m.Subject(fmt.Sprintf("This is a test mail from go-mail/v%s", VERSION)) - m.SetBulk() - m.SetDate() - m.SetMessageID() - m.SetBodyString(TypeTextPlain, "This is a test mail from the go-mail library") - - c, err := getTestConnection(true) - if err != nil { - t.Skipf("failed to create test client: %s. Skipping tests", err) - } - ctx, cfn := context.WithTimeout(context.Background(), DefaultTimeout) - defer cfn() - err = c.DialAndSendWithContext(ctx, m) - if err == nil { - t.Errorf("expected DialAndSendWithContext with broken mail recipient to fail, but didn't") - return - } - var se *SendError - if !errors.As(err, &se) { - t.Errorf("expected *SendError type as returned error, but didn't") - return - } - if se.IsTemp() { - t.Errorf("expected permanent error but IsTemp() returned true") - } - if m.IsDelivered() { - t.Errorf("message is indicated to be delivered but shouldn't") - } -} - -func TestClient_SendErrorNoEncoding(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - featureSet := "250-AUTH PLAIN\r\n250-DSN\r\n250 SMTPUTF8" - serverPort := TestServerPortBase + 1 - go func() { - if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 300) - - message := NewMsg() - if err := message.From("valid-from@domain.tld"); err != nil { - t.Errorf("failed to set FROM address: %s", err) - return - } - if err := message.To("valid-to@domain.tld"); err != nil { - t.Errorf("failed to set TO address: %s", err) - return - } - message.Subject("Test subject") - message.SetBodyString(TypeTextPlain, "Test body") - message.SetMessageIDWithValue("this.is.a.message.id") - message.SetEncoding(NoEncoding) - - client, err := NewClient(TestServerAddr, WithPort(serverPort), - WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), - WithUsername("toni@tester.com"), - WithPassword("V3ryS3cr3t+")) - if err != nil { - t.Errorf("unable to create new client: %s", err) - } - if err = client.DialWithContext(context.Background()); err != nil { - t.Errorf("failed to dial to test server: %s", err) - } - if err = client.Send(message); err == nil { - t.Error("expected Send() to fail but didn't") - } - - var sendErr *SendError - if !errors.As(err, &sendErr) { - t.Errorf("expected *SendError type as returned error, but got %T", sendErr) - } - if errors.As(err, &sendErr) { - if sendErr.IsTemp() { - t.Errorf("expected permanent error but IsTemp() returned true") - } - if sendErr.Reason != ErrNoUnencoded { - t.Errorf("expected ErrNoUnencoded error, but got %s", sendErr.Reason) - } - if !strings.EqualFold(sendErr.MessageID(), "") { - t.Errorf("expected message ID: %q, but got %q", "", - sendErr.MessageID()) - } - if sendErr.Msg() == nil { - t.Errorf("expected message to be set, but got nil") - } - } - - if err = client.Close(); err != nil { - t.Errorf("failed to close server connection: %s", err) - } -} - -func TestClient_SendErrorMailFrom(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - serverPort := TestServerPortBase + 2 - featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" - go func() { - if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 300) - - message := NewMsg() - if err := message.From("invalid-from@domain.tld"); err != nil { - t.Errorf("failed to set FROM address: %s", err) - return - } - if err := message.To("valid-to@domain.tld"); err != nil { - t.Errorf("failed to set TO address: %s", err) - return - } - message.Subject("Test subject") - message.SetBodyString(TypeTextPlain, "Test body") - message.SetMessageIDWithValue("this.is.a.message.id") - - client, err := NewClient(TestServerAddr, WithPort(serverPort), - WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), - WithUsername("toni@tester.com"), - WithPassword("V3ryS3cr3t+")) - if err != nil { - t.Errorf("unable to create new client: %s", err) - } - if err = client.DialWithContext(context.Background()); err != nil { - t.Errorf("failed to dial to test server: %s", err) - } - if err = client.Send(message); err == nil { - t.Error("expected Send() to fail but didn't") - } - - var sendErr *SendError - if !errors.As(err, &sendErr) { - t.Errorf("expected *SendError type as returned error, but got %T", sendErr) - } - if errors.As(err, &sendErr) { - if sendErr.IsTemp() { - t.Errorf("expected permanent error but IsTemp() returned true") - } - if sendErr.Reason != ErrSMTPMailFrom { - t.Errorf("expected ErrSMTPMailFrom error, but got %s", sendErr.Reason) - } - if !strings.EqualFold(sendErr.MessageID(), "") { - t.Errorf("expected message ID: %q, but got %q", "", - sendErr.MessageID()) - } - if sendErr.Msg() == nil { - t.Errorf("expected message to be set, but got nil") - } - } - - if err = client.Close(); err != nil { - t.Errorf("failed to close server connection: %s", err) - } -} - - -func TestClient_SendErrorToReset(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - serverPort := TestServerPortBase + 4 - featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" - go func() { - if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 300) - - message := NewMsg() - if err := message.From("valid-from@domain.tld"); err != nil { - t.Errorf("failed to set FROM address: %s", err) - return - } - if err := message.To("invalid-to@domain.tld"); err != nil { - t.Errorf("failed to set TO address: %s", err) - return - } - message.Subject("Test subject") - message.SetBodyString(TypeTextPlain, "Test body") - message.SetMessageIDWithValue("this.is.a.message.id") - - client, err := NewClient(TestServerAddr, WithPort(serverPort), - WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), - WithUsername("toni@tester.com"), - WithPassword("V3ryS3cr3t+")) - if err != nil { - t.Errorf("unable to create new client: %s", err) - } - if err = client.DialWithContext(context.Background()); err != nil { - t.Errorf("failed to dial to test server: %s", err) - } - if err = client.Send(message); err == nil { - t.Error("expected Send() to fail but didn't") - } - - var sendErr *SendError - if !errors.As(err, &sendErr) { - t.Errorf("expected *SendError type as returned error, but got %T", sendErr) - } - if errors.As(err, &sendErr) { - if sendErr.IsTemp() { - t.Errorf("expected permanent error but IsTemp() returned true") - } - if sendErr.Reason != ErrSMTPRcptTo { - t.Errorf("expected ErrSMTPRcptTo error, but got %s", sendErr.Reason) - } - if !strings.EqualFold(sendErr.MessageID(), "") { - t.Errorf("expected message ID: %q, but got %q", "", - sendErr.MessageID()) - } - if len(sendErr.errlist) != 2 { - t.Errorf("expected 2 errors, but got %d", len(sendErr.errlist)) - return - } - if !strings.EqualFold(sendErr.errlist[0].Error(), "500 5.1.2 Invalid to: ") { - t.Errorf("expected error: %q, but got %q", - "500 5.1.2 Invalid to: ", sendErr.errlist[0].Error()) - } - if !strings.EqualFold(sendErr.errlist[1].Error(), "500 5.1.2 Error: reset failed") { - t.Errorf("expected error: %q, but got %q", - "500 5.1.2 Error: reset failed", sendErr.errlist[1].Error()) - } - } - - if err = client.Close(); err != nil { - t.Errorf("failed to close server connection: %s", err) - } -} - -func TestClient_SendErrorDataClose(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - serverPort := TestServerPortBase + 5 - featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" - go func() { - if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 300) - - message := NewMsg() - if err := message.From("valid-from@domain.tld"); err != nil { - t.Errorf("failed to set FROM address: %s", err) - return - } - if err := message.To("valid-to@domain.tld"); err != nil { - t.Errorf("failed to set TO address: %s", err) - return - } - message.Subject("Test subject") - message.SetBodyString(TypeTextPlain, "DATA close should fail") - message.SetMessageIDWithValue("this.is.a.message.id") - - client, err := NewClient(TestServerAddr, WithPort(serverPort), - WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), - WithUsername("toni@tester.com"), - WithPassword("V3ryS3cr3t+")) - if err != nil { - t.Errorf("unable to create new client: %s", err) - } - if err = client.DialWithContext(context.Background()); err != nil { - t.Errorf("failed to dial to test server: %s", err) - } - if err = client.Send(message); err == nil { - t.Error("expected Send() to fail but didn't") - } - - var sendErr *SendError - if !errors.As(err, &sendErr) { - t.Errorf("expected *SendError type as returned error, but got %T", sendErr) - } - if errors.As(err, &sendErr) { - if sendErr.IsTemp() { - t.Errorf("expected permanent error but IsTemp() returned true") - } - if sendErr.Reason != ErrSMTPDataClose { - t.Errorf("expected ErrSMTPDataClose error, but got %s", sendErr.Reason) - } - if !strings.EqualFold(sendErr.MessageID(), "") { - t.Errorf("expected message ID: %q, but got %q", "", - sendErr.MessageID()) - } - } - - if err = client.Close(); err != nil { - t.Errorf("failed to close server connection: %s", err) - } -} - -func TestClient_SendErrorDataWrite(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - serverPort := TestServerPortBase + 6 - featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" - go func() { - if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 300) - - message := NewMsg() - if err := message.From("valid-from@domain.tld"); err != nil { - t.Errorf("failed to set FROM address: %s", err) - return - } - if err := message.To("valid-to@domain.tld"); err != nil { - t.Errorf("failed to set TO address: %s", err) - return - } - message.Subject("Test subject") - message.SetBodyString(TypeTextPlain, "DATA write should fail") - message.SetMessageIDWithValue("this.is.a.message.id") - message.SetGenHeader("X-Test-Header", "DATA write should fail") - - client, err := NewClient(TestServerAddr, WithPort(serverPort), - WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), - WithUsername("toni@tester.com"), - WithPassword("V3ryS3cr3t+")) - if err != nil { - t.Errorf("unable to create new client: %s", err) - } - if err = client.DialWithContext(context.Background()); err != nil { - t.Errorf("failed to dial to test server: %s", err) - } - if err = client.Send(message); err == nil { - t.Error("expected Send() to fail but didn't") - } - - var sendErr *SendError - if !errors.As(err, &sendErr) { - t.Errorf("expected *SendError type as returned error, but got %T", sendErr) - } - if errors.As(err, &sendErr) { - if sendErr.IsTemp() { - t.Errorf("expected permanent error but IsTemp() returned true") - } - if sendErr.Reason != ErrSMTPDataClose { - t.Errorf("expected ErrSMTPDataClose error, but got %s", sendErr.Reason) - } - if !strings.EqualFold(sendErr.MessageID(), "") { - t.Errorf("expected message ID: %q, but got %q", "", - sendErr.MessageID()) - } - } -} - -func TestClient_SendErrorReset(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - serverPort := TestServerPortBase + 7 - featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" - go func() { - if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 300) - - message := NewMsg() - if err := message.From("valid-from@domain.tld"); err != nil { - t.Errorf("failed to set FROM address: %s", err) - return - } - if err := message.To("valid-to@domain.tld"); err != nil { - t.Errorf("failed to set TO address: %s", err) - return - } - message.Subject("Test subject") - message.SetBodyString(TypeTextPlain, "Test body") - message.SetMessageIDWithValue("this.is.a.message.id") - - client, err := NewClient(TestServerAddr, WithPort(serverPort), - WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), - WithUsername("toni@tester.com"), - WithPassword("V3ryS3cr3t+")) - if err != nil { - t.Errorf("unable to create new client: %s", err) - } - if err = client.DialWithContext(context.Background()); err != nil { - t.Errorf("failed to dial to test server: %s", err) - } - if err = client.Send(message); err == nil { - t.Error("expected Send() to fail but didn't") - } - - var sendErr *SendError - if !errors.As(err, &sendErr) { - t.Errorf("expected *SendError type as returned error, but got %T", sendErr) - } - if errors.As(err, &sendErr) { - if sendErr.IsTemp() { - t.Errorf("expected permanent error but IsTemp() returned true") - } - if sendErr.Reason != ErrSMTPReset { - t.Errorf("expected ErrSMTPReset error, but got %s", sendErr.Reason) - } - if !strings.EqualFold(sendErr.MessageID(), "") { - t.Errorf("expected message ID: %q, but got %q", "", - sendErr.MessageID()) - } - } - - if err = client.Close(); err != nil { - t.Errorf("failed to close server connection: %s", err) - } -} - -func TestClient_DialSendConcurrent_online(t *testing.T) { - if os.Getenv("TEST_ALLOW_SEND") == "" { - t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test") - } - - client, err := getTestConnection(true) - if err != nil { - t.Skipf("failed to create test client: %s. Skipping tests", err) - } - - var messages []*Msg - for i := 0; i < 10; i++ { - message := NewMsg() - if err := message.FromFormat("go-mail Test Mailer", os.Getenv("TEST_FROM")); err != nil { - t.Errorf("failed to set FROM address: %s", err) - return - } - if err := message.To(TestRcpt); err != nil { - t.Errorf("failed to set TO address: %s", err) - return - } - message.Subject(fmt.Sprintf("Test subject for mail %d", i)) - message.SetBodyString(TypeTextPlain, fmt.Sprintf("This is the test body of the mail no. %d", i)) - message.SetMessageID() - messages = append(messages, message) - } - - if err = client.DialWithContext(context.Background()); err != nil { - t.Errorf("failed to dial to test server: %s", err) - } - - wg := sync.WaitGroup{} - for id, message := range messages { - wg.Add(1) - go func(curMsg *Msg, curID int) { - defer wg.Done() - if goroutineErr := client.Send(curMsg); err != nil { - t.Errorf("failed to send message with ID %d: %s", curID, goroutineErr) - } - }(message, id) - } - wg.Wait() - - if err = client.Close(); err != nil { - t.Errorf("failed to close server connection: %s", err) - } -} - -func TestClient_DialSendConcurrent_local(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - serverPort := TestServerPortBase + 20 - featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" - go func() { - if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 500) - - client, err := NewClient(TestServerAddr, WithPort(serverPort), - WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain), - WithUsername("toni@tester.com"), - WithPassword("V3ryS3cr3t+")) - if err != nil { - t.Errorf("unable to create new client: %s", err) - } - - var messages []*Msg - for i := 0; i < 20; i++ { - message := NewMsg() - if err := message.From("valid-from@domain.tld"); err != nil { - t.Errorf("failed to set FROM address: %s", err) - return - } - if err := message.To("valid-to@domain.tld"); err != nil { - t.Errorf("failed to set TO address: %s", err) - return - } - message.Subject("Test subject") - message.SetBodyString(TypeTextPlain, "Test body") - message.SetMessageIDWithValue("this.is.a.message.id") - messages = append(messages, message) - } - - if err = client.DialWithContext(context.Background()); err != nil { - t.Errorf("failed to dial to test server: %s", err) - } - - wg := sync.WaitGroup{} - for id, message := range messages { - wg.Add(1) - go func(curMsg *Msg, curID int) { - defer wg.Done() - if goroutineErr := client.Send(curMsg); err != nil { - t.Errorf("failed to send message with ID %d: %s", curID, goroutineErr) - } - }(message, id) - } - wg.Wait() - - if err = client.Close(); err != nil { - t.Logf("failed to close server connection: %s", err) - } -} - -func TestClient_AuthSCRAMSHAX(t *testing.T) { - if os.Getenv("TEST_ONLINE_SCRAM") == "" { - t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests") - } - hostname := os.Getenv("TEST_HOST_SCRAM") - username := os.Getenv("TEST_USER_SCRAM") - password := os.Getenv("TEST_PASS_SCRAM") - - tests := []struct { - name string - authtype SMTPAuthType - }{ - {"SCRAM-SHA-1", SMTPAuthSCRAMSHA1}, - {"SCRAM-SHA-256", SMTPAuthSCRAMSHA256}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client, err := NewClient(hostname, - WithTLSPortPolicy(TLSMandatory), - WithSMTPAuth(tt.authtype), - WithUsername(username), WithPassword(password)) - if err != nil { - t.Errorf("unable to create new client: %s", err) - return - } - if err = client.DialWithContext(context.Background()); err != nil { - t.Errorf("failed to dial to test server: %s", err) - } - if err = client.Close(); err != nil { - t.Errorf("failed to close server connection: %s", err) - } - }) - } -} - -func TestClient_AuthLoginSuccess(t *testing.T) { - tests := []struct { - name string - featureSet string - }{ - {"default", "250-AUTH LOGIN\r\n250-X-DEFAULT-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"}, - {"mox server", "250-AUTH LOGIN\r\n250-X-MOX-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"}, - {"null byte", "250-AUTH LOGIN\r\n250-X-NULLBYTE-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"}, - {"bogus responses", "250-AUTH LOGIN\r\n250-X-BOGUS-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"}, - {"empty responses", "250-AUTH LOGIN\r\n250-X-EMPTY-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"}, - } - - for i, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - serverPort := TestServerPortBase + 40 + i - go func() { - if err := simpleSMTPServer(ctx, tt.featureSet, true, serverPort); err != nil { - t.Errorf("failed to start test server: %s", err) - return + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := NewClient(hostname, + WithTLSPortPolicy(TLSMandatory), + WithSMTPAuth(tt.authtype), + WithUsername(username), WithPassword(password)) + if err != nil { + t.Fatalf("unable to create new client: %s", err) } - }() - time.Sleep(time.Millisecond * 300) - client, err := NewClient(TestServerAddr, - WithPort(serverPort), - WithTLSPortPolicy(NoTLS), - WithSMTPAuth(SMTPAuthLogin), - WithUsername("toni@tester.com"), - WithPassword("V3ryS3cr3t+")) - if err != nil { - t.Errorf("unable to create new client: %s", err) - return + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + t.Cleanup(cancel) + + if err = client.DialWithContext(ctx); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.smtpClient.Noop(); err != nil { + t.Errorf("failed to send noop: %s", err) + } + if err = client.Close(); err != nil { + t.Errorf("failed to close client connection: %s", err) + } + }) + } + }) + t.Run("SCRAM-SHA-PLUS TLSExporter method (TLS 1.3)", func(t *testing.T) { + hostname := os.Getenv("TEST_HOST") + username := os.Getenv("TEST_USER") + password := os.Getenv("TEST_PASS") + tests := []struct { + name string + authtype SMTPAuthType + }{ + {"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS}, + {"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS}, + } + + tlsConfig := &tls.Config{ + MaxVersion: tls.VersionTLS13, + MinVersion: tls.VersionTLS13, + ServerName: hostname, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := NewClient(hostname, + WithTLSPortPolicy(TLSMandatory), WithTLSConfig(tlsConfig), + WithSMTPAuth(tt.authtype), + WithUsername(username), WithPassword(password)) + if err != nil { + t.Fatalf("unable to create new client: %s", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + t.Cleanup(cancel) + + if err = client.DialWithContext(ctx); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.smtpClient.Noop(); err != nil { + t.Errorf("failed to send noop: %s", err) + } + if err = client.Close(); err != nil { + t.Errorf("failed to close client connection: %s", err) + } + }) + } + }) + t.Run("SCRAM-SHA-PLUS TLSUnique method (TLS 1.2)", func(t *testing.T) { + hostname := os.Getenv("TEST_HOST") + username := os.Getenv("TEST_USER") + password := os.Getenv("TEST_PASS") + tests := []struct { + name string + authtype SMTPAuthType + }{ + {"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS}, + {"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS}, + } + + tlsConfig := &tls.Config{ + MaxVersion: tls.VersionTLS12, + MinVersion: tls.VersionTLS12, + ServerName: hostname, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := NewClient(hostname, + WithTLSPortPolicy(TLSMandatory), WithTLSConfig(tlsConfig), + WithSMTPAuth(tt.authtype), + WithUsername(username), WithPassword(password)) + if err != nil { + t.Fatalf("unable to create new client: %s", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + t.Cleanup(cancel) + + if err = client.DialWithContext(ctx); err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.smtpClient.Noop(); err != nil { + t.Errorf("failed to send noop: %s", err) + } + if err = client.Close(); err != nil { + t.Errorf("failed to close client connection: %s", err) + } + }) + } + }) +} + +func TestClient_XOAuth2OnFaker(t *testing.T) { + t.Run("Success", func(t *testing.T) { + server := []string{ + "220 Fake server ready ESMTP", + "250-fake.server", + "250-AUTH LOGIN XOAUTH2", + "250 8BITMIME", + "235 2.7.0 Accepted", + "221 OK", + } + var wrote strings.Builder + var fake faker + fake.ReadWriter = struct { + io.Reader + io.Writer + }{ + strings.NewReader(strings.Join(server, "\r\n")), + &wrote, + } + c, err := NewClient("fake.host", + WithDialContextFunc(getFakeDialFunc(fake)), + WithTLSPortPolicy(NoTLS), + WithSMTPAuth(SMTPAuthXOAUTH2), + WithUsername("user"), + WithPassword("token")) + if err != nil { + t.Fatalf("unable to create new client: %v", err) + } + if err = c.DialWithContext(context.Background()); err != nil { + t.Fatalf("unexpected dial error: %v", err) + } + if err = c.Close(); err != nil { + t.Fatalf("disconnect from test server failed: %v", err) + } + if !strings.Contains(wrote.String(), "AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n") { + t.Fatalf("got %q; want AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n", wrote.String()) + } + }) + t.Run("Unsupported", func(t *testing.T) { + server := []string{ + "220 Fake server ready ESMTP", + "250-fake.server", + "250-AUTH LOGIN PLAIN", + "250 8BITMIME", + "221 OK", + } + var wrote strings.Builder + var fake faker + fake.ReadWriter = struct { + io.Reader + io.Writer + }{ + strings.NewReader(strings.Join(server, "\r\n")), + &wrote, + } + c, err := NewClient("fake.host", + WithDialContextFunc(getFakeDialFunc(fake)), + WithTLSPortPolicy(TLSOpportunistic), + WithSMTPAuth(SMTPAuthXOAUTH2)) + if err != nil { + t.Fatalf("unable to create new client: %v", err) + } + if err = c.DialWithContext(context.Background()); err == nil { + t.Fatal("expected dial error got nil") + } else { + if !errors.Is(err, ErrXOauth2AuthNotSupported) { + t.Fatalf("expected %v; got %v", ErrXOauth2AuthNotSupported, err) } - if err = client.DialWithContext(context.Background()); err != nil { - t.Errorf("failed to dial to test server: %s", err) - } - if err = client.Close(); err != nil { - t.Errorf("failed to close server connection: %s", err) - } - }) - } -} - -func TestClient_AuthLoginFail(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - serverPort := TestServerPortBase + 50 - featureSet := "250-AUTH LOGIN\r\n250-X-DEFAULT-LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" - go func() { - if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 300) - - client, err := NewClient(TestServerAddr, - WithPort(serverPort), - WithTLSPortPolicy(NoTLS), - WithSMTPAuth(SMTPAuthLogin), - WithUsername("toni@tester.com"), - WithPassword("InvalidPassword")) - if err != nil { - t.Errorf("unable to create new client: %s", err) - return - } - if err = client.DialWithContext(context.Background()); err == nil { - t.Error("expected to fail to dial to test server, but it succeeded") - } -} - -func TestClient_AuthLoginFail_noTLS(t *testing.T) { - if os.Getenv("TEST_SKIP_ONLINE") != "" { - t.Skipf("env variable TEST_SKIP_ONLINE is set. Skipping online tests") - } - th := os.Getenv("TEST_HOST") - if th == "" { - t.Skipf("no host set. Skipping online tests") - } - tp := 587 - if tps := os.Getenv("TEST_PORT"); tps != "" { - tpi, err := strconv.Atoi(tps) - if err == nil { - tp = tpi - } - } - client, err := NewClient(th, WithPort(tp), WithSMTPAuth(SMTPAuthLogin), WithTLSPolicy(NoTLS)) - if err != nil { - t.Errorf("failed to create new client: %s", err) - } - u := os.Getenv("TEST_SMTPAUTH_USER") - if u != "" { - client.SetUsername(u) - } - p := os.Getenv("TEST_SMTPAUTH_PASS") - if p != "" { - client.SetPassword(p) - } - // We don't want to log authentication data in tests - client.SetDebugLog(false) - - if err = client.DialWithContext(context.Background()); err == nil { - t.Error("expected to fail to dial to test server, but it succeeded") - } - if !errors.Is(err, smtp.ErrUnencrypted) { - t.Errorf("expected error to be %s, but got %s", smtp.ErrUnencrypted, err) - } -} - -func TestClient_AuthSCRAMSHAX_fail(t *testing.T) { - if os.Getenv("TEST_ONLINE_SCRAM") == "" { - t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests") - } - hostname := os.Getenv("TEST_HOST_SCRAM") - - tests := []struct { - name string - authtype SMTPAuthType - }{ - {"SCRAM-SHA-1", SMTPAuthSCRAMSHA1}, - {"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS}, - {"SCRAM-SHA-256", SMTPAuthSCRAMSHA256}, - {"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client, err := NewClient(hostname, - WithTLSPortPolicy(TLSMandatory), - WithSMTPAuth(tt.authtype), - WithUsername("invalid"), WithPassword("invalid")) - if err != nil { - t.Errorf("unable to create new client: %s", err) - return - } - if err = client.DialWithContext(context.Background()); err == nil { - t.Errorf("expected error but got nil") - } - }) - } -} - -func TestClient_AuthSCRAMSHAX_unsupported(t *testing.T) { - if os.Getenv("TEST_ALLOW_SEND") == "" { - t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test") - } - - client, err := getTestConnection(true) - if err != nil { - t.Skipf("failed to create test client: %s. Skipping tests", err) - } - - tests := []struct { - name string - authtype SMTPAuthType - expErr error - }{ - {"SCRAM-SHA-1", SMTPAuthSCRAMSHA1, ErrSCRAMSHA1AuthNotSupported}, - {"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS, ErrSCRAMSHA1PLUSAuthNotSupported}, - {"SCRAM-SHA-256", SMTPAuthSCRAMSHA256, ErrSCRAMSHA256AuthNotSupported}, - {"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS, ErrSCRAMSHA256PLUSAuthNotSupported}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client.SetSMTPAuth(tt.authtype) - client.SetTLSPolicy(TLSMandatory) - if err = client.DialWithContext(context.Background()); err == nil { - t.Errorf("expected error but got nil") - } - if !errors.Is(err, tt.expErr) { - t.Errorf("expected error %s, but got %s", tt.expErr, err) - } - }) - } -} - -func TestClient_AuthSCRAMSHAXPLUS_tlsexporter(t *testing.T) { - if os.Getenv("TEST_ONLINE_SCRAM") == "" { - t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests") - } - hostname := os.Getenv("TEST_HOST_SCRAM") - username := os.Getenv("TEST_USER_SCRAM") - password := os.Getenv("TEST_PASS_SCRAM") - - tests := []struct { - name string - authtype SMTPAuthType - }{ - {"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS}, - {"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client, err := NewClient(hostname, - WithTLSPortPolicy(TLSMandatory), - WithSMTPAuth(tt.authtype), - WithUsername(username), WithPassword(password)) - if err != nil { - t.Errorf("unable to create new client: %s", err) - return - } - if err = client.DialWithContext(context.Background()); err != nil { - t.Errorf("failed to dial to test server: %s", err) - } - if err = client.Close(); err != nil { - t.Errorf("failed to close server connection: %s", err) - } - }) - } -} - -func TestClient_AuthSCRAMSHAXPLUS_tlsunique(t *testing.T) { - if os.Getenv("TEST_ONLINE_SCRAM") == "" { - t.Skipf("TEST_ONLINE_SCRAM is not set. Skipping online SCRAM tests") - } - hostname := os.Getenv("TEST_HOST_SCRAM") - username := os.Getenv("TEST_USER_SCRAM") - password := os.Getenv("TEST_PASS_SCRAM") - tlsConfig := &tls.Config{} - tlsConfig.MaxVersion = tls.VersionTLS12 - tlsConfig.ServerName = hostname - - tests := []struct { - name string - authtype SMTPAuthType - }{ - {"SCRAM-SHA-1-PLUS", SMTPAuthSCRAMSHA1PLUS}, - {"SCRAM-SHA-256-PLUS", SMTPAuthSCRAMSHA256PLUS}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client, err := NewClient(hostname, - WithTLSPortPolicy(TLSMandatory), - WithTLSConfig(tlsConfig), - WithSMTPAuth(tt.authtype), - WithUsername(username), WithPassword(password)) - if err != nil { - t.Errorf("unable to create new client: %s", err) - return - } - if err = client.DialWithContext(context.Background()); err != nil { - t.Errorf("failed to dial to test server: %s", err) - } - if err = client.Close(); err != nil { - t.Errorf("failed to close server connection: %s", err) - } - }) - } -} - -// getTestConnection takes environment variables to establish a connection to a real -// SMTP server to test all functionality that requires a connection -func getTestConnection(auth bool) (*Client, error) { - if os.Getenv("TEST_SKIP_ONLINE") != "" { - return nil, fmt.Errorf("env variable TEST_SKIP_ONLINE is set. Skipping online tests") - } - th := os.Getenv("TEST_HOST") - if th == "" { - return nil, fmt.Errorf("no TEST_HOST set") - } - tp := 25 - if tps := os.Getenv("TEST_PORT"); tps != "" { - tpi, err := strconv.Atoi(tps) - if err == nil { - tp = tpi - } - } - sv := false - if sve := os.Getenv("TEST_TLS_SKIP_VERIFY"); sve != "" { - sv = true - } - c, err := NewClient(th, WithPort(tp)) - if err != nil { - return c, err - } - c.tlsconfig.InsecureSkipVerify = sv - if auth { - st := os.Getenv("TEST_SMTPAUTH_TYPE") - if st != "" { - c.SetSMTPAuth(SMTPAuthType(st)) - } - u := os.Getenv("TEST_SMTPAUTH_USER") - if u != "" { - c.SetUsername(u) - } - p := os.Getenv("TEST_SMTPAUTH_PASS") - if p != "" { - c.SetPassword(p) - } - // We don't want to log authentication data in tests - c.SetDebugLog(false) - } - if err = c.DialWithContext(context.Background()); err != nil { - return c, fmt.Errorf("connection to test server failed: %w", err) - } - if err = c.Close(); err != nil { - return c, fmt.Errorf("disconnect from test server failed: %w", err) - } - return c, nil -} - -// getTestConnectionNoTestPort takes environment variables (except the port) to establish a -// connection to a real SMTP server to test all functionality that requires a connection -func getTestConnectionNoTestPort(auth bool) (*Client, error) { - if os.Getenv("TEST_SKIP_ONLINE") != "" { - return nil, fmt.Errorf("env variable TEST_SKIP_ONLINE is set. Skipping online tests") - } - th := os.Getenv("TEST_HOST") - if th == "" { - return nil, fmt.Errorf("no TEST_HOST set") - } - sv := false - if sve := os.Getenv("TEST_TLS_SKIP_VERIFY"); sve != "" { - sv = true - } - c, err := NewClient(th) - if err != nil { - return c, err - } - c.tlsconfig.InsecureSkipVerify = sv - if auth { - st := os.Getenv("TEST_SMTPAUTH_TYPE") - if st != "" { - c.SetSMTPAuth(SMTPAuthType(st)) - } - u := os.Getenv("TEST_SMTPAUTH_USER") - if u != "" { - c.SetUsername(u) - } - p := os.Getenv("TEST_SMTPAUTH_PASS") - if p != "" { - c.SetPassword(p) - } - // We don't want to log authentication data in tests - c.SetDebugLog(false) - } - if err := c.DialWithContext(context.Background()); err != nil { - return c, fmt.Errorf("connection to test server failed: %w", err) - } - if err := c.Close(); err != nil { - return c, fmt.Errorf("disconnect from test server failed: %w", err) - } - return c, nil -} - -// getTestClient takes environment variables to establish a client without connecting -// to the SMTP server -func getTestClient(auth bool) (*Client, error) { - if os.Getenv("TEST_SKIP_ONLINE") != "" { - return nil, fmt.Errorf("env variable TEST_SKIP_ONLINE is set. Skipping online tests") - } - th := os.Getenv("TEST_HOST") - if th == "" { - return nil, fmt.Errorf("no TEST_HOST set") - } - tp := 25 - if tps := os.Getenv("TEST_PORT"); tps != "" { - tpi, err := strconv.Atoi(tps) - if err == nil { - tp = tpi - } - } - sv := false - if sve := os.Getenv("TEST_TLS_SKIP_VERIFY"); sve != "" { - sv = true - } - c, err := NewClient(th, WithPort(tp)) - if err != nil { - return c, err - } - c.tlsconfig.InsecureSkipVerify = sv - if auth { - st := os.Getenv("TEST_SMTPAUTH_TYPE") - if st != "" { - c.SetSMTPAuth(SMTPAuthType(st)) - } - u := os.Getenv("TEST_SMTPAUTH_USER") - if u != "" { - c.SetUsername(u) - } - p := os.Getenv("TEST_SMTPAUTH_PASS") - if p != "" { - c.SetPassword(p) - } - // We don't want to log authentication data in tests - c.SetDebugLog(false) - } - return c, nil -} - -// getTestConnectionWithDSN takes environment variables to establish a connection to a real -// SMTP server to test all functionality that requires a connection. It also enables DSN -func getTestConnectionWithDSN(auth bool) (*Client, error) { - if os.Getenv("TEST_SKIP_ONLINE") != "" { - return nil, fmt.Errorf("env variable TEST_SKIP_ONLINE is set. Skipping online tests") - } - th := os.Getenv("TEST_HOST") - if th == "" { - return nil, fmt.Errorf("no TEST_HOST set") - } - tp := 25 - if tps := os.Getenv("TEST_PORT"); tps != "" { - tpi, err := strconv.Atoi(tps) - if err == nil { - tp = tpi - } - } - c, err := NewClient(th, WithDSN(), WithPort(tp)) - if err != nil { - return c, err - } - if auth { - st := os.Getenv("TEST_SMTPAUTH_TYPE") - if st != "" { - c.SetSMTPAuth(SMTPAuthType(st)) - } - u := os.Getenv("TEST_SMTPAUTH_USER") - if u != "" { - c.SetUsername(u) - } - p := os.Getenv("TEST_SMTPAUTH_PASS") - if p != "" { - c.SetPassword(p) - } - } - if err := c.DialWithContext(context.Background()); err != nil { - return c, fmt.Errorf("connection to test server failed: %w", err) - } - if err := c.Close(); err != nil { - return c, fmt.Errorf("disconnect from test server failed: %w", err) - } - return c, nil -} - -func TestXOAuth2OK(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - serverPort := TestServerPortBase + 30 - featureSet := "250-AUTH XOAUTH2\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" - go func() { - if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 500) - - c, err := NewClient("127.0.0.1", - WithPort(serverPort), - WithTLSPortPolicy(TLSOpportunistic), - WithSMTPAuth(SMTPAuthXOAUTH2), - WithUsername("user"), - WithPassword("token")) - if err != nil { - t.Fatalf("unable to create new client: %v", err) - } - if err = c.DialWithContext(context.Background()); err != nil { - t.Fatalf("unexpected dial error: %v", err) - } - if err = c.Close(); err != nil { - t.Fatalf("disconnect from test server failed: %v", err) - } -} - -func TestXOAuth2Unsupported(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - serverPort := TestServerPortBase + 31 - featureSet := "250-AUTH LOGIN PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" - go func() { - if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 500) - - c, err := NewClient("127.0.0.1", - WithPort(serverPort), - WithTLSPolicy(TLSOpportunistic), - WithSMTPAuth(SMTPAuthXOAUTH2), - WithUsername("user"), - WithPassword("token")) - if err != nil { - t.Fatalf("unable to create new client: %v", err) - } - if err = c.DialWithContext(context.Background()); err == nil { - t.Fatal("expected dial error got nil") - } else { - if !errors.Is(err, ErrXOauth2AuthNotSupported) { - t.Fatalf("expected %v; got %v", ErrXOauth2AuthNotSupported, err) - } - } - if err = c.Close(); err != nil { - t.Fatalf("disconnect from test server failed: %v", err) - } -} - -func TestXOAuth2OK_faker(t *testing.T) { - server := []string{ - "220 Fake server ready ESMTP", - "250-fake.server", - "250-AUTH LOGIN XOAUTH2", - "250 8BITMIME", - "250 OK", - "235 2.7.0 Accepted", - "221 OK", - } - var wrote strings.Builder - var fake faker - fake.ReadWriter = struct { - io.Reader - io.Writer - }{ - strings.NewReader(strings.Join(server, "\r\n")), - &wrote, - } - c, err := NewClient("fake.host", - WithDialContextFunc(getFakeDialFunc(fake)), - WithTLSPortPolicy(TLSOpportunistic), - WithSMTPAuth(SMTPAuthXOAUTH2), - WithUsername("user"), - WithPassword("token")) - if err != nil { - t.Fatalf("unable to create new client: %v", err) - } - if err = c.DialWithContext(context.Background()); err != nil { - t.Fatalf("unexpected dial error: %v", err) - } - if err = c.Close(); err != nil { - t.Fatalf("disconnect from test server failed: %v", err) - } - if !strings.Contains(wrote.String(), "AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n") { - t.Fatalf("got %q; want AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n", wrote.String()) - } -} - -func TestXOAuth2Unsupported_faker(t *testing.T) { - server := []string{ - "220 Fake server ready ESMTP", - "250-fake.server", - "250-AUTH LOGIN PLAIN", - "250 8BITMIME", - "250 OK", - "221 OK", - } - var wrote strings.Builder - var fake faker - fake.ReadWriter = struct { - io.Reader - io.Writer - }{ - strings.NewReader(strings.Join(server, "\r\n")), - &wrote, - } - c, err := NewClient("fake.host", - WithDialContextFunc(getFakeDialFunc(fake)), - WithTLSPortPolicy(TLSOpportunistic), - WithSMTPAuth(SMTPAuthXOAUTH2)) - if err != nil { - t.Fatalf("unable to create new client: %v", err) - } - if err = c.DialWithContext(context.Background()); err == nil { - t.Fatal("expected dial error got nil") - } else { - if !errors.Is(err, ErrXOauth2AuthNotSupported) { - t.Fatalf("expected %v; got %v", ErrXOauth2AuthNotSupported, err) - } - } - if err = c.Close(); err != nil { - t.Fatalf("disconnect from test server failed: %v", err) - } - client := strings.Split(wrote.String(), "\r\n") - if len(client) != 4 { - t.Fatalf("unexpected number of client requests got %d; want 5", len(client)) - } - if !strings.HasPrefix(client[0], "EHLO") { - t.Fatalf("expected EHLO, got %q", client[0]) - } - if client[1] != "NOOP" { - t.Fatalf("expected NOOP, got %q", client[1]) - } - if client[2] != "QUIT" { - t.Fatalf("expected QUIT, got %q", client[3]) - } + } + if err = c.Close(); err != nil { + t.Fatalf("disconnect from test server failed: %v", err) + } + client := strings.Split(wrote.String(), "\r\n") + if len(client) != 3 { + t.Fatalf("unexpected number of client requests got %d; want 3", len(client)) + } + if !strings.HasPrefix(client[0], "EHLO") { + t.Fatalf("expected EHLO, got %q", client[0]) + } + if client[1] != "QUIT" { + t.Fatalf("expected QUIT, got %q", client[3]) + } + }) } +// getFakeDialFunc returns a DialContextFunc that always returns the given net.Conn without establishing a +// real network connection. func getFakeDialFunc(conn net.Conn) DialContextFunc { return func(ctx context.Context, network, address string) (net.Conn, error) { return conn, nil } } +// faker is an internal structure that embeds io.ReadWriter to simulate network read/write operations. type faker struct { io.ReadWriter } @@ -4578,7 +3339,6 @@ func (f faker) RemoteAddr() net.Addr { return nil } func (f faker) SetDeadline(time.Time) error { return nil } func (f faker) SetReadDeadline(time.Time) error { return nil } func (f faker) SetWriteDeadline(time.Time) error { return nil } -*/ // parseJSONLog parses a JSON encoded log from the provided buffer and returns a slice of logLine structs. // In case of a decode error, it reports the error to the testing framework. From 127cfdf2bcde0fbce7a1ebd0ba77abe6dfbc8ca2 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 24 Oct 2024 13:20:09 +0200 Subject: [PATCH 046/188] 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. --- eml.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eml.go b/eml.go index 17077a3..e150068 100644 --- a/eml.go +++ b/eml.go @@ -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) } From 887e3cd768ec89aa5cb679fcdb37e44c08dc9432 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 24 Oct 2024 13:36:03 +0200 Subject: [PATCH 047/188] 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. --- eml_test.go | 136 +++++++++++++++--- testdata/RFC5322-A1-1-invalid-from.eml | 8 ++ .../RFC5322-A1-1-invalid-from.eml.license | 3 + testdata/RFC5322-A1-1.eml | 8 ++ testdata/RFC5322-A1-1.eml.license | 3 + 5 files changed, 135 insertions(+), 23 deletions(-) create mode 100644 testdata/RFC5322-A1-1-invalid-from.eml create mode 100644 testdata/RFC5322-A1-1-invalid-from.eml.license create mode 100644 testdata/RFC5322-A1-1.eml create mode 100644 testdata/RFC5322-A1-1.eml.license diff --git a/eml_test.go b/eml_test.go index 44bfb54..1a2cbb2 100644 --- a/eml_test.go +++ b/eml_test.go @@ -6,11 +6,8 @@ package mail import ( "bytes" - "fmt" - "os" "strings" "testing" - "time" ) const ( @@ -22,6 +19,14 @@ 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 +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 @@ -614,6 +619,108 @@ VGhpcyBpcyBhIHRlc3QgaW4gQmFzZTY0 --------------26A45336F6C6196BD8BBA2A2--` ) +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") + } + }) +} + +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 +728,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 +1096,6 @@ func stringToTempFile(data, name string) (string, string, error) { } return tempDir, filePath, nil } + + +*/ diff --git a/testdata/RFC5322-A1-1-invalid-from.eml b/testdata/RFC5322-A1-1-invalid-from.eml new file mode 100644 index 0000000..48a1248 --- /dev/null +++ b/testdata/RFC5322-A1-1-invalid-from.eml @@ -0,0 +1,8 @@ +From: §§§§§§§§ +To: Mary Smith +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". diff --git a/testdata/RFC5322-A1-1-invalid-from.eml.license b/testdata/RFC5322-A1-1-invalid-from.eml.license new file mode 100644 index 0000000..615ea2b --- /dev/null +++ b/testdata/RFC5322-A1-1-invalid-from.eml.license @@ -0,0 +1,3 @@ +// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors +// +// SPDX-License-Identifier: MIT diff --git a/testdata/RFC5322-A1-1.eml b/testdata/RFC5322-A1-1.eml new file mode 100644 index 0000000..adb1a85 --- /dev/null +++ b/testdata/RFC5322-A1-1.eml @@ -0,0 +1,8 @@ +From: John Doe +To: Mary Smith +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". \ No newline at end of file diff --git a/testdata/RFC5322-A1-1.eml.license b/testdata/RFC5322-A1-1.eml.license new file mode 100644 index 0000000..615ea2b --- /dev/null +++ b/testdata/RFC5322-A1-1.eml.license @@ -0,0 +1,3 @@ +// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors +// +// SPDX-License-Identifier: MIT From 9f1e1976fe210347adbf8a291a1f8f0cb22c5043 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 24 Oct 2024 14:49:24 +0200 Subject: [PATCH 048/188] 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. --- eml.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eml.go b/eml.go index e150068..61a7151 100644 --- a/eml.go +++ b/eml.go @@ -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) } From 769783f0375e928fe43e4c6c594dc951d9ad9f67 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 24 Oct 2024 16:42:00 +0200 Subject: [PATCH 049/188] 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. --- eml.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/eml.go b/eml.go index 61a7151..0363d89 100644 --- a/eml.go +++ b/eml.go @@ -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 } From 75e035c7832634271d4acffdaa3647085e827b45 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 24 Oct 2024 16:42:11 +0200 Subject: [PATCH 050/188] 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. --- eml_test.go | 452 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 452 insertions(+) diff --git a/eml_test.go b/eml_test.go index 1a2cbb2..b00098c 100644 --- a/eml_test.go +++ b/eml_test.go @@ -27,6 +27,15 @@ 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 +To: Mary Smith +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 @@ -47,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" +To: +Cc: +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" +To: +Cc: +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 @@ -309,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" +To: +Cc: +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" +To: +Cc: +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" +To: +Cc: +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" +To: +Cc: +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 @@ -583,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" +To: +Cc: +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 @@ -617,6 +827,68 @@ 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" +To: +Content-Type: multipart/related; boundary="abc123" + +--abc123 +Content-Type: text/html; charset=UTF-8 +Content-Transfer-Encoding: quoted-printable + + + +

Hello,

+

This is an example email with an inline image:

+ Inline Image +

Best regards,
The go-mail team

+ + +--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" +To: +Content-Type: multipart/related; boundary="abc123" + +--abc123 +Content-Type: text/html; charset=UTF-8 +Content-Transfer-Encoding: quoted-printable + + + +

Hello,

+

This is an example email with an inline image:

+ Inline Image +

Best regards,
The go-mail team

+ + +--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) { @@ -683,6 +955,186 @@ func TestEMLToMsgFromReader(t *testing.T) { 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) { From 9834c6508d39b985fa8faf9b5d432883ec7aa40c Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 24 Oct 2024 17:09:55 +0200 Subject: [PATCH 051/188] 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. --- file_test.go | 252 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 153 insertions(+), 99 deletions(-) diff --git a/file_test.go b/file_test.go index 43b8cfe..9e23db5 100644 --- a/file_test.go +++ b/file_test.go @@ -6,108 +6,159 @@ 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) - } +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) + } + }) + } + }) } -// 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) { @@ -137,3 +188,6 @@ func TestFile_WithFileContentType(t *testing.T) { }) } } + + +*/ From 8353b4b255936fc9ad0401fdcf2e11d27e38d9a3 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 09:33:45 +0200 Subject: [PATCH 052/188] Follow upstream for HELO during Quit bug I reported the bug I fixed in https://github.com/wneessen/go-mail/commit/74fa3f6f62728cab1597bca5deea6ee758157e24 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. --- smtp/smtp.go | 17 +++-------------- smtp/smtp_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/smtp/smtp.go b/smtp/smtp.go index 63504fe..4841ec8 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -554,20 +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 we already tried to send a EHLO/HELO but it failed, we still need to be able to send - // a QUIT to close the connection. - // c.hello() will return the global helloErr of the Client, which will always be set if the HELO - // failed before. Therefore if we already sent a HELO and the error is not nil, we skip another - // EHLO/HELO try - c.mutex.RLock() - didHello := c.didHello - helloErr := c.helloError - c.mutex.RUnlock() - if !didHello || helloErr == nil { - 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 diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 737bd58..4fe0481 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -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") From eebbaa251391b0e710f131c60046e5ce53244be7 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 09:40:15 +0200 Subject: [PATCH 053/188] 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. --- file_test.go | 65 ++++++++++++++++++++++++---------------------------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/file_test.go b/file_test.go index 9e23db5..66ec84f 100644 --- a/file_test.go +++ b/file_test.go @@ -155,39 +155,34 @@ func TestFile(t *testing.T) { }) } }) + 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) + } + }) + } + }) } - -/* - - -// 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) - } - }) - } -} - - -*/ From c58d52e49a478aa71a090c301c992b7f8a48455e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 09:48:00 +0200 Subject: [PATCH 054/188] 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. --- header_test.go | 58 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/header_test.go b/header_test.go index a060ae6..832c63d 100644 --- a/header_test.go +++ b/header_test.go @@ -8,14 +8,13 @@ import ( "testing" ) -// TestImportance_StringFuncs tests the different string method of the Importance object -func TestImportance_StringFuncs(t *testing.T) { +func TestImportance_Stringer(t *testing.T) { tests := []struct { - name string - imp Importance - wantns string - xprio string - want string + name string + imp Importance + wantnum string + xprio string + want string }{ {"Importance: Non-Urgent", ImportanceNonUrgent, "0", "5", "non-urgent"}, {"Importance: Low", ImportanceLow, "0", "5", "low"}, @@ -24,22 +23,35 @@ func TestImportance_StringFuncs(t *testing.T) { {"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()) - } - }) - } + 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()) + } + }) + } + }) } // TestAddrHeader_String tests the string method of the AddrHeader object From 64cfbf9e46637ae8d57a7cae33363a190f7df9a0 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 09:53:49 +0200 Subject: [PATCH 055/188] 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. --- header_test.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/header_test.go b/header_test.go index 832c63d..bfe1d12 100644 --- a/header_test.go +++ b/header_test.go @@ -54,8 +54,7 @@ func TestImportance_Stringer(t *testing.T) { }) } -// TestAddrHeader_String tests the string method of the AddrHeader object -func TestAddrHeader_String(t *testing.T) { +func TestAddrHeader_Stringer(t *testing.T) { tests := []struct { name string ah AddrHeader @@ -76,13 +75,13 @@ func TestAddrHeader_String(t *testing.T) { } } -// TestHeader_String tests the string method of the Header object -func TestHeader_String(t *testing.T) { +func TestHeader_Stringer(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"}, @@ -90,6 +89,8 @@ 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"}, @@ -99,10 +100,11 @@ 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"}, From c4946af3ab924a5041e24936905a21f900d15f6b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 11:08:11 +0200 Subject: [PATCH 056/188] 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. --- msg_test.go | 6575 +++++++++++++++++++++++++++------------------------ 1 file changed, 3421 insertions(+), 3154 deletions(-) diff --git a/msg_test.go b/msg_test.go index 7fcbd99..1d4cf58 100644 --- a/msg_test.go +++ b/msg_test.go @@ -5,44 +5,241 @@ package mail import ( - "bufio" - "bytes" "embed" - "errors" "fmt" - htpl "html/template" - "io" - "net/mail" - "os" - "sort" + "reflect" "strings" "testing" - ttpl "text/template" - "time" ) //go:embed README.md var efs embed.FS -// TestNewMsg tests the NewMsg method +/* +addrHeader: make(map[AddrHeader][]*mail.Address), +charset: CharsetUTF8, +encoding: EncodingQP, +genHeader: make(map[Header][]string), +preformHeader: make(map[Header]string), +mimever: MIME10, +*/ func TestNewMsg(t *testing.T) { - m := NewMsg() - var err error - if m.encoding != EncodingQP { - err = fmt.Errorf("default encoding is not Quoted-Prinable") - } - if m.charset != CharsetUTF8 { - err = fmt.Errorf("default charset is not UTF-8") - } + t.Run("create new message", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if message.addrHeader == nil { + t.Errorf("address header map is nil") + } + if message.genHeader == nil { + t.Errorf("generic header map is nil") + } + if message.preformHeader == nil { + t.Errorf("preformatted header map is nil") + } + if message.charset != CharsetUTF8 { + t.Errorf("default charset for new Msg mismatch. Expected: %s, got: %s", CharsetUTF8, + message.charset) + } + if message.encoding != EncodingQP { + t.Errorf("default encoding for new Msg mismatch. Expected: %s, got: %s", EncodingQP, + message.encoding) + } + if message.mimever != MIME10 { + t.Errorf("default MIME version for new Msg mismatch. Expected: %s, got: %s", MIME10, + message.mimever) + } + if reflect.TypeOf(message.encoder).String() != "mime.WordEncoder" { + t.Errorf("default encoder for new Msg mismatch. Expected: %s, got: %s", "mime.WordEncoder", + reflect.TypeOf(message.encoder).String()) + } + if !strings.EqualFold(message.encoder.Encode(message.charset.String(), "ab12§$/"), + `=?UTF-8?q?ab12=C2=A7$/?=`) { + t.Errorf("default encoder for new Msg mismatch. QP encoded expected string: %s, got: %s", + `=?UTF-8?q?ab12=C2=A7$/?=`, message.encoder.Encode(message.charset.String(), "ab12§$/")) + } + }) + t.Run("new message with nil option", func(t *testing.T) { + message := NewMsg(nil) + if message == nil { + t.Fatal("message is nil") + } + }) + t.Run("new message with custom charsets", func(t *testing.T) { + tests := []struct { + name string + value Charset + want Charset + }{ + {"charset is UTF-7", CharsetUTF7, "UTF-7"}, + {"charset is UTF-8", CharsetUTF8, "UTF-8"}, + {"charset is US-ASCII", CharsetASCII, "US-ASCII"}, + {"charset is ISO-8859-1", CharsetISO88591, "ISO-8859-1"}, + {"charset is ISO-8859-2", CharsetISO88592, "ISO-8859-2"}, + {"charset is ISO-8859-3", CharsetISO88593, "ISO-8859-3"}, + {"charset is ISO-8859-4", CharsetISO88594, "ISO-8859-4"}, + {"charset is ISO-8859-5", CharsetISO88595, "ISO-8859-5"}, + {"charset is ISO-8859-6", CharsetISO88596, "ISO-8859-6"}, + {"charset is ISO-8859-7", CharsetISO88597, "ISO-8859-7"}, + {"charset is ISO-8859-9", CharsetISO88599, "ISO-8859-9"}, + {"charset is ISO-8859-13", CharsetISO885913, "ISO-8859-13"}, + {"charset is ISO-8859-14", CharsetISO885914, "ISO-8859-14"}, + {"charset is ISO-8859-15", CharsetISO885915, "ISO-8859-15"}, + {"charset is ISO-8859-16", CharsetISO885916, "ISO-8859-16"}, + {"charset is ISO-2022-JP", CharsetISO2022JP, "ISO-2022-JP"}, + {"charset is ISO-2022-KR", CharsetISO2022KR, "ISO-2022-KR"}, + {"charset is windows-1250", CharsetWindows1250, "windows-1250"}, + {"charset is windows-1251", CharsetWindows1251, "windows-1251"}, + {"charset is windows-1252", CharsetWindows1252, "windows-1252"}, + {"charset is windows-1255", CharsetWindows1255, "windows-1255"}, + {"charset is windows-1256", CharsetWindows1256, "windows-1256"}, + {"charset is KOI8-R", CharsetKOI8R, "KOI8-R"}, + {"charset is KOI8-U", CharsetKOI8U, "KOI8-U"}, + {"charset is Big5", CharsetBig5, "Big5"}, + {"charset is GB18030", CharsetGB18030, "GB18030"}, + {"charset is GB2312", CharsetGB2312, "GB2312"}, + {"charset is TIS-620", CharsetTIS620, "TIS-620"}, + {"charset is EUC-KR", CharsetEUCKR, "EUC-KR"}, + {"charset is Shift_JIS", CharsetShiftJIS, "Shift_JIS"}, + {"charset is GBK", CharsetGBK, "GBK"}, + {"charset is Unknown", CharsetUnknown, "Unknown"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg(WithCharset(tt.value), nil) + if message == nil { + t.Fatal("message is nil") + } + if message.charset != tt.want { + t.Fatalf("NewMsg(WithCharset(%s)) failed. Expected charset: %s, got: %s", tt.value, tt.want, + message.charset) + } + }) + } + }) + t.Run("new message with custom encoding", func(t *testing.T) { + tests := []struct { + name string + value Encoding + want Encoding + }{ + {"encoding is Quoted-Printable", EncodingQP, "quoted-printable"}, + {"encoding is Base64", EncodingB64, "base64"}, + {"encoding is Unencoded 8-Bit", NoEncoding, "8bit"}, + {"encoding is US-ASCII 7-Bit", EncodingUSASCII, "7bit"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg(WithEncoding(tt.value), nil) + if message == nil { + t.Fatal("message is nil") + } + if message.encoding != tt.want { + t.Errorf("NewMsg(WithEncoding(%s)) failed. Expected encoding: %s, got: %s", tt.value, + tt.want, message.encoding) + } + }) + } + }) + t.Run("new message with custom MIME version", func(t *testing.T) { + tests := []struct { + name string + value MIMEVersion + want MIMEVersion + }{ + {"MIME version: 1.0", MIME10, "1.0"}, + {"MIME version: 1.1", MIMEVersion("1.1"), "1.1"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg(WithMIMEVersion(tt.value)) + if message == nil { + t.Fatal("message is nil") + } + if message.mimever != tt.want { + t.Errorf("NewMsg(WithMIMEVersion(%s)) failed. Expected MIME version: %s, got: %s", + tt.value, tt.want, message.mimever) + } + }) + } + }) + t.Run("new message with custom boundary", func(t *testing.T) { + tests := []struct { + name string + value string + }{ + {"boundary: test123", "test123"}, + {"boundary is empty", ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg(WithBoundary(tt.value)) + if message == nil { + t.Fatal("message is nil") + } + if message.boundary != tt.value { + t.Errorf("NewMsg(WithBoundary(%s)) failed. Expected boundary: %s, got: %s", tt.value, + tt.value, message.boundary) + } + }) + } + }) + t.Run("new message with custom PGP type", func(t *testing.T) { + tests := []struct { + name string + value PGPType + }{ + {"Not a PGP encoded message", NoPGP}, + {"PGP encrypted message", PGPEncrypt}, + {"PGP signed message", PGPSignature}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg(WithPGPType(tt.value)) + if message == nil { + t.Fatal("message is nil") + } + if message.pgptype != tt.value { + t.Errorf("NewMsg(WithPGPType(%d)) failed. Expected PGP type: %d, got: %d", tt.value, + tt.value, message.pgptype) + } + }) + } + }) + t.Run("new message with middleware: uppercase", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if len(message.middlewares) != 0 { + t.Errorf("NewMsg() failed. Expected empty middlewares, got: %d", len(message.middlewares)) + } + message = NewMsg(WithMiddleware(uppercaseMiddleware{})) + if len(message.middlewares) != 1 { + t.Errorf("NewMsg(WithMiddleware(uppercaseMiddleware{})) failed. Expected 1 middleware, got: %d", + len(message.middlewares)) + } + message = NewMsg(WithMiddleware(uppercaseMiddleware{}), WithMiddleware(encodeMiddleware{})) + if len(message.middlewares) != 2 { + t.Errorf("NewMsg(WithMiddleware(uppercaseMiddleware{}),WithMiddleware(encodeMiddleware{})) "+ + "failed. Expected 2 middleware, got: %d", len(message.middlewares)) + } + }) + t.Run("new message without default user-agent", func(t *testing.T) { + message := NewMsg(WithNoDefaultUserAgent()) + if message == nil { + t.Fatal("message is nil") + } + if !message.noDefaultUserAgent { + t.Errorf("NewMsg(WithNoDefaultUserAgent()) failed. Expected noDefaultUserAgent to be true, got: %t", + message.noDefaultUserAgent) + } - if err != nil { - t.Errorf("NewMsg() failed: %s", err) - return - } + }) } -// TestNewMsgCharset tests WithCharset and Msg.SetCharset -func TestNewMsgCharset(t *testing.T) { +func TestMsg_SetCharset(t *testing.T) { tests := []struct { name string value Charset @@ -81,27 +278,37 @@ func TestNewMsgCharset(t *testing.T) { {"charset is GBK", CharsetGBK, "GBK"}, {"charset is Unknown", CharsetUnknown, "Unknown"}, } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m := NewMsg(WithCharset(tt.value), nil) - if m.charset != tt.want { - t.Errorf("WithCharset() failed. Expected: %s, got: %s", tt.want, m.charset) - } - m.SetCharset(CharsetUTF8) - if m.charset != CharsetUTF8 { - t.Errorf("SetCharset() failed. Expected: %s, got: %s", CharsetUTF8, m.charset) - } - m.SetCharset(tt.value) - if m.charset != tt.want { - t.Errorf("SetCharset() failed. Expected: %s, got: %s", tt.want, m.charset) - } - }) - } + t.Run("SetCharset on new message", func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetCharset(tt.value) + if message.charset != tt.want { + t.Errorf("failed to set charset. Expected: %s, got: %s", tt.want, message.charset) + } + }) + } + }) + t.Run("SetCharset to override WithCharset", func(t *testing.T) { + message := NewMsg(WithCharset(CharsetUTF7)) + if message == nil { + t.Fatal("message is nil") + } + if message.charset != CharsetUTF7 { + t.Errorf("failed to set charset on message creation. Expected: %s, got: %s", CharsetUTF7, + message.charset) + } + message.SetCharset(CharsetUTF8) + if message.charset != CharsetUTF8 { + t.Errorf("failed to set charset. Expected: %s, got: %s", CharsetUTF8, message.charset) + } + }) } -// TestNewMsgWithCharset tests WithEncoding and Msg.SetEncoding -func TestNewMsgWithEncoding(t *testing.T) { +func TestMsg_SetEncoding(t *testing.T) { tests := []struct { name string value Encoding @@ -110,105 +317,3194 @@ func TestNewMsgWithEncoding(t *testing.T) { {"encoding is Quoted-Printable", EncodingQP, "quoted-printable"}, {"encoding is Base64", EncodingB64, "base64"}, {"encoding is Unencoded 8-Bit", NoEncoding, "8bit"}, + {"encoding is US-ASCII 7-Bit", EncodingUSASCII, "7bit"}, } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m := NewMsg(WithEncoding(tt.value)) - if m.encoding != tt.want { - t.Errorf("WithEncoding() failed. Expected: %s, got: %s", tt.want, m.encoding) - } - m.SetEncoding(NoEncoding) - if m.encoding != NoEncoding { - t.Errorf("SetEncoding() failed. Expected: %s, got: %s", NoEncoding, m.encoding) - } - m.SetEncoding(tt.want) - if m.encoding != tt.want { - t.Errorf("SetEncoding() failed. Expected: %s, got: %s", tt.want, m.encoding) - } - }) - } + t.Run("SetEncoding on new message", func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetEncoding(tt.value) + if message.encoding != tt.want { + t.Errorf("failed to set encoding. Expected: %s, got: %s", tt.want, message.encoding) + } + }) + } + }) + t.Run("SetEncoding to override WithEncoding", func(t *testing.T) { + message := NewMsg(WithEncoding(EncodingUSASCII)) + if message == nil { + t.Fatal("message is nil") + } + if message.encoding != EncodingUSASCII { + t.Errorf("failed to set encoding on message creation. Expected: %s, got: %s", EncodingUSASCII, + message.encoding) + } + message.SetEncoding(EncodingB64) + if message.encoding != EncodingB64 { + t.Errorf("failed to set encoding. Expected: %s, got: %s", EncodingB64, message.encoding) + } + }) } -// TestNewMsgWithMIMEVersion tests WithMIMEVersion and Msg.SetMIMEVersion -func TestNewMsgWithMIMEVersion(t *testing.T) { - tests := []struct { - name string - value MIMEVersion - want MIMEVersion - }{ - {"MIME version is 1.0", MIME10, "1.0"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m := NewMsg(WithMIMEVersion(tt.value)) - if m.mimever != tt.want { - t.Errorf("WithMIMEVersion() failed. Expected: %s, got: %s", tt.want, m.mimever) - } - m.mimever = "" - m.SetMIMEVersion(tt.value) - if m.mimever != tt.want { - t.Errorf("SetMIMEVersion() failed. Expected: %s, got: %s", tt.want, m.mimever) - } - }) - } -} - -// TestNewMsgWithBoundary tests WithBoundary and Msg.SetBoundary -func TestNewMsgWithBoundary(t *testing.T) { +func TestMsg_SetBoundary(t *testing.T) { tests := []struct { name string value string }{ - {"boundary is test123", "test123"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m := NewMsg(WithBoundary(tt.value)) - if m.boundary != tt.value { - t.Errorf("WithBoundary() failed. Expected: %s, got: %s", tt.value, m.boundary) - } - m.boundary = "" - m.SetBoundary(tt.value) - if m.boundary != tt.value { - t.Errorf("SetBoundary() failed. Expected: %s, got: %s", tt.value, m.boundary) - } - }) + {"boundary: test123", "test123"}, + {"boundary is empty", ""}, } + t.Run("SetBoundary on new message", func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetBoundary(tt.value) + if message.boundary != tt.value { + t.Errorf("failed to set boundary. Expected: %s, got: %s", tt.value, message.boundary) + } + }) + } + }) + t.Run("SetBoundary to override WithBoundary", func(t *testing.T) { + message := NewMsg(WithBoundary("123Test")) + if message == nil { + t.Fatal("message is nil") + } + if message.boundary != "123Test" { + t.Errorf("failed to set boundary on message creation. Expected: %s, got: %s", "123Test", + message.boundary) + } + message.SetBoundary("test123") + if message.boundary != "test123" { + t.Errorf("failed to set boundary. Expected: %s, got: %s", "test123", message.boundary) + } + }) } -// TestNewMsg_WithPGPType tests WithPGPType option -func TestNewMsg_WithPGPType(t *testing.T) { - tests := []struct { - name string - pt PGPType - hpt bool - }{ - {"Not a PGP encoded message", NoPGP, false}, - {"PGP encrypted message", PGPEncrypt, true}, - {"PGP signed message", PGPSignature, true}, +/* +// TestNewMsgWithMiddleware tests WithMiddleware + + func TestNewMsgWithMiddleware(t *testing.T) { + m := NewMsg() + if len(m.middlewares) != 0 { + t.Errorf("empty middlewares failed. m.middlewares expected to be: empty, got: %d middleware", len(m.middlewares)) + } + m = NewMsg(WithMiddleware(uppercaseMiddleware{})) + if len(m.middlewares) != 1 { + t.Errorf("empty middlewares failed. m.middlewares expected to be: 1, got: %d middleware", len(m.middlewares)) + } + m = NewMsg(WithMiddleware(uppercaseMiddleware{}), WithMiddleware(encodeMiddleware{})) + if len(m.middlewares) != 2 { + t.Errorf("empty middlewares failed. m.middlewares expected to be: 2, got: %d middleware", len(m.middlewares)) + } } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m := NewMsg(WithPGPType(tt.pt)) - if m.pgptype != tt.pt { - t.Errorf("WithPGPType() failed. Expected: %d, got: %d", tt.pt, m.pgptype) +// TestApplyMiddlewares tests the applyMiddlewares for the Msg object + + func TestApplyMiddlewares(t *testing.T) { + tests := []struct { + name string + sub string + want string + }{ + {"normal subject", "This is a test subject", "THIS IS @ TEST SUBJECT"}, + {"subject with one middleware effect", "This is test subject", "THIS IS TEST SUBJECT"}, + {"subject with one middleware effect", "This is A test subject", "THIS IS A TEST SUBJECT"}, + } + m := NewMsg(WithMiddleware(encodeMiddleware{}), WithMiddleware(uppercaseMiddleware{})) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m.Subject(tt.sub) + if m.genHeader[HeaderSubject] == nil { + t.Errorf("Subject() method failed in applyMiddlewares() test. Generic header for subject is empty") + return + } + m = m.applyMiddlewares(m) + s, ok := m.genHeader[HeaderSubject] + if !ok { + t.Errorf("failed to get subject header") + } + if s[0] != tt.want { + t.Errorf("applyMiddlewares() method failed. Expected: %s, got: %s", tt.want, s[0]) + } + }) + } + } + +// TestMsg_SetGenHeader tests Msg.SetGenHeader + + func TestMsg_SetGenHeader(t *testing.T) { + tests := []struct { + name string + header Header + values []string + }{ + {"set subject", HeaderSubject, []string{"This is Subject"}}, + {"set content-language", HeaderContentLang, []string{"en", "de", "fr", "es"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := NewMsg() + m.SetGenHeader(tt.header, tt.values...) + if m.genHeader[tt.header] == nil { + t.Errorf("SetGenHeader() failed. Tried to set header %s, but it is empty", tt.header) + return + } + for _, v := range tt.values { + found := false + for _, hv := range m.genHeader[tt.header] { + if hv == v { + found = true + } + } + if !found { + t.Errorf("SetGenHeader() failed. Value %s not found in header field", v) + } + } + }) + } + } + +// TestMsg_SetGenHeaderPreformatted tests Msg.SetGenHeaderPreformatted + + func TestMsg_SetGenHeaderPreformatted(t *testing.T) { + tests := []struct { + name string + header Header + value string + }{ + {"set subject", HeaderSubject, "This is Subject"}, + {"set content-language", HeaderContentLang, fmt.Sprintf("%s, %s, %s, %s", + "en", "de", "fr", "es")}, + {"set subject with newline", HeaderSubject, "This is Subject\r\n with 2nd line"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &Msg{} + m.SetGenHeaderPreformatted(tt.header, tt.value) + m = NewMsg() + m.SetGenHeaderPreformatted(tt.header, tt.value) + if m.preformHeader[tt.header] == "" { + t.Errorf("SetGenHeaderPreformatted() failed. Tried to set header %s, but it is empty", tt.header) + } + if m.preformHeader[tt.header] != tt.value { + t.Errorf("SetGenHeaderPreformatted() failed. Expected: %q, got: %q", tt.value, + m.preformHeader[tt.header]) + } + buf := bytes.Buffer{} + _, err := m.WriteTo(&buf) + if err != nil { + t.Errorf("failed to write message to memory: %s", err) + return + } + if !strings.Contains(buf.String(), fmt.Sprintf("%s: %s%s", tt.header, tt.value, SingleNewLine)) { + t.Errorf("SetGenHeaderPreformatted() failed. Unable to find correctly formated header in " + + "mail message output") + } + }) + } + } + +// TestMsg_AddTo tests the Msg.AddTo method + + func TestMsg_AddTo(t *testing.T) { + a := []string{"address1@example.com", "address2@example.com"} + na := "address3@example.com" + m := NewMsg() + if err := m.To(a...); err != nil { + t.Errorf("failed to set TO addresses: %s", err) + return + } + if err := m.AddTo(na); err != nil { + t.Errorf("AddTo failed: %s", err) + return + } + + atf := false + for _, v := range m.addrHeader[HeaderTo] { + if v.Address == na { + atf = true } - m.pgptype = 99 - m.SetPGPType(tt.pt) - if m.pgptype != tt.pt { - t.Errorf("SetPGPType() failed. Expected: %d, got: %d", tt.pt, m.pgptype) + } + if !atf { + t.Errorf("AddTo() failed. Address %q not found in TO address slice.", na) + } + } + +// TestMsg_From tests the Msg.From and Msg.GetSender methods + + func TestMsg_From(t *testing.T) { + a := "toni@example.com" + n := "Toni Tester" + na := fmt.Sprintf(`"%s" <%s>`, n, a) + m := NewMsg() + + _, err := m.GetSender(false) + if err == nil { + t.Errorf("GetSender(false) without a set From address succeeded but was expected to fail") + return + } + + if err := m.From(na); err != nil { + t.Errorf("failed to set FROM addresses: %s", err) + return + } + gs, err := m.GetSender(false) + if err != nil { + t.Errorf("GetSender(false) failed: %s", err) + return + } + if gs != a { + t.Errorf("From() failed. Expected: %s, got: %s", a, gs) + return + } + + gs, err = m.GetSender(true) + if err != nil { + t.Errorf("GetSender(true) failed: %s", err) + return + } + if gs != na { + t.Errorf("From() failed. Expected: %s, got: %s", na, gs) + return + } + } + +// TestMsg_EnvelopeFrom tests the Msg.EnvelopeFrom and Msg.GetSender methods + + func TestMsg_EnvelopeFrom(t *testing.T) { + e := "envelope@example.com" + a := "toni@example.com" + n := "Toni Tester" + na := fmt.Sprintf(`"%s" <%s>`, n, a) + ne := fmt.Sprintf(`<%s>`, e) + m := NewMsg() + + _, err := m.GetSender(false) + if err == nil { + t.Errorf("GetSender(false) without a set envelope From address succeeded but was expected to fail") + return + } + + if err := m.EnvelopeFrom(e); err != nil { + t.Errorf("failed to set envelope FROM addresses: %s", err) + return + } + gs, err := m.GetSender(false) + if err != nil { + t.Errorf("GetSender(false) failed: %s", err) + return + } + if gs != e { + t.Errorf("From() failed. Expected: %s, got: %s", e, gs) + return + } + + if err := m.From(na); err != nil { + t.Errorf("failed to set FROM addresses: %s", err) + return + } + gs, err = m.GetSender(false) + if err != nil { + t.Errorf("GetSender(false) failed: %s", err) + return + } + if gs != e { + t.Errorf("From() failed. Expected: %s, got: %s", e, gs) + return + } + + gs, err = m.GetSender(true) + if err != nil { + t.Errorf("GetSender(true) failed: %s", err) + return + } + if gs != ne { + t.Errorf("From() failed. Expected: %s, got: %s", ne, gs) + return + } + m.Reset() + + if err := m.From(na); err != nil { + t.Errorf("failed to set FROM addresses: %s", err) + return + } + gs, err = m.GetSender(false) + if err != nil { + t.Errorf("GetSender(true) failed: %s", err) + return + } + if gs != a { + t.Errorf("From() failed. Expected: %s, got: %s", a, gs) + return + } + gs, err = m.GetSender(true) + if err != nil { + t.Errorf("GetSender(true) failed: %s", err) + return + } + if gs != na { + t.Errorf("From() failed. Expected: %s, got: %s", na, gs) + return + } + } + +// TestMsg_AddToFormat tests the Msg.AddToFormat method + + func TestMsg_AddToFormat(t *testing.T) { + a := []string{"address1@example.com", "address2@example.com"} + nn := "Toni Tester" + na := "address3@example.com" + w := `"Toni Tester" ` + m := NewMsg() + if err := m.To(a...); err != nil { + t.Errorf("failed to set TO addresses: %s", err) + return + } + if err := m.AddToFormat(nn, na); err != nil { + t.Errorf("AddToFormat failed: %s", err) + return + } + + atf := false + for _, v := range m.addrHeader[HeaderTo] { + if v.String() == w { + atf = true } - if m.hasPGPType() != tt.hpt { - t.Errorf("hasPGPType() failed. Expected %t, got: %t", tt.hpt, m.hasPGPType()) + } + if !atf { + t.Errorf("AddToFormat() failed. Address %q not found in TO address slice.", w) + } + } + +// TestMsg_ToIgnoreInvalid tests the Msg.ToIgnoreInvalid method + + func TestMsg_ToIgnoreInvalid(t *testing.T) { + a := []string{"address1@example.com", "address2@example.com"} + fa := []string{"address1@example.com", "address2@example.com", "failedaddress.com"} + m := NewMsg() + + m.ToIgnoreInvalid(a...) + l := len(m.addrHeader[HeaderTo]) + if l != len(a) { + t.Errorf("ToIgnoreInvalid() failed. Expected %d addresses, got: %d", len(a), l) + } + m.ToIgnoreInvalid(fa...) + l = len(m.addrHeader[HeaderTo]) + if l != len(fa)-1 { + t.Errorf("ToIgnoreInvalid() failed. Expected %d addresses, got: %d", len(fa)-1, l) + } + } + +// TestMsg_AddCc tests the Msg.AddCc method + + func TestMsg_AddCc(t *testing.T) { + a := []string{"address1@example.com", "address2@example.com"} + na := "address3@example.com" + m := NewMsg() + if err := m.Cc(a...); err != nil { + t.Errorf("failed to set CC addresses: %s", err) + return + } + if err := m.AddCc(na); err != nil { + t.Errorf("AddCc failed: %s", err) + return + } + + atf := false + for _, v := range m.addrHeader[HeaderCc] { + if v.Address == na { + atf = true } + } + if !atf { + t.Errorf("AddCc() failed. Address %q not found in CC address slice.", na) + } + } + +// TestMsg_AddCcFormat tests the Msg.AddCcFormat method + + func TestMsg_AddCcFormat(t *testing.T) { + a := []string{"address1@example.com", "address2@example.com"} + nn := "Toni Tester" + na := "address3@example.com" + w := `"Toni Tester" ` + m := NewMsg() + if err := m.Cc(a...); err != nil { + t.Errorf("failed to set CC addresses: %s", err) + return + } + if err := m.AddCcFormat(nn, na); err != nil { + t.Errorf("AddCcFormat failed: %s", err) + return + } + + atf := false + for _, v := range m.addrHeader[HeaderCc] { + if v.String() == w { + atf = true + } + } + if !atf { + t.Errorf("AddCcFormat() failed. Address %q not found in CC address slice.", w) + } + } + +// TestMsg_CcIgnoreInvalid tests the Msg.CcIgnoreInvalid method + + func TestMsg_CcIgnoreInvalid(t *testing.T) { + a := []string{"address1@example.com", "address2@example.com"} + fa := []string{"address1@example.com", "address2@example.com", "failedaddress.com"} + m := NewMsg() + + m.CcIgnoreInvalid(a...) + l := len(m.addrHeader[HeaderCc]) + if l != len(a) { + t.Errorf("CcIgnoreInvalid() failed. Expected %d addresses, got: %d", len(a), l) + } + m.CcIgnoreInvalid(fa...) + l = len(m.addrHeader[HeaderCc]) + if l != len(fa)-1 { + t.Errorf("CcIgnoreInvalid() failed. Expected %d addresses, got: %d", len(fa)-1, l) + } + } + +// TestMsg_AddBcc tests the Msg.AddBcc method + + func TestMsg_AddBcc(t *testing.T) { + a := []string{"address1@example.com", "address2@example.com"} + na := "address3@example.com" + m := NewMsg() + if err := m.Bcc(a...); err != nil { + t.Errorf("failed to set BCC addresses: %s", err) + return + } + if err := m.AddBcc(na); err != nil { + t.Errorf("AddBcc failed: %s", err) + return + } + + atf := false + for _, v := range m.addrHeader[HeaderBcc] { + if v.Address == na { + atf = true + } + } + if !atf { + t.Errorf("AddBcc() failed. Address %q not found in BCC address slice.", na) + } + } + +// TestMsg_AddBccFormat tests the Msg.AddBccFormat method + + func TestMsg_AddBccFormat(t *testing.T) { + a := []string{"address1@example.com", "address2@example.com"} + nn := "Toni Tester" + na := "address3@example.com" + w := `"Toni Tester" ` + m := NewMsg() + if err := m.Bcc(a...); err != nil { + t.Errorf("failed to set BCC addresses: %s", err) + return + } + if err := m.AddBccFormat(nn, na); err != nil { + t.Errorf("AddBccFormat failed: %s", err) + return + } + + atf := false + for _, v := range m.addrHeader[HeaderBcc] { + if v.String() == w { + atf = true + } + } + if !atf { + t.Errorf("AddBccFormat() failed. Address %q not found in BCC address slice.", w) + } + } + +// TestMsg_BccIgnoreInvalid tests the Msg.BccIgnoreInvalid method + + func TestMsg_BccIgnoreInvalid(t *testing.T) { + a := []string{"address1@example.com", "address2@example.com"} + fa := []string{"address1@example.com", "address2@example.com", "failedaddress.com"} + m := NewMsg() + + m.BccIgnoreInvalid(a...) + l := len(m.addrHeader[HeaderBcc]) + if l != len(a) { + t.Errorf("BccIgnoreInvalid() failed. Expected %d addresses, got: %d", len(a), l) + } + m.BccIgnoreInvalid(fa...) + l = len(m.addrHeader[HeaderBcc]) + if l != len(fa)-1 { + t.Errorf("BccIgnoreInvalid() failed. Expected %d addresses, got: %d", len(fa)-1, l) + } + } + +// TestMsg_SetBulk tests the Msg.SetBulk method + + func TestMsg_SetBulk(t *testing.T) { + m := NewMsg() + m.SetBulk() + if m.genHeader[HeaderPrecedence] == nil { + t.Errorf("SetBulk() failed. Precedence header is nil") + return + } + if m.genHeader[HeaderPrecedence][0] != "bulk" { + t.Errorf("SetBulk() failed. Expected Precedence header: %q, got: %q", "bulk", + m.genHeader[HeaderPrecedence][0]) + } + if m.genHeader[HeaderXAutoResponseSuppress] == nil { + t.Errorf("SetBulk() failed. X-Auto-Response-Suppress header is nil") + return + } + if m.genHeader[HeaderXAutoResponseSuppress][0] != "All" { + t.Errorf("SetBulk() failed. Expected X-Auto-Response-Suppress header: %q, got: %q", "All", + m.genHeader[HeaderXAutoResponseSuppress][0]) + } + } + +// TestMsg_SetDate tests the Msg.SetDate and Msg.SetDateWithValue method + + func TestMsg_SetDate(t *testing.T) { + m := NewMsg() + m.SetDate() + if m.genHeader[HeaderDate] == nil { + t.Errorf("SetDate() failed. Date header is nil") + return + } + d, ok := m.genHeader[HeaderDate] + if !ok { + t.Errorf("failed to get date header") + return + } + _, err := time.Parse(time.RFC1123Z, d[0]) + if err != nil { + t.Errorf("failed to parse time in date header: %s", err) + } + m.genHeader = nil + m.genHeader = make(map[Header][]string) + + now := time.Now() + m.SetDateWithValue(now) + if m.genHeader[HeaderDate] == nil { + t.Errorf("SetDateWithValue() failed. Date header is nil") + return + } + d, ok = m.genHeader[HeaderDate] + if !ok { + t.Errorf("failed to get date header") + return + } + pt, err := time.Parse(time.RFC1123Z, d[0]) + if err != nil { + t.Errorf("failed to parse time in date header: %s", err) + } + if pt.Unix() != now.Unix() { + t.Errorf("SetDateWithValue() failed. Expected time: %d, got: %d", now.Unix(), + pt.Unix()) + } + } + +// TestMsg_SetMessageIDWIthValue tests the Msg.SetMessageIDWithValue and Msg.SetMessageID methods + + func TestMsg_SetMessageIDWithValue(t *testing.T) { + m := NewMsg() + m.SetMessageID() + if m.genHeader[HeaderMessageID] == nil { + t.Errorf("SetMessageID() failed. MessageID header is nil") + return + } + if m.genHeader[HeaderMessageID][0] == "" { + t.Errorf("SetMessageID() failed. Expected value, got: empty") + return + } + if _, ok := m.genHeader[HeaderMessageID]; ok { + m.genHeader[HeaderMessageID] = nil + } + v := "This.is.a.message.id" + vf := "" + m.SetMessageIDWithValue(v) + if m.genHeader[HeaderMessageID] == nil { + t.Errorf("SetMessageIDWithValue() failed. MessageID header is nil") + return + } + if m.genHeader[HeaderMessageID][0] != vf { + t.Errorf("SetMessageIDWithValue() failed. Expected: %s, got: %s", vf, m.genHeader[HeaderMessageID][0]) + return + } + } + +// TestMsg_SetMessageIDRandomness tests the randomness of Msg.SetMessageID methods + + func TestMsg_SetMessageIDRandomness(t *testing.T) { + var mids []string + m := NewMsg() + for i := 0; i < 50_000; i++ { + m.SetMessageID() + mid := m.GetMessageID() + mids = append(mids, mid) + } + c := make(map[string]int) + for i := range mids { + c[mids[i]]++ + } + for k, v := range c { + if v > 1 { + t.Errorf("MessageID randomness not given. MessageID %q was generated %d times", k, v) + } + } + } + + func TestMsg_GetMessageID(t *testing.T) { + expected := "this.is.a.message.id" + msg := NewMsg() + msg.SetMessageIDWithValue(expected) + val := msg.GetMessageID() + if !strings.EqualFold(val, fmt.Sprintf("<%s>", expected)) { + t.Errorf("GetMessageID() failed. Expected: %s, got: %s", fmt.Sprintf("<%s>", expected), val) + } + msg.genHeader[HeaderMessageID] = nil + val = msg.GetMessageID() + if val != "" { + t.Errorf("GetMessageID() failed. Expected empty string, got: %s", val) + } + } + +// TestMsg_FromFormat tests the FromFormat and EnvelopeFrom methods for the Msg object + + func TestMsg_FromFormat(t *testing.T) { + tests := []struct { + tname string + name string + addr string + want string + fail bool + }{ + { + "valid name and addr", "Toni Tester", "tester@example.com", + `"Toni Tester" `, false, + }, + { + "no name with valid addr", "", "tester@example.com", + ``, false, + }, + { + "valid name with invalid addr", "Toni Tester", "@example.com", + ``, true, + }, + } + + m := NewMsg() + for _, tt := range tests { + t.Run(tt.tname, func(t *testing.T) { + if err := m.FromFormat(tt.name, tt.addr); err != nil && !tt.fail { + t.Errorf("failed to FromFormat(): %s", err) + return + } + if err := m.EnvelopeFromFormat(tt.name, tt.addr); err != nil && !tt.fail { + t.Errorf("failed to EnvelopeFromFormat(): %s", err) + return + } + + var fa *mail.Address + f, ok := m.addrHeader[HeaderFrom] + if ok && len(f) > 0 { + fa = f[0] + } + if (!ok || len(f) == 0) && !tt.fail { + t.Errorf(`valid from address expected, but "From:" field is empty`) + return + } + if tt.fail && len(f) > 0 { + t.Errorf("FromFormat() was supposed to failed but got value: %s", fa.String()) + return + } + + if !tt.fail && fa.String() != tt.want { + t.Errorf("wrong result for FromFormat(). Want: %s, got: %s", tt.want, fa.String()) + } + m.addrHeader[HeaderFrom] = nil + }) + } + } + + func TestMsg_GetRecipients(t *testing.T) { + a := []string{"to@example.com", "cc@example.com", "bcc@example.com"} + m := NewMsg() + + _, err := m.GetRecipients() + if err == nil { + t.Errorf("GetRecipients() succeeded but was expected to fail") + return + } + + if err := m.AddTo(a[0]); err != nil { + t.Errorf("AddTo() failed: %s", err) + return + } + if err := m.AddCc(a[1]); err != nil { + t.Errorf("AddCc() failed: %s", err) + return + } + if err := m.AddBcc(a[2]); err != nil { + t.Errorf("AddBcc() failed: %s", err) + return + } + + al, err := m.GetRecipients() + if err != nil { + t.Errorf("GetRecipients() failed: %s", err) + return + } + + tf, cf, bf := false, false, false + for _, r := range al { + if r == a[0] { + tf = true + } + if r == a[1] { + cf = true + } + if r == a[2] { + bf = true + } + } + if !tf { + t.Errorf("GetRecipients() failed. Expected to address %s but was not found", a[0]) + return + } + if !cf { + t.Errorf("GetRecipients() failed. Expected cc address %s but was not found", a[1]) + return + } + if !bf { + t.Errorf("GetRecipients() failed. Expected bcc address %s but was not found", a[2]) + return + } + } + +// TestMsg_ReplyTo tests the Msg.ReplyTo and Msg.ReplyToFormat methods + + func TestMsg_ReplyTo(t *testing.T) { + tests := []struct { + tname string + name string + addr string + want string + sf bool + }{ + { + "valid name and addr", "Toni Tester", "tester@example.com", + `"Toni Tester" `, false, + }, + { + "no name with valid addr", "", "tester@example.com", + ``, false, + }, + { + "valid name with invalid addr", "Toni Tester", "@example.com", + ``, true, + }, + } + m := NewMsg() + for _, tt := range tests { + t.Run(tt.tname, func(t *testing.T) { + if err := m.ReplyTo(tt.want); err != nil && !tt.sf { + t.Errorf("ReplyTo() method failed: %s", err) + } + if !tt.sf { + rt, ok := m.genHeader[HeaderReplyTo] + if !ok { + t.Errorf("ReplyTo() failed: ReplyTo generic header not set") + return + } + if len(rt) <= 0 { + t.Errorf("ReplyTo() failed: length of generic ReplyTo header is zero or less than zero") + return + } + if rt[0] != tt.want { + t.Errorf("ReplyTo() failed: expected value: %s, got: %s", tt.want, rt[0]) + } + } + m.genHeader = nil + m.genHeader = make(map[Header][]string) + if err := m.ReplyToFormat(tt.name, tt.addr); err != nil && !tt.sf { + t.Errorf("ReplyToFormat() method failed: %s", err) + } + if !tt.sf { + rt, ok := m.genHeader[HeaderReplyTo] + if !ok { + t.Errorf("ReplyTo() failed: ReplyTo generic header not set") + return + } + if len(rt) <= 0 { + t.Errorf("ReplyTo() failed: length of generic ReplyTo header is zero or less than zero") + return + } + if rt[0] != tt.want { + t.Errorf("ReplyTo() failed: expected value: %s, got: %s", tt.want, rt[0]) + } + } + m.genHeader = nil + m.genHeader = make(map[Header][]string) + }) + } + } + +// TestMsg_Subject tests the Msg.Subject method + + func TestMsg_Subject(t *testing.T) { + tests := []struct { + name string + sub string + want string + }{ + {"normal subject", "This is a test subject", "This is a test subject"}, + { + "subject with umlauts", "This is a test subject with umlauts: üäöß", + "=?UTF-8?q?This_is_a_test_subject_with_umlauts:_=C3=BC=C3=A4=C3=B6=C3=9F?=", + }, + { + "subject with emoji", "This is a test subject with emoji: 📧", + "=?UTF-8?q?This_is_a_test_subject_with_emoji:_=F0=9F=93=A7?=", + }, + } + m := NewMsg() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m.Subject(tt.sub) + s, ok := m.genHeader[HeaderSubject] + if !ok || len(s) <= 0 { + t.Errorf("Subject() method failed. Generic header for Subject is empty") + return + } + if s[0] != tt.want { + t.Errorf("Subject() method failed. Expected: %s, got: %s", tt.want, s[0]) + } + }) + } + } + +// TestMsg_SetImportance tests the Msg.SetImportance method + + func TestMsg_SetImportance(t *testing.T) { + tests := []struct { + name string + imp Importance + wantns string + xprio string + want string + sf bool + }{ + {"Importance: Non-Urgent", ImportanceNonUrgent, "0", "5", "non-urgent", false}, + {"Importance: Low", ImportanceLow, "0", "5", "low", false}, + {"Importance: Normal", ImportanceNormal, "", "", "", true}, + {"Importance: High", ImportanceHigh, "1", "1", "high", false}, + {"Importance: Urgent", ImportanceUrgent, "1", "1", "urgent", false}, + {"Importance: Unknown", 9, "", "", "", true}, + } + m := NewMsg() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m.SetImportance(tt.imp) + hi, ok := m.genHeader[HeaderImportance] + if (!ok || len(hi) <= 0) && !tt.sf { + t.Errorf("SetImportance() method failed. Generic header for Importance is empty") + } + hp, ok := m.genHeader[HeaderPriority] + if (!ok || len(hp) <= 0) && !tt.sf { + t.Errorf("SetImportance() method failed. Generic header for Priority is empty") + } + hx, ok := m.genHeader[HeaderXPriority] + if (!ok || len(hx) <= 0) && !tt.sf { + t.Errorf("SetImportance() method failed. Generic header for X-Priority is empty") + } + hm, ok := m.genHeader[HeaderXMSMailPriority] + if (!ok || len(hm) <= 0) && !tt.sf { + t.Errorf("SetImportance() method failed. Generic header for X-MS-XPriority is empty") + } + if !tt.sf { + if hi[0] != tt.want { + t.Errorf("SetImportance() method failed. Expected Imporance: %s, got: %s", tt.want, hi[0]) + } + if hp[0] != tt.wantns { + t.Errorf("SetImportance() method failed. Expected Priority: %s, got: %s", tt.want, hp[0]) + } + if hx[0] != tt.xprio { + t.Errorf("SetImportance() method failed. Expected X-Priority: %s, got: %s", tt.want, hx[0]) + } + if hm[0] != tt.wantns { + t.Errorf("SetImportance() method failed. Expected X-MS-Priority: %s, got: %s", tt.wantns, hm[0]) + } + } + m.genHeader = nil + m.genHeader = make(map[Header][]string) + }) + } + } + +// TestMsg_SetOrganization tests the Msg.SetOrganization method + + func TestMsg_SetOrganization(t *testing.T) { + tests := []struct { + name string + org string + }{ + {"Org: testcorp", "testcorp"}, + } + m := NewMsg() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m.SetOrganization(tt.org) + o, ok := m.genHeader[HeaderOrganization] + if !ok || len(o) <= 0 { + t.Errorf("SetOrganization() method failed. Generic header for Organization is empty") + return + } + if o[0] != tt.org { + t.Errorf("SetOrganization() method failed. Expected: %s, got: %s", tt.org, o[0]) + } + }) + } + } + +// TestMsg_SetUserAgent tests the Msg.SetUserAgent method + + func TestMsg_SetUserAgent(t *testing.T) { + tests := []struct { + name string + ua string + }{ + {"UA: Testmail 1.0", "Testmailer 1.0"}, + } + m := NewMsg() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m.SetUserAgent(tt.ua) + xm, ok := m.genHeader[HeaderXMailer] + if !ok || len(xm) <= 0 { + t.Errorf("SetUserAgent() method failed. Generic header for X-Mailer is empty") + return + } + ua, ok := m.genHeader[HeaderUserAgent] + if !ok || len(ua) <= 0 { + t.Errorf("SetUserAgent() method failed. Generic header for UserAgent is empty") + return + } + if xm[0] != tt.ua { + t.Errorf("SetUserAgent() method failed. Expected X-Mailer: %s, got: %s", tt.ua, xm[0]) + } + if ua[0] != tt.ua { + t.Errorf("SetUserAgent() method failed. Expected User-Agent: %s, got: %s", tt.ua, ua[0]) + } + }) + } + } + +// TestMsg_RequestMDN tests the different RequestMDN* related methods of Msg + + func TestMsg_RequestMDN(t *testing.T) { + n := "Toni Tester" + n2 := "Melanie Tester" + v := "toni.tester@example.com" + v2 := "melanie.tester@example.com" + iv := "testertest.tld" + vl := []string{v, v2} + m := NewMsg() + + // Single valid address + if err := m.RequestMDNTo(v); err != nil { + t.Errorf("RequestMDNTo with a single valid address failed: %s", err) + } + if val := m.genHeader[HeaderDispositionNotificationTo]; len(val) > 1 { + if val[0] != fmt.Sprintf("<%s>", v) { + t.Errorf("RequestMDNTo with a single valid address failed. Expected: %s, got: %s", v, + val[0]) + } + } + m.Reset() + + // Multiples valid addresses + if err := m.RequestMDNTo(vl...); err != nil { + t.Errorf("RequestMDNTo with a multiple valid address failed: %s", err) + } + if val := m.genHeader[HeaderDispositionNotificationTo]; len(val) > 0 { + if val[0] != fmt.Sprintf("<%s>", v) { + t.Errorf("RequestMDNTo with a multiple valid addresses failed. Expected 0: %s, got 0: %s", v, + val[0]) + } + } + if val := m.genHeader[HeaderDispositionNotificationTo]; len(val) > 1 { + if val[1] != fmt.Sprintf("<%s>", v2) { + t.Errorf("RequestMDNTo with a multiple valid addresses failed. Expected 1: %s, got 1: %s", v2, + val[1]) + } + } + m.Reset() + + // Invalid address + if err := m.RequestMDNTo(iv); err == nil { + t.Errorf("RequestMDNTo with an invalid address was supposed to failed, but didn't") + } + m.Reset() + + // Single valid addresses + AddTo + if err := m.RequestMDNTo(v); err != nil { + t.Errorf("RequestMDNTo with a single valid address failed: %s", err) + } + if err := m.RequestMDNAddTo(v2); err != nil { + t.Errorf("RequestMDNAddTo with a valid address failed: %s", err) + } + if val := m.genHeader[HeaderDispositionNotificationTo]; len(val) > 1 { + if val[1] != fmt.Sprintf("<%s>", v2) { + t.Errorf("RequestMDNTo with a multiple valid addresses failed. Expected 1: %s, got 1: %s", v2, + val[1]) + } + } + m.Reset() + + // Single valid address formated + AddToFromat + if err := m.RequestMDNToFormat(n, v); err != nil { + t.Errorf("RequestMDNToFormat with a single valid address failed: %s", err) + } + if val := m.genHeader[HeaderDispositionNotificationTo]; len(val) > 0 { + if val[0] != fmt.Sprintf(`"%s" <%s>`, n, v) { + t.Errorf(`RequestMDNToFormat with a single valid address failed. Expected: "%s" <%s>, got: %s`, n, v, + val[0]) + } + } + if err := m.RequestMDNAddToFormat(n2, v2); err != nil { + t.Errorf("RequestMDNAddToFormat with a valid address failed: %s", err) + } + if val := m.genHeader[HeaderDispositionNotificationTo]; len(val) > 1 { + if val[1] != fmt.Sprintf(`"%s" <%s>`, n2, v2) { + t.Errorf(`RequestMDNAddToFormat with a single valid address failed. Expected: "%s" <%s>, got: %s`, n2, v2, + val[1]) + } + } + m.Reset() + + // Invalid formated address + if err := m.RequestMDNToFormat(n, iv); err == nil { + t.Errorf("RequestMDNToFormat with an invalid address was supposed to failed, but didn't") + } + + // Invalid address AddTo + AddToFormat + if err := m.RequestMDNAddTo(iv); err == nil { + t.Errorf("RequestMDNAddTo with an invalid address was supposed to failed, but didn't") + } + if err := m.RequestMDNAddToFormat(n, iv); err == nil { + t.Errorf("RequestMDNAddToFormat with an invalid address was supposed to failed, but didn't") + } + } + +// TestMsg_SetBodyString tests the Msg.SetBodyString method + + func TestMsg_SetBodyString(t *testing.T) { + tests := []struct { + name string + ct ContentType + value string + want string + sf bool + }{ + {"Body: test", TypeTextPlain, "test", "test", false}, + { + "Body: with Umlauts", TypeTextHTML, "üäöß", + "üäöß", false, + }, + {"Body: with emoji", TypeTextPlain, "📧", "📧", false}, + } + m := NewMsg() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m.SetBodyString(tt.ct, tt.value) + if len(m.parts) != 1 { + t.Errorf("SetBodyString() failed: no mail parts found") + } + part := m.parts[0] + res := bytes.Buffer{} + if _, err := part.writeFunc(&res); err != nil && !tt.sf { + t.Errorf("WriteFunc of part failed: %s", err) + } + if res.String() != tt.want { + t.Errorf("SetBodyString() failed. Expecteding: %s, got: %s", tt.want, res.String()) + } + }) + } + } + +// TestMsg_AddAlternativeString tests the Msg.AddAlternativeString method + + func TestMsg_AddAlternativeString(t *testing.T) { + tests := []struct { + name string + value string + want string + sf bool + }{ + {"Body: test", "test", "test", false}, + {"Body: with Umlauts", "üäöß", "üäöß", false}, + {"Body: with emoji", "📧", "📧", false}, + } + m := NewMsg() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m.SetBodyString(TypeTextPlain, tt.value) + if len(m.parts) != 1 { + t.Errorf("AddAlternativeString() => SetBodyString() failed: no mail parts found") + } + m.AddAlternativeString(TypeTextHTML, tt.value) + if len(m.parts) != 2 { + t.Errorf("AddAlternativeString() failed: no alternative mail parts found") + } + apart := m.parts[1] + res := bytes.Buffer{} + if _, err := apart.writeFunc(&res); err != nil && !tt.sf { + t.Errorf("WriteFunc of part failed: %s", err) + } + if res.String() != tt.want { + t.Errorf("AddAlternativeString() failed. Expecteding: %s, got: %s", tt.want, res.String()) + } + }) + } + } + +// TestMsg_AttachFile tests the Msg.AttachFile and the WithFilename FileOption method + + func TestMsg_AttachFile(t *testing.T) { + tests := []struct { + name string + file string + fn string + sf bool + }{ + {"File: README.md", "README.md", "README.md", false}, + {"File: doc.go", "doc.go", "foo.go", false}, + {"File: nonexisting", "", "invalid.file", true}, + } + m := NewMsg() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m.AttachFile(tt.file, WithFileName(tt.fn), nil) + if len(m.attachments) != 1 && !tt.sf { + t.Errorf("AttachFile() failed. Number of attachments expected: %d, got: %d", 1, + len(m.attachments)) + return + } + if !tt.sf { + file := m.attachments[0] + if file == nil { + t.Errorf("AttachFile() failed. Attachment file pointer is nil") + return + } + if file.Name != tt.fn { + t.Errorf("AttachFile() failed. Filename of attachment expected: %s, got: %s", tt.fn, + file.Name) + } + buf := bytes.Buffer{} + if _, err := file.Writer(&buf); err != nil { + t.Errorf("failed to execute WriterFunc: %s", err) + return + } + } + m.Reset() + }) + } + } + +// TestMsg_GetAttachments tests the Msg.GetAttachments method + + func TestMsg_GetAttachments(t *testing.T) { + tests := []struct { + name string + files []string + }{ + {"File: README.md", []string{"README.md"}}, + {"File: doc.go", []string{"doc.go"}}, + {"File: README.md and doc.go", []string{"README.md", "doc.go"}}, + {"File: nonexisting", nil}, + } + m := NewMsg() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for _, f := range tt.files { + m.AttachFile(f, WithFileName(f), nil) + } + if len(m.attachments) != len(tt.files) { + t.Errorf("AttachFile() failed. Number of attachments expected: %d, got: %d", len(tt.files), + len(m.attachments)) + return + } + ff := m.GetAttachments() + if len(m.attachments) != len(ff) { + t.Errorf("GetAttachments() failed. Number of attachments expected: %d, got: %d", len(m.attachments), + len(ff)) + return + } + var fn []string + for _, f := range ff { + fn = append(fn, f.Name) + } + sort.Strings(fn) + sort.Strings(tt.files) + for i, f := range tt.files { + if f != fn[i] { + t.Errorf("GetAttachments() failed. Attachment name expected: %s, got: %s", f, + fn[i]) + return + } + } + m.Reset() + }) + } + } + +// TestMsg_SetAttachments tests the Msg.GetAttachments method + + func TestMsg_SetAttachments(t *testing.T) { + tests := []struct { + name string + attachments []string + files []string + }{ + {"File: replace README.md with doc.go", []string{"README.md"}, []string{"doc.go"}}, + {"File: add README.md with doc.go ", []string{"doc.go"}, []string{"README.md", "doc.go"}}, + {"File: remove README.md and doc.go", []string{"README.md", "doc.go"}, nil}, + {"File: add README.md and doc.go", nil, []string{"README.md", "doc.go"}}, + } + m := NewMsg() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sort.Strings(tt.attachments) + sort.Strings(tt.files) + for _, a := range tt.attachments { + m.AttachFile(a, WithFileName(a), nil) + } + if len(m.attachments) != len(tt.attachments) { + t.Errorf("AttachFile() failed. Number of attachments expected: %d, got: %d", len(tt.files), + len(m.attachments)) + return + } + var files []*File + for _, f := range tt.files { + files = append(files, &File{Name: f}) + } + m.SetAttachments(files) + if len(m.attachments) != len(files) { + t.Errorf("SetAttachements() failed. Number of attachments expected: %d, got: %d", len(files), + len(m.attachments)) + return + } + for i, f := range tt.files { + if f != m.attachments[i].Name { + t.Errorf("SetAttachments() failed. Attachment name expected: %s, got: %s", f, + m.attachments[i].Name) + return + } + } + m.Reset() + }) + } + } + +// TestMsg_UnsetAllAttachments tests the Msg.UnsetAllAttachments method + + func TestMsg_UnsetAllAttachments(t *testing.T) { + tests := []struct { + name string + attachments []string + }{ + {"File: one file", []string{"README.md"}}, + {"File: two files", []string{"README.md", "doc.go"}}, + {"File: nil", nil}, + } + m := NewMsg() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var files []*File + for _, f := range tt.attachments { + files = append(files, &File{Name: f}) + } + m.SetAttachments(files) + + if len(m.attachments) != len(files) { + t.Errorf("SetAttachements() failed. Number of attachments expected: %d, got: %d", len(files), + len(m.attachments)) + return + } + m.UnsetAllAttachments() + if m.attachments != nil { + t.Errorf("UnsetAllAttachments() failed. The attachments file's pointer is not nil") + return + } + m.Reset() + }) + } + } + +// TestMsg_GetEmbeds tests the Msg.GetEmbeds method + + func TestMsg_GetEmbeds(t *testing.T) { + tests := []struct { + name string + files []string + }{ + {"File: README.md", []string{"README.md"}}, + {"File: doc.go", []string{"doc.go"}}, + {"File: README.md and doc.go", []string{"README.md", "doc.go"}}, + {"File: nonexisting", nil}, + } + m := NewMsg() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for _, f := range tt.files { + m.EmbedFile(f, WithFileName(f), nil) + } + if len(m.embeds) != len(tt.files) { + t.Errorf("EmbedFile() failed. Number of embedded files expected: %d, got: %d", len(tt.files), + len(m.embeds)) + return + } + ff := m.GetEmbeds() + if len(m.embeds) != len(ff) { + t.Errorf("GetEmbeds() failed. Number of embedded files expected: %d, got: %d", len(m.embeds), + len(ff)) + return + } + var fn []string + for _, f := range ff { + fn = append(fn, f.Name) + } + sort.Strings(fn) + sort.Strings(tt.files) + for i, f := range tt.files { + if f != fn[i] { + t.Errorf("GetEmbeds() failed. Embedded file name expected: %s, got: %s", f, + fn[i]) + return + } + } + m.Reset() + }) + } + } + +// TestMsg_SetEmbeds tests the Msg.GetEmbeds method + + func TestMsg_SetEmbeds(t *testing.T) { + tests := []struct { + name string + embeds []string + files []string + }{ + {"File: replace README.md with doc.go", []string{"README.md"}, []string{"doc.go"}}, + {"File: add README.md with doc.go ", []string{"doc.go"}, []string{"README.md", "doc.go"}}, + {"File: remove README.md and doc.go", []string{"README.md", "doc.go"}, nil}, + {"File: add README.md and doc.go", nil, []string{"README.md", "doc.go"}}, + } + m := NewMsg() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sort.Strings(tt.embeds) + sort.Strings(tt.files) + for _, a := range tt.embeds { + m.EmbedFile(a, WithFileName(a), nil) + } + if len(m.embeds) != len(tt.embeds) { + t.Errorf("EmbedFile() failed. Number of embedded files expected: %d, got: %d", len(tt.files), + len(m.embeds)) + return + } + var files []*File + for _, f := range tt.files { + files = append(files, &File{Name: f}) + } + m.SetEmbeds(files) + if len(m.embeds) != len(files) { + t.Errorf("SetEmbeds() failed. Number of embedded files expected: %d, got: %d", len(files), + len(m.embeds)) + return + } + for i, f := range tt.files { + if f != m.embeds[i].Name { + t.Errorf("SetEmbeds() failed. Embedded file name expected: %s, got: %s", f, + m.embeds[i].Name) + return + } + } + m.Reset() + }) + } + } + +// TestMsg_UnsetAllEmbeds tests the Msg.TestMsg_UnsetAllEmbeds method + + func TestMsg_UnsetAllEmbeds(t *testing.T) { + tests := []struct { + name string + embeds []string + }{ + {"File: one file", []string{"README.md"}}, + {"File: two files", []string{"README.md", "doc.go"}}, + {"File: nil", nil}, + } + m := NewMsg() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var files []*File + for _, f := range tt.embeds { + files = append(files, &File{Name: f}) + } + m.SetEmbeds(files) + if len(m.embeds) != len(files) { + t.Errorf("SetEmbeds() failed. Number of embedded files expected: %d, got: %d", len(files), + len(m.embeds)) + return + } + m.UnsetAllEmbeds() + if m.embeds != nil { + t.Errorf("UnsetAllEmbeds() failed. The embeds file's point is not nil") + return + } + m.Reset() + }) + } + } + +// TestMsg_UnsetAllParts tests the Msg.TestMsg_UnsetAllParts method + + func TestMsg_UnsetAllParts(t *testing.T) { + tests := []struct { + name string + attachments []string + embeds []string + }{ + {"File: both is exist", []string{"README.md"}, []string{"doc.go"}}, + {"File: both is nil", nil, nil}, + {"File: attachment exist, embed nil", []string{"README.md"}, nil}, + {"File: attachment nil, embed exist", nil, []string{"README.md"}}, + } + m := NewMsg() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var attachments []*File + for _, f := range tt.attachments { + attachments = append(attachments, &File{Name: f}) + } + m.SetAttachments(attachments) + if len(m.attachments) != len(attachments) { + t.Errorf("SetAttachements() failed. Number of attachments files expected: %d, got: %d", + len(attachments), len(m.attachments)) + return + } + var embeds []*File + for _, f := range tt.embeds { + embeds = append(embeds, &File{Name: f}) + } + m.SetEmbeds(embeds) + if len(m.embeds) != len(embeds) { + t.Errorf("SetEmbeds() failed. Number of embedded files expected: %d, got: %d", len(embeds), + len(m.embeds)) + return + } + m.UnsetAllParts() + if m.attachments != nil { + t.Errorf("UnsetAllParts() failed. The attachments file's point is not nil") + return + } + if m.embeds != nil { + t.Errorf("UnsetAllParts() failed. The embeds file's point is not nil") + return + } + m.Reset() + }) + } + } + +// TestMsg_AttachFromEmbedFS tests the Msg.AttachFromEmbedFS and the WithFilename FileOption method + + func TestMsg_AttachFromEmbedFS(t *testing.T) { + tests := []struct { + name string + file string + fn string + sf bool + }{ + {"File: README.md", "README.md", "README.md", false}, + {"File: nonexisting", "", "invalid.file", true}, + } + m := NewMsg() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := m.AttachFromEmbedFS(tt.file, &efs, WithFileName(tt.fn)); err != nil && !tt.sf { + t.Errorf("AttachFromEmbedFS() failed: %s", err) + return + } + if len(m.attachments) != 1 && !tt.sf { + t.Errorf("AttachFile() failed. Number of attachments expected: %d, got: %d", 1, + len(m.attachments)) + return + } + if !tt.sf { + file := m.attachments[0] + if file == nil { + t.Errorf("AttachFile() failed. Attachment file pointer is nil") + return + } + if file.Name != tt.fn { + t.Errorf("AttachFile() failed. Filename of attachment expected: %s, got: %s", tt.fn, + file.Name) + } + buf := bytes.Buffer{} + if _, err := file.Writer(&buf); err != nil { + t.Errorf("failed to execute WriterFunc: %s", err) + return + } + } + m.Reset() + }) + } + } + +// TestMsg_AttachFileBrokenFunc tests WriterFunc of the Msg.AttachFile method + + func TestMsg_AttachFileBrokenFunc(t *testing.T) { + m := NewMsg() + m.AttachFile("README.md") + if len(m.attachments) != 1 { + t.Errorf("AttachFile() failed. Number of attachments expected: %d, got: %d", 1, + len(m.attachments)) + return + } + file := m.attachments[0] + if file == nil { + t.Errorf("AttachFile() failed. Attachment file pointer is nil") + return + } + file.Writer = func(io.Writer) (int64, error) { + return 0, fmt.Errorf("failing intentionally") + } + buf := bytes.Buffer{} + if _, err := file.Writer(&buf); err == nil { + t.Errorf("execute WriterFunc did not fail, but was expected to fail") + } + } + +// TestMsg_AttachReader tests the Msg.AttachReader method + + func TestMsg_AttachReader(t *testing.T) { + m := NewMsg() + ts := "This is a test string" + rbuf := bytes.Buffer{} + rbuf.WriteString(ts) + r := bufio.NewReader(&rbuf) + if err := m.AttachReader("testfile.txt", r); err != nil { + t.Errorf("AttachReader() failed. Expected no error, got: %s", err.Error()) + return + } + if len(m.attachments) != 1 { + t.Errorf("AttachReader() failed. Number of attachments expected: %d, got: %d", 1, + len(m.attachments)) + return + } + file := m.attachments[0] + if file == nil { + t.Errorf("AttachReader() failed. Attachment file pointer is nil") + return + } + if file.Name != "testfile.txt" { + t.Errorf("AttachReader() failed. Expected file name: %s, got: %s", "testfile.txt", + file.Name) + } + wbuf := bytes.Buffer{} + if _, err := file.Writer(&wbuf); err != nil { + t.Errorf("execute WriterFunc failed: %s", err) + } + if wbuf.String() != ts { + t.Errorf("AttachReader() failed. Expected string: %q, got: %q", ts, wbuf.String()) + } + } + +// TestMsg_EmbedFile tests the Msg.EmbedFile and the WithFilename FileOption method + + func TestMsg_EmbedFile(t *testing.T) { + tests := []struct { + name string + file string + fn string + sf bool + }{ + {"File: README.md", "README.md", "README.md", false}, + {"File: doc.go", "doc.go", "foo.go", false}, + {"File: nonexisting", "", "invalid.file", true}, + } + m := NewMsg() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m.EmbedFile(tt.file, WithFileName(tt.fn), nil) + if len(m.embeds) != 1 && !tt.sf { + t.Errorf("EmbedFile() failed. Number of embeds expected: %d, got: %d", 1, + len(m.embeds)) + return + } + if !tt.sf { + file := m.embeds[0] + if file == nil { + t.Errorf("EmbedFile() failed. Embedded file pointer is nil") + return + } + if file.Name != tt.fn { + t.Errorf("EmbedFile() failed. Filename of embeds expected: %s, got: %s", tt.fn, + file.Name) + } + buf := bytes.Buffer{} + if _, err := file.Writer(&buf); err != nil { + t.Errorf("failed to execute WriterFunc: %s", err) + return + } + } + m.Reset() + }) + } + } + +// TestMsg_EmbedFromEmbedFS tests the Msg.EmbedFromEmbedFS and the WithFilename FileOption method + + func TestMsg_EmbedFromEmbedFS(t *testing.T) { + tests := []struct { + name string + file string + fn string + sf bool + }{ + {"File: README.md", "README.md", "README.md", false}, + {"File: nonexisting", "", "invalid.file", true}, + } + m := NewMsg() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := m.EmbedFromEmbedFS(tt.file, &efs, WithFileName(tt.fn)); err != nil && !tt.sf { + t.Errorf("EmbedFromEmbedFS() failed: %s", err) + return + } + if len(m.embeds) != 1 && !tt.sf { + t.Errorf("EmbedFile() failed. Number of embeds expected: %d, got: %d", 1, + len(m.embeds)) + return + } + if !tt.sf { + file := m.embeds[0] + if file == nil { + t.Errorf("EmbedFile() failed. Embedded file pointer is nil") + return + } + if file.Name != tt.fn { + t.Errorf("EmbedFile() failed. Filename of embeds expected: %s, got: %s", tt.fn, + file.Name) + } + buf := bytes.Buffer{} + if _, err := file.Writer(&buf); err != nil { + t.Errorf("failed to execute WriterFunc: %s", err) + return + } + } + m.Reset() + }) + } + } + +// TestMsg_EmbedFileBrokenFunc tests WriterFunc of the Msg.EmbedFile method + + func TestMsg_EmbedFileBrokenFunc(t *testing.T) { + m := NewMsg() + m.EmbedFile("README.md") + if len(m.embeds) != 1 { + t.Errorf("EmbedFile() failed. Number of embeds expected: %d, got: %d", 1, + len(m.embeds)) + return + } + file := m.embeds[0] + if file == nil { + t.Errorf("EmbedFile() failed. Embedded file pointer is nil") + return + } + file.Writer = func(io.Writer) (int64, error) { + return 0, fmt.Errorf("failing intentionally") + } + buf := bytes.Buffer{} + if _, err := file.Writer(&buf); err == nil { + t.Errorf("execute WriterFunc did not fail, but was expected to fail") + } + } + +// TestMsg_EmbedReader tests the Msg.EmbedReader method + + func TestMsg_EmbedReader(t *testing.T) { + m := NewMsg() + ts := "This is a test string" + rbuf := bytes.Buffer{} + rbuf.WriteString(ts) + r := bufio.NewReader(&rbuf) + if err := m.EmbedReader("testfile.txt", r); err != nil { + t.Errorf("EmbedReader() failed. Expected no error, got: %s", err.Error()) + return + } + if len(m.embeds) != 1 { + t.Errorf("EmbedReader() failed. Number of embeds expected: %d, got: %d", 1, + len(m.embeds)) + return + } + file := m.embeds[0] + if file == nil { + t.Errorf("EmbedReader() failed. Embedded file pointer is nil") + return + } + if file.Name != "testfile.txt" { + t.Errorf("EmbedReader() failed. Expected file name: %s, got: %s", "testfile.txt", + file.Name) + } + wbuf := bytes.Buffer{} + if _, err := file.Writer(&wbuf); err != nil { + t.Errorf("execute WriterFunc failed: %s", err) + } + if wbuf.String() != ts { + t.Errorf("EmbedReader() failed. Expected string: %q, got: %q", ts, wbuf.String()) + } + } + +// TestMsg_hasAlt tests the hasAlt() method of the Msg + + func TestMsg_hasAlt(t *testing.T) { + m := NewMsg() + m.SetBodyString(TypeTextPlain, "Plain") + m.AddAlternativeString(TypeTextHTML, "HTML") + if !m.hasAlt() { + t.Errorf("mail has alternative parts but hasAlt() returned true") + } + } + +// TestMsg_hasRelated tests the hasRelated() method of the Msg + + func TestMsg_hasRelated(t *testing.T) { + m := NewMsg() + m.SetBodyString(TypeTextPlain, "Plain") + m.EmbedFile("README.md") + if !m.hasRelated() { + t.Errorf("mail has related parts but hasRelated() returned true") + } + } + +// TestMsg_hasMixed tests the hasMixed() method of the Msg + + func TestMsg_hasMixed(t *testing.T) { + m := NewMsg() + m.SetBodyString(TypeTextPlain, "Plain") + m.AttachFile("README.md") + if !m.hasMixed() { + t.Errorf("mail has mixed parts but hasMixed() returned true") + } + } + +// TestMsg_WriteTo tests the WriteTo() method of the Msg + + func TestMsg_WriteTo(t *testing.T) { + m := NewMsg() + m.SetBodyString(TypeTextPlain, "Plain") + wbuf := bytes.Buffer{} + n, err := m.WriteTo(&wbuf) + if err != nil { + t.Errorf("WriteTo() failed: %s", err) + return + } + if n != int64(wbuf.Len()) { + t.Errorf("WriteTo() failed: expected written byte length: %d, got: %d", n, wbuf.Len()) + } + } + +// TestMsg_WriteToSkipMiddleware tests the WriteTo() method of the Msg + + func TestMsg_WriteToSkipMiddleware(t *testing.T) { + m := NewMsg(WithMiddleware(encodeMiddleware{}), WithMiddleware(uppercaseMiddleware{})) + m.Subject("This is a test") + m.SetBodyString(TypeTextPlain, "Plain") + wbuf := bytes.Buffer{} + n, err := m.WriteToSkipMiddleware(&wbuf, "uppercase") + if err != nil { + t.Errorf("WriteToSkipMiddleware() failed: %s", err) + return + } + if n != int64(wbuf.Len()) { + t.Errorf("WriteToSkipMiddleware() failed: expected written byte length: %d, got: %d", n, wbuf.Len()) + } + if !strings.Contains(wbuf.String(), "Subject: This is @ test") { + t.Errorf("WriteToSkipMiddleware failed. Unable to find encoded subject") + } + + wbuf2 := bytes.Buffer{} + n, err = m.WriteTo(&wbuf2) + if err != nil { + t.Errorf("WriteTo() failed: %s", err) + return + } + if n != int64(wbuf2.Len()) { + t.Errorf("WriteTo() failed: expected written byte length: %d, got: %d", n, wbuf2.Len()) + } + if !strings.Contains(wbuf2.String(), "Subject: THIS IS @ TEST") { + t.Errorf("WriteToSkipMiddleware failed. Unable to find encoded and upperchase subject") + } + } + +// TestMsg_WriteTo_fails tests the WriteTo() method of the Msg but with a failing body writer function + + func TestMsg_WriteTo_fails(t *testing.T) { + m := NewMsg() + m.SetBodyWriter(TypeTextPlain, func(io.Writer) (int64, error) { + return 0, errors.New("failed") + }) + _, err := m.WriteTo(io.Discard) + if err == nil { + t.Errorf("WriteTo() with failing BodyWriter function was supposed to fail, but didn't") + return + } + + // NoEncoding handles the errors separately + m = NewMsg(WithEncoding(NoEncoding)) + m.SetBodyWriter(TypeTextPlain, func(io.Writer) (int64, error) { + return 0, errors.New("failed") + }) + _, err = m.WriteTo(io.Discard) + if err == nil { + t.Errorf("WriteTo() (no encoding) with failing BodyWriter function was supposed to fail, but didn't") + return + } + } + +// TestMsg_Write tests the Write() method of the Msg + + func TestMsg_Write(t *testing.T) { + m := NewMsg() + m.SetBodyString(TypeTextPlain, "Plain") + wbuf := bytes.Buffer{} + n, err := m.Write(&wbuf) + if err != nil { + t.Errorf("WriteTo() failed: %s", err) + return + } + if n != int64(wbuf.Len()) { + t.Errorf("WriteTo() failed: expected written byte length: %d, got: %d", n, wbuf.Len()) + } + } + +// TestMsg_WriteWithLongHeader tests the WriteTo() method of the Msg with a long header + + func TestMsg_WriteWithLongHeader(t *testing.T) { + m := NewMsg() + m.SetBodyString(TypeTextPlain, "Plain") + m.SetGenHeader(HeaderContentLang, "de", "en", "fr", "es", "xxxx", "yyyy", "de", "en", "fr", + "es", "xxxx", "yyyy", "de", "en", "fr", "es", "xxxx", "yyyy", "de", "en", "fr") + m.SetGenHeader(HeaderContentID, "XXXXXXXXXXXXXXX XXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXX", + "XXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXX") + wbuf := bytes.Buffer{} + n, err := m.WriteTo(&wbuf) + if err != nil { + t.Errorf("WriteTo() failed: %s", err) + return + } + if n != int64(wbuf.Len()) { + t.Errorf("WriteTo() failed: expected written byte length: %d, got: %d", n, wbuf.Len()) + } + } + +// TestMsg_WriteDiffEncoding tests the WriteTo() method of the Msg with different Encoding + + func TestMsg_WriteDiffEncoding(t *testing.T) { + tests := []struct { + name string + ct ContentType + en Encoding + alt bool + wa bool + we bool + }{ + {"Plain/QP/NoAlt/NoAttach/NoEmbed", TypeTextPlain, EncodingQP, false, false, false}, + {"Plain/B64/NoAlt/NoAttach/NoEmbed", TypeTextPlain, EncodingB64, false, false, false}, + {"Plain/No/NoAlt/NoAttach/NoEmbed", TypeTextPlain, NoEncoding, false, false, false}, + {"HTML/QP/NoAlt/NoAttach/NoEmbed", TypeTextHTML, EncodingQP, false, false, false}, + {"HTML/B64/NoAlt/NoAttach/NoEmbed", TypeTextHTML, EncodingB64, false, false, false}, + {"HTML/No/NoAlt/NoAttach/NoEmbed", TypeTextHTML, NoEncoding, false, false, false}, + {"Plain/QP/HTML/NoAttach/NoEmbed", TypeTextPlain, EncodingQP, true, false, false}, + {"Plain/B64/HTML/NoAttach/NoEmbed", TypeTextPlain, EncodingB64, true, false, false}, + {"Plain/No/HTML/NoAttach/NoEmbed", TypeTextPlain, NoEncoding, true, false, false}, + {"Plain/QP/NoAlt/Attach/NoEmbed", TypeTextPlain, EncodingQP, false, true, false}, + {"Plain/B64/NoAlt/Attach/NoEmbed", TypeTextPlain, EncodingB64, false, true, false}, + {"Plain/No/NoAlt/Attach/NoEmbed", TypeTextPlain, NoEncoding, false, true, false}, + {"Plain/QP/NoAlt/NoAttach/Embed", TypeTextPlain, EncodingQP, false, false, true}, + {"Plain/B64/NoAlt/NoAttach/Embed", TypeTextPlain, EncodingB64, false, false, true}, + {"Plain/No/NoAlt/NoAttach/Embed", TypeTextPlain, NoEncoding, false, false, true}, + {"Plain/QP/HTML/Attach/Embed", TypeTextPlain, EncodingQP, true, true, true}, + {"Plain/B64/HTML/Attach/Embed", TypeTextPlain, EncodingB64, true, true, true}, + {"Plain/No/HTML/Attach/Embed", TypeTextPlain, NoEncoding, true, true, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := NewMsg(WithEncoding(tt.en)) + m.SetBodyString(tt.ct, tt.name) + if tt.alt { + m.AddAlternativeString(TypeTextHTML, fmt.Sprintf("

%s

", tt.name)) + } + if tt.wa { + m.AttachFile("README.md") + } + if tt.we { + m.EmbedFile("README.md") + } + wbuf := bytes.Buffer{} + n, err := m.WriteTo(&wbuf) + if err != nil { + t.Errorf("WriteTo() failed: %s", err) + return + } + if n != int64(wbuf.Len()) { + t.Errorf("WriteTo() failed: expected written byte length: %d, got: %d", n, wbuf.Len()) + } + wbuf.Reset() + }) + } + } + +// TestMsg_appendFile tests the appendFile() method of the Msg + + func TestMsg_appendFile(t *testing.T) { + m := NewMsg() + var fl []*File + f := &File{ + Name: "file.txt", + } + fl = m.appendFile(fl, f, nil) + if len(fl) != 1 { + t.Errorf("appendFile() failed. Expected length: %d, got: %d", 1, len(fl)) + } + fl = m.appendFile(fl, f, nil) + if len(fl) != 2 { + t.Errorf("appendFile() failed. Expected length: %d, got: %d", 2, len(fl)) + } + } + +// TestMsg_multipleWrites tests multiple executions of WriteTo on the Msg + + func TestMsg_multipleWrites(t *testing.T) { + ts := "XXX_UNIQUE_STRING_XXX" + wbuf := bytes.Buffer{} + m := NewMsg() + m.SetBodyString(TypeTextPlain, ts) + + // First WriteTo() + _, err := m.WriteTo(&wbuf) + if err != nil { + t.Errorf("failed to write body to buffer: %s", err) + } + if !strings.Contains(wbuf.String(), ts) { + t.Errorf("first WriteTo() body does not contain unique string: %s", ts) + } + + // Second WriteTo() + wbuf.Reset() + _, err = m.WriteTo(&wbuf) + if err != nil { + t.Errorf("failed to write body to buffer: %s", err) + } + if !strings.Contains(wbuf.String(), ts) { + t.Errorf("second WriteTo() body does not contain unique string: %s", ts) + } + } + +// TestMsg_NewReader tests the Msg.NewReader method + + func TestMsg_NewReader(t *testing.T) { + m := NewMsg() + m.SetBodyString(TypeTextPlain, "TEST123") + mr := m.NewReader() + if mr == nil { + t.Errorf("NewReader failed: Reader is nil") + } + if mr.Error() != nil { + t.Errorf("NewReader failed: %s", mr.Error()) + } + } + +// TestMsg_NewReader_ioCopy tests the Msg.NewReader method using io.Copy + + func TestMsg_NewReader_ioCopy(t *testing.T) { + wbuf1 := bytes.Buffer{} + wbuf2 := bytes.Buffer{} + m := NewMsg() + m.SetBodyString(TypeTextPlain, "TEST123") + mr := m.NewReader() + if mr == nil { + t.Errorf("NewReader failed: Reader is nil") + } + + // First we use WriteTo to have something to compare to + _, err := m.WriteTo(&wbuf1) + if err != nil { + t.Errorf("failed to write body to buffer: %s", err) + } + + // Then we write to wbuf2 via io.Copy + n, err := io.Copy(&wbuf2, mr) + if err != nil { + t.Errorf("failed to use io.Copy on Reader: %s", err) + } + if n != int64(wbuf1.Len()) { + t.Errorf("message length of WriteTo and io.Copy differ. Expected: %d, got: %d", wbuf1.Len(), n) + } + if wbuf1.String() != wbuf2.String() { + t.Errorf("message content of WriteTo and io.Copy differ") + } + } + +// TestMsg_UpdateReader tests the Msg.UpdateReader method + + func TestMsg_UpdateReader(t *testing.T) { + m := NewMsg() + m.Subject("Subject-Run 1") + mr := m.NewReader() + if mr == nil { + t.Errorf("NewReader failed: Reader is nil") + } + wbuf1 := bytes.Buffer{} + _, err := io.Copy(&wbuf1, mr) + if err != nil { + t.Errorf("io.Copy on Reader failed: %s", err) + } + if !strings.Contains(wbuf1.String(), "Subject: Subject-Run 1") { + t.Errorf("io.Copy on Reader failed. Expected to find %q but string in Subject was not found", + "Subject-Run 1") + } + + m.Subject("Subject-Run 2") + m.UpdateReader(mr) + wbuf2 := bytes.Buffer{} + _, err = io.Copy(&wbuf2, mr) + if err != nil { + t.Errorf("2nd io.Copy on Reader failed: %s", err) + } + if !strings.Contains(wbuf2.String(), "Subject: Subject-Run 2") { + t.Errorf("io.Copy on Reader failed. Expected to find %q but string in Subject was not found", + "Subject-Run 2") + } + } + +// TestMsg_SetBodyTextTemplate tests the Msg.SetBodyTextTemplate method + + func TestMsg_SetBodyTextTemplate(t *testing.T) { + tests := []struct { + name string + tpl string + ph string + sf bool + }{ + {"normal text", "This is a {{.Placeholder}}", "TemplateTest", false}, + {"invalid tpl", "This is a {{ foo .Placeholder}}", "TemplateTest", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data := struct { + Placeholder string + }{Placeholder: tt.ph} + tpl, err := ttpl.New("test").Parse(tt.tpl) + if err != nil && !tt.sf { + t.Errorf("failed to render template: %s", err) + return + } + m := NewMsg() + if err := m.SetBodyTextTemplate(tpl, data); err != nil && !tt.sf { + t.Errorf("failed to set template as body: %s", err) + } + + wbuf := bytes.Buffer{} + _, err = m.WriteTo(&wbuf) + if err != nil { + t.Errorf("failed to write body to buffer: %s", err) + } + if !strings.Contains(wbuf.String(), tt.ph) && !tt.sf { + t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) + } + m.Reset() + }) + } + } + +// TestMsg_SetBodyHTMLTemplate tests the Msg.SetBodyHTMLTemplate method + + func TestMsg_SetBodyHTMLTemplate(t *testing.T) { + tests := []struct { + name string + tpl string + ph string + ex string + sf bool + }{ + {"normal HTML", "

This is a {{.Placeholder}}

", "TemplateTest", "TemplateTest", false}, + { + "HTML with HTML", "

This is a {{.Placeholder}}

", "", + "<script>alert(1)</script>", false, + }, + {"invalid tpl", "

This is a {{ foo .Placeholder}}

", "TemplateTest", "", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data := struct { + Placeholder string + }{Placeholder: tt.ph} + tpl, err := htpl.New("test").Parse(tt.tpl) + if err != nil && !tt.sf { + t.Errorf("failed to render template: %s", err) + return + } + m := NewMsg() + if err := m.SetBodyHTMLTemplate(tpl, data); err != nil && !tt.sf { + t.Errorf("failed to set template as body: %s", err) + } + + wbuf := bytes.Buffer{} + _, err = m.WriteTo(&wbuf) + if err != nil { + t.Errorf("failed to write body to buffer: %s", err) + } + if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf { + t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) + } + m.Reset() + }) + } + } + +// TestMsg_AddAlternativeTextTemplate tests the Msg.AddAlternativeTextTemplate method + + func TestMsg_AddAlternativeTextTemplate(t *testing.T) { + tests := []struct { + name string + tpl string + ph string + sf bool + }{ + {"normal text", "This is a {{.Placeholder}}", "TemplateTest", false}, + {"invalid tpl", "This is a {{ foo .Placeholder}}", "TemplateTest", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data := struct { + Placeholder string + }{Placeholder: tt.ph} + tpl, err := ttpl.New("test").Parse(tt.tpl) + if err != nil && !tt.sf { + t.Errorf("failed to render template: %s", err) + return + } + m := NewMsg() + m.SetBodyString(TypeTextHTML, "") + if err := m.AddAlternativeTextTemplate(tpl, data); err != nil && !tt.sf { + t.Errorf("failed to set template as alternative part: %s", err) + } + + wbuf := bytes.Buffer{} + _, err = m.WriteTo(&wbuf) + if err != nil { + t.Errorf("failed to write body to buffer: %s", err) + } + if !strings.Contains(wbuf.String(), tt.ph) && !tt.sf { + t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) + } + m.Reset() + }) + } + } + +// TestMsg_AddAlternativeHTMLTemplate tests the Msg.AddAlternativeHTMLTemplate method + + func TestMsg_AddAlternativeHTMLTemplate(t *testing.T) { + tests := []struct { + name string + tpl string + ph string + ex string + sf bool + }{ + {"normal HTML", "

This is a {{.Placeholder}}

", "TemplateTest", "TemplateTest", false}, + { + "HTML with HTML", "

This is a {{.Placeholder}}

", "", + "<script>alert(1)</script>", false, + }, + {"invalid tpl", "

This is a {{ foo .Placeholder}}

", "TemplateTest", "", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data := struct { + Placeholder string + }{Placeholder: tt.ph} + tpl, err := htpl.New("test").Parse(tt.tpl) + if err != nil && !tt.sf { + t.Errorf("failed to render template: %s", err) + return + } + m := NewMsg() + m.SetBodyString(TypeTextPlain, "") + if err := m.AddAlternativeHTMLTemplate(tpl, data); err != nil && !tt.sf { + t.Errorf("failed to set template as alternative part: %s", err) + } + + wbuf := bytes.Buffer{} + _, err = m.WriteTo(&wbuf) + if err != nil { + t.Errorf("failed to write body to buffer: %s", err) + } + if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf { + t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) + } + m.Reset() + }) + } + } + +// TestMsg_AttachTextTemplate tests the Msg.AttachTextTemplate method + + func TestMsg_AttachTextTemplate(t *testing.T) { + tests := []struct { + name string + tpl string + ph string + ex string + ac int + sf bool + }{ + { + "normal text", "This is a {{.Placeholder}}", "TemplateTest", + "VGhpcyBpcyBhIFRlbXBsYXRlVGVzdA==", 1, false, + }, + {"invalid tpl", "This is a {{ foo .Placeholder}}", "TemplateTest", "", 0, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data := struct { + Placeholder string + }{Placeholder: tt.ph} + tpl, err := ttpl.New("test").Parse(tt.tpl) + if err != nil && !tt.sf { + t.Errorf("failed to render template: %s", err) + return + } + m := NewMsg() + m.SetBodyString(TypeTextPlain, "This is the body") + if err := m.AttachTextTemplate("attachment.txt", tpl, data); err != nil && !tt.sf { + t.Errorf("failed to attach template: %s", err) + } + + wbuf := bytes.Buffer{} + _, err = m.WriteTo(&wbuf) + if err != nil { + t.Errorf("failed to write body to buffer: %s", err) + } + if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf { + t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) + } + if len(m.attachments) != tt.ac { + t.Errorf("wrong number of attachments. Expected: %d, got: %d", tt.ac, len(m.attachments)) + } + m.Reset() + }) + } + } + +// TestMsg_AttachHTMLTemplate tests the Msg.AttachHTMLTemplate method + + func TestMsg_AttachHTMLTemplate(t *testing.T) { + tests := []struct { + name string + tpl string + ph string + ex string + ac int + sf bool + }{ + { + "normal HTML", "

This is a {{.Placeholder}}

", "TemplateTest", + "PHA+VGhpcyBpcyBhIFRlbXBsYXRlVGVzdDwvcD4=", 1, false, + }, + { + "HTML with HTML", "

This is a {{.Placeholder}}

", "", + "PHA+VGhpcyBpcyBhICZsdDtzY3JpcHQmZ3Q7YWxlcnQoMSkmbHQ7L3NjcmlwdCZndDs8L3A+", 1, false, + }, + {"invalid tpl", "

This is a {{ foo .Placeholder}}

", "TemplateTest", "", 0, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data := struct { + Placeholder string + }{Placeholder: tt.ph} + tpl, err := htpl.New("test").Parse(tt.tpl) + if err != nil && !tt.sf { + t.Errorf("failed to render template: %s", err) + return + } + m := NewMsg() + m.SetBodyString(TypeTextPlain, "") + if err := m.AttachHTMLTemplate("attachment.html", tpl, data); err != nil && !tt.sf { + t.Errorf("failed to set template as alternative part: %s", err) + } + + wbuf := bytes.Buffer{} + _, err = m.WriteTo(&wbuf) + if err != nil { + t.Errorf("failed to write body to buffer: %s", err) + } + if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf { + t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) + } + if len(m.attachments) != tt.ac { + t.Errorf("wrong number of attachments. Expected: %d, got: %d", tt.ac, len(m.attachments)) + } + m.Reset() + }) + } + } + +// TestMsg_EmbedTextTemplate tests the Msg.EmbedTextTemplate method + + func TestMsg_EmbedTextTemplate(t *testing.T) { + tests := []struct { + name string + tpl string + ph string + ex string + ec int + sf bool + }{ + { + "normal text", "This is a {{.Placeholder}}", "TemplateTest", + "VGhpcyBpcyBhIFRlbXBsYXRlVGVzdA==", 1, false, + }, + {"invalid tpl", "This is a {{ foo .Placeholder}}", "TemplateTest", "", 0, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data := struct { + Placeholder string + }{Placeholder: tt.ph} + tpl, err := ttpl.New("test").Parse(tt.tpl) + if err != nil && !tt.sf { + t.Errorf("failed to render template: %s", err) + return + } + m := NewMsg() + m.SetBodyString(TypeTextPlain, "This is the body") + if err := m.EmbedTextTemplate("attachment.txt", tpl, data); err != nil && !tt.sf { + t.Errorf("failed to attach template: %s", err) + } + + wbuf := bytes.Buffer{} + _, err = m.WriteTo(&wbuf) + if err != nil { + t.Errorf("failed to write body to buffer: %s", err) + } + if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf { + t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) + } + if len(m.embeds) != tt.ec { + t.Errorf("wrong number of attachments. Expected: %d, got: %d", tt.ec, len(m.attachments)) + } + m.Reset() + }) + } + } + +// TestMsg_EmbedHTMLTemplate tests the Msg.EmbedHTMLTemplate method + + func TestMsg_EmbedHTMLTemplate(t *testing.T) { + tests := []struct { + name string + tpl string + ph string + ex string + ec int + sf bool + }{ + { + "normal HTML", "

This is a {{.Placeholder}}

", "TemplateTest", + "PHA+VGhpcyBpcyBhIFRlbXBsYXRlVGVzdDwvcD4=", 1, false, + }, + { + "HTML with HTML", "

This is a {{.Placeholder}}

", "", + "PHA+VGhpcyBpcyBhICZsdDtzY3JpcHQmZ3Q7YWxlcnQoMSkmbHQ7L3NjcmlwdCZndDs8L3A+", 1, false, + }, + {"invalid tpl", "

This is a {{ foo .Placeholder}}

", "TemplateTest", "", 0, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data := struct { + Placeholder string + }{Placeholder: tt.ph} + tpl, err := htpl.New("test").Parse(tt.tpl) + if err != nil && !tt.sf { + t.Errorf("failed to render template: %s", err) + return + } + m := NewMsg() + m.SetBodyString(TypeTextPlain, "") + if err := m.EmbedHTMLTemplate("attachment.html", tpl, data); err != nil && !tt.sf { + t.Errorf("failed to set template as alternative part: %s", err) + } + + wbuf := bytes.Buffer{} + _, err = m.WriteTo(&wbuf) + if err != nil { + t.Errorf("failed to write body to buffer: %s", err) + } + if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf { + t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) + } + if len(m.embeds) != tt.ec { + t.Errorf("wrong number of attachments. Expected: %d, got: %d", tt.ec, len(m.attachments)) + } + m.Reset() + }) + } + } + +// TestMsg_WriteToTempFile will test the output to temporary files + + func TestMsg_WriteToTempFile(t *testing.T) { + m := NewMsg() + _ = m.From("Toni Tester ") + _ = m.To("Ellenor Tester ") + m.SetBodyString(TypeTextPlain, "This is a test") + f, err := m.WriteToTempFile() + if err != nil { + t.Errorf("failed to write message to temporary output file: %s", err) + } + _ = os.Remove(f) + } + +// TestMsg_WriteToFile will test the output to a file + + func TestMsg_WriteToFile(t *testing.T) { + f, err := os.CreateTemp("", "go-mail-test_*.eml") + if err != nil { + t.Errorf("failed to create temporary output file: %s", err) + } + defer func() { + _ = f.Close() + _ = os.Remove(f.Name()) + }() + + m := NewMsg() + _ = m.From("Toni Tester ") + _ = m.To("Ellenor Tester ") + m.SetBodyString(TypeTextPlain, "This is a test") + if err := m.WriteToFile(f.Name()); err != nil { + t.Errorf("failed to write to output file: %s", err) + } + fi, err := os.Stat(f.Name()) + if err != nil { + t.Errorf("failed to stat output file: %s", err) + } + if fi == nil { + t.Errorf("received empty file handle") + return + } + if fi.Size() <= 0 { + t.Errorf("output file is expected to contain data but its size is zero") + } + } + +// TestMsg_GetGenHeader will test the GetGenHeader method of the Msg + + func TestMsg_GetGenHeader(t *testing.T) { + m := NewMsg() + m.Subject("this is a test") + sa := m.GetGenHeader(HeaderSubject) + if len(sa) <= 0 { + t.Errorf("GetGenHeader on subject failed. Got empty slice") + return + } + if sa[0] == "" { + t.Errorf("GetGenHeader on subject failed. Got empty value") + } + if sa[0] != "this is a test" { + t.Errorf("GetGenHeader on subject failed. Expected: %q, got: %q", "this is a test", sa[0]) + } + } + +// TestMsg_GetAddrHeader will test the Msg.GetAddrHeader method + + func TestMsg_GetAddrHeader(t *testing.T) { + m := NewMsg() + if err := m.FromFormat("Toni Sender", "sender@example.com"); err != nil { + t.Errorf("failed to set FROM address: %s", err) + } + if err := m.AddToFormat("Toni To", "to@example.com"); err != nil { + t.Errorf("failed to set TO address: %s", err) + } + if err := m.AddCcFormat("Toni Cc", "cc@example.com"); err != nil { + t.Errorf("failed to set CC address: %s", err) + } + if err := m.AddBccFormat("Toni Bcc", "bcc@example.com"); err != nil { + t.Errorf("failed to set BCC address: %s", err) + } + fh := m.GetAddrHeader(HeaderFrom) + if len(fh) <= 0 { + t.Errorf("GetAddrHeader on FROM failed. Got empty slice") + return + } + if fh[0].String() == "" { + t.Errorf("GetAddrHeader on FROM failed. Got empty value") + } + if fh[0].String() != `"Toni Sender" ` { + t.Errorf("GetAddrHeader on FROM failed. Expected: %q, got: %q", + `"Toni Sender" "`, fh[0].String()) + } + th := m.GetAddrHeader(HeaderTo) + if len(th) <= 0 { + t.Errorf("GetAddrHeader on TO failed. Got empty slice") + return + } + if th[0].String() == "" { + t.Errorf("GetAddrHeader on TO failed. Got empty value") + } + if th[0].String() != `"Toni To" ` { + t.Errorf("GetAddrHeader on TO failed. Expected: %q, got: %q", + `"Toni To" "`, th[0].String()) + } + ch := m.GetAddrHeader(HeaderCc) + if len(ch) <= 0 { + t.Errorf("GetAddrHeader on CC failed. Got empty slice") + return + } + if ch[0].String() == "" { + t.Errorf("GetAddrHeader on CC failed. Got empty value") + } + if ch[0].String() != `"Toni Cc" ` { + t.Errorf("GetAddrHeader on CC failed. Expected: %q, got: %q", + `"Toni Cc" "`, ch[0].String()) + } + bh := m.GetAddrHeader(HeaderBcc) + if len(bh) <= 0 { + t.Errorf("GetAddrHeader on BCC failed. Got empty slice") + return + } + if bh[0].String() == "" { + t.Errorf("GetAddrHeader on BCC failed. Got empty value") + } + if bh[0].String() != `"Toni Bcc" ` { + t.Errorf("GetAddrHeader on BCC failed. Expected: %q, got: %q", + `"Toni Bcc" "`, bh[0].String()) + } + } + +// TestMsg_GetFrom will test the Msg.GetFrom method + + func TestMsg_GetFrom(t *testing.T) { + m := NewMsg() + if err := m.FromFormat("Toni Sender", "sender@example.com"); err != nil { + t.Errorf("failed to set FROM address: %s", err) + } + fh := m.GetFrom() + if len(fh) <= 0 { + t.Errorf("GetFrom failed. Got empty slice") + return + } + if fh[0].String() == "" { + t.Errorf("GetFrom failed. Got empty value") + } + if fh[0].String() != `"Toni Sender" ` { + t.Errorf("GetFrom failed. Expected: %q, got: %q", + `"Toni Sender" "`, fh[0].String()) + } + } + +// TestMsg_GetFromString will test the Msg.GetFromString method + + func TestMsg_GetFromString(t *testing.T) { + m := NewMsg() + if err := m.FromFormat("Toni Sender", "sender@example.com"); err != nil { + t.Errorf("failed to set FROM address: %s", err) + } + fh := m.GetFromString() + if len(fh) <= 0 { + t.Errorf("GetFromString failed. Got empty slice") + return + } + if fh[0] == "" { + t.Errorf("GetFromString failed. Got empty value") + } + if fh[0] != `"Toni Sender" ` { + t.Errorf("GetFromString failed. Expected: %q, got: %q", + `"Toni Sender" "`, fh[0]) + } + } + +// TestMsg_GetTo will test the Msg.GetTo method + + func TestMsg_GetTo(t *testing.T) { + m := NewMsg() + if err := m.AddToFormat("Toni To", "to@example.com"); err != nil { + t.Errorf("failed to set TO address: %s", err) + } + fh := m.GetTo() + if len(fh) <= 0 { + t.Errorf("GetTo failed. Got empty slice") + return + } + if fh[0].String() == "" { + t.Errorf("GetTo failed. Got empty value") + } + if fh[0].String() != `"Toni To" ` { + t.Errorf("GetTo failed. Expected: %q, got: %q", + `"Toni To" "`, fh[0].String()) + } + } + +// TestMsg_GetToString will test the Msg.GetToString method + + func TestMsg_GetToString(t *testing.T) { + m := NewMsg() + if err := m.AddToFormat("Toni To", "to@example.com"); err != nil { + t.Errorf("failed to set TO address: %s", err) + } + fh := m.GetToString() + if len(fh) <= 0 { + t.Errorf("GetToString failed. Got empty slice") + return + } + if fh[0] == "" { + t.Errorf("GetToString failed. Got empty value") + } + if fh[0] != `"Toni To" ` { + t.Errorf("GetToString failed. Expected: %q, got: %q", + `"Toni To" "`, fh[0]) + } + } + +// TestMsg_GetCc will test the Msg.GetCc method + + func TestMsg_GetCc(t *testing.T) { + m := NewMsg() + if err := m.AddCcFormat("Toni Cc", "cc@example.com"); err != nil { + t.Errorf("failed to set TO address: %s", err) + } + fh := m.GetCc() + if len(fh) <= 0 { + t.Errorf("GetCc failed. Got empty slice") + return + } + if fh[0].String() == "" { + t.Errorf("GetCc failed. Got empty value") + } + if fh[0].String() != `"Toni Cc" ` { + t.Errorf("GetCc failed. Expected: %q, got: %q", + `"Toni Cc" "`, fh[0].String()) + } + } + +// TestMsg_GetCcString will test the Msg.GetCcString method + + func TestMsg_GetCcString(t *testing.T) { + m := NewMsg() + if err := m.AddCcFormat("Toni Cc", "cc@example.com"); err != nil { + t.Errorf("failed to set TO address: %s", err) + } + fh := m.GetCcString() + if len(fh) <= 0 { + t.Errorf("GetCcString failed. Got empty slice") + return + } + if fh[0] == "" { + t.Errorf("GetCcString failed. Got empty value") + } + if fh[0] != `"Toni Cc" ` { + t.Errorf("GetCcString failed. Expected: %q, got: %q", + `"Toni Cc" "`, fh[0]) + } + } + +// TestMsg_GetBcc will test the Msg.GetBcc method + + func TestMsg_GetBcc(t *testing.T) { + m := NewMsg() + if err := m.AddBccFormat("Toni Bcc", "bcc@example.com"); err != nil { + t.Errorf("failed to set TO address: %s", err) + } + fh := m.GetBcc() + if len(fh) <= 0 { + t.Errorf("GetBcc failed. Got empty slice") + return + } + if fh[0].String() == "" { + t.Errorf("GetBcc failed. Got empty value") + } + if fh[0].String() != `"Toni Bcc" ` { + t.Errorf("GetBcc failed. Expected: %q, got: %q", + `"Toni Cc" "`, fh[0].String()) + } + } + +// TestMsg_GetBccString will test the Msg.GetBccString method + + func TestMsg_GetBccString(t *testing.T) { + m := NewMsg() + if err := m.AddBccFormat("Toni Bcc", "bcc@example.com"); err != nil { + t.Errorf("failed to set TO address: %s", err) + } + fh := m.GetBccString() + if len(fh) <= 0 { + t.Errorf("GetBccString failed. Got empty slice") + return + } + if fh[0] == "" { + t.Errorf("GetBccString failed. Got empty value") + } + if fh[0] != `"Toni Bcc" ` { + t.Errorf("GetBccString failed. Expected: %q, got: %q", + `"Toni Cc" "`, fh[0]) + } + } + +// TestMsg_GetBoundary will test the Msg.GetBoundary method + + func TestMsg_GetBoundary(t *testing.T) { + b := "random_boundary_string" + m := NewMsg() + if boundary := m.GetBoundary(); boundary != "" { + t.Errorf("GetBoundary failed. Expected empty string, but got: %s", boundary) + } + m = NewMsg(WithBoundary(b)) + if boundary := m.GetBoundary(); boundary != b { + t.Errorf("GetBoundary failed. Expected boundary: %s, got: %s", b, boundary) + } + } + +// TestMsg_AttachEmbedReader_consecutive tests the Msg.AttachReader and Msg.EmbedReader +// methods with consecutive calls to Msg.WriteTo to make sure the attachments are not +// lost (see Github issue #110) + + func TestMsg_AttachEmbedReader_consecutive(t *testing.T) { + ts1 := "This is a test string" + ts2 := "Another test string" + m := NewMsg() + if err := m.AttachReader("attachment.txt", bytes.NewBufferString(ts1)); err != nil { + t.Errorf("AttachReader() failed. Expected no error, got: %s", err.Error()) + return + } + if err := m.EmbedReader("embedded.txt", bytes.NewBufferString(ts2)); err != nil { + t.Errorf("EmbedReader() failed. Expected no error, got: %s", err.Error()) + return + } + obuf1 := &bytes.Buffer{} + obuf2 := &bytes.Buffer{} + _, err := m.WriteTo(obuf1) + if err != nil { + t.Errorf("WriteTo to first output buffer failed: %s", err) + } + _, err = m.WriteTo(obuf2) + if err != nil { + t.Errorf("WriteTo to second output buffer failed: %s", err) + } + if !strings.Contains(obuf1.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { + t.Errorf("Expected file attachment string not found in first output buffer") + } + if !strings.Contains(obuf2.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { + t.Errorf("Expected file attachment string not found in second output buffer") + } + if !strings.Contains(obuf1.String(), "QW5vdGhlciB0ZXN0IHN0cmluZw==") { + t.Errorf("Expected embedded file string not found in first output buffer") + } + if !strings.Contains(obuf2.String(), "QW5vdGhlciB0ZXN0IHN0cmluZw==") { + t.Errorf("Expected embded file string not found in second output buffer") + } + } + +// TestMsg_AttachEmbedReadSeeker_consecutive tests the Msg.AttachReadSeeker and +// Msg.EmbedReadSeeker methods with consecutive calls to Msg.WriteTo to make +// sure the attachments are not lost (see Github issue #110) + + func TestMsg_AttachEmbedReadSeeker_consecutive(t *testing.T) { + ts1 := []byte("This is a test string") + ts2 := []byte("Another test string") + m := NewMsg() + m.AttachReadSeeker("attachment.txt", bytes.NewReader(ts1)) + m.EmbedReadSeeker("embedded.txt", bytes.NewReader(ts2)) + obuf1 := &bytes.Buffer{} + obuf2 := &bytes.Buffer{} + _, err := m.WriteTo(obuf1) + if err != nil { + t.Errorf("WriteTo to first output buffer failed: %s", err) + } + _, err = m.WriteTo(obuf2) + if err != nil { + t.Errorf("WriteTo to second output buffer failed: %s", err) + } + if !strings.Contains(obuf1.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { + t.Errorf("Expected file attachment string not found in first output buffer") + } + if !strings.Contains(obuf2.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { + t.Errorf("Expected file attachment string not found in second output buffer") + } + if !strings.Contains(obuf1.String(), "QW5vdGhlciB0ZXN0IHN0cmluZw==") { + t.Errorf("Expected embedded file string not found in first output buffer") + } + if !strings.Contains(obuf2.String(), "QW5vdGhlciB0ZXN0IHN0cmluZw==") { + t.Errorf("Expected embded file string not found in second output buffer") + } + } + +// TestMsg_AttachReadSeeker tests the Msg.AttachReadSeeker method + + func TestMsg_AttachReadSeeker(t *testing.T) { + m := NewMsg() + ts := []byte("This is a test string") + r := bytes.NewReader(ts) + m.AttachReadSeeker("testfile.txt", r) + if len(m.attachments) != 1 { + t.Errorf("AttachReadSeeker() failed. Number of attachments expected: %d, got: %d", 1, + len(m.attachments)) + return + } + file := m.attachments[0] + if file == nil { + t.Errorf("AttachReadSeeker() failed. Attachment file pointer is nil") + return + } + if file.Name != "testfile.txt" { + t.Errorf("AttachReadSeeker() failed. Expected file name: %s, got: %s", "testfile.txt", + file.Name) + } + wbuf := bytes.Buffer{} + if _, err := file.Writer(&wbuf); err != nil { + t.Errorf("execute WriterFunc failed: %s", err) + } + if wbuf.String() != string(ts) { + t.Errorf("AttachReadSeeker() failed. Expected string: %q, got: %q", ts, wbuf.String()) + } + } + +// TestMsg_EmbedReadSeeker tests the Msg.EmbedReadSeeker method + + func TestMsg_EmbedReadSeeker(t *testing.T) { + m := NewMsg() + ts := []byte("This is a test string") + r := bytes.NewReader(ts) + m.EmbedReadSeeker("testfile.txt", r) + if len(m.embeds) != 1 { + t.Errorf("EmbedReadSeeker() failed. Number of attachments expected: %d, got: %d", 1, + len(m.embeds)) + return + } + file := m.embeds[0] + if file == nil { + t.Errorf("EmbedReadSeeker() failed. Embedded file pointer is nil") + return + } + if file.Name != "testfile.txt" { + t.Errorf("EmbedReadSeeker() failed. Expected file name: %s, got: %s", "testfile.txt", + file.Name) + } + wbuf := bytes.Buffer{} + if _, err := file.Writer(&wbuf); err != nil { + t.Errorf("execute WriterFunc failed: %s", err) + } + if wbuf.String() != string(ts) { + t.Errorf("EmbedReadSeeker() failed. Expected string: %q, got: %q", ts, wbuf.String()) + } + } + +// TestMsg_ToFromString tests Msg.ToFromString in different scenarios + + func TestMsg_ToFromString(t *testing.T) { + tests := []struct { + n string + v string + w []*mail.Address + sf bool + }{ + {"valid single address", "test@test.com", []*mail.Address{ + {Name: "", Address: "test@test.com"}, + }, false}, + { + "valid multiple addresses", "test@test.com,test2@example.com", + []*mail.Address{ + {Name: "", Address: "test@test.com"}, + {Name: "", Address: "test2@example.com"}, + }, + false, + }, + { + "valid multiple addresses with space and name", + `test@test.com, "Toni Tester" `, + []*mail.Address{ + {Name: "", Address: "test@test.com"}, + {Name: "Toni Tester", Address: "test2@example.com"}, + }, + false, + }, + { + "invalid and valid multiple addresses", "test@test.com,test2#example.com", nil, + true, + }, + } + + for _, tt := range tests { + t.Run(tt.n, func(t *testing.T) { + m := NewMsg() + if err := m.ToFromString(tt.v); err != nil && !tt.sf { + t.Errorf("Msg.ToFromString failed: %s", err) + return + } + mto := m.GetTo() + if len(mto) != len(tt.w) { + t.Errorf("Msg.ToFromString failed, expected len: %d, got: %d", len(tt.w), + len(mto)) + return + } + for i := range mto { + w := tt.w[i] + g := mto[i] + if w.String() != g.String() { + t.Errorf("Msg.ToFromString failed, expected address: %s, got: %s", + w.String(), g.String()) + } + } + }) + } + } + +// TestMsg_CcFromString tests Msg.CcFromString in different scenarios + + func TestMsg_CcFromString(t *testing.T) { + tests := []struct { + n string + v string + w []*mail.Address + sf bool + }{ + {"valid single address", "test@test.com", []*mail.Address{ + {Name: "", Address: "test@test.com"}, + }, false}, + { + "valid multiple addresses", "test@test.com,test2@example.com", + []*mail.Address{ + {Name: "", Address: "test@test.com"}, + {Name: "", Address: "test2@example.com"}, + }, + false, + }, + { + "valid multiple addresses with space and name", + `test@test.com, "Toni Tester" `, + []*mail.Address{ + {Name: "", Address: "test@test.com"}, + {Name: "Toni Tester", Address: "test2@example.com"}, + }, + false, + }, + { + "invalid and valid multiple addresses", "test@test.com,test2#example.com", nil, + true, + }, + } + + for _, tt := range tests { + t.Run(tt.n, func(t *testing.T) { + m := NewMsg() + if err := m.CcFromString(tt.v); err != nil && !tt.sf { + t.Errorf("Msg.CcFromString failed: %s", err) + return + } + mto := m.GetCc() + if len(mto) != len(tt.w) { + t.Errorf("Msg.CcFromString failed, expected len: %d, got: %d", len(tt.w), + len(mto)) + return + } + for i := range mto { + w := tt.w[i] + g := mto[i] + if w.String() != g.String() { + t.Errorf("Msg.CcFromString failed, expected address: %s, got: %s", + w.String(), g.String()) + } + } + }) + } + } + +// TestMsg_BccFromString tests Msg.BccFromString in different scenarios + + func TestMsg_BccFromString(t *testing.T) { + tests := []struct { + n string + v string + w []*mail.Address + sf bool + }{ + {"valid single address", "test@test.com", []*mail.Address{ + {Name: "", Address: "test@test.com"}, + }, false}, + { + "valid multiple addresses", "test@test.com,test2@example.com", + []*mail.Address{ + {Name: "", Address: "test@test.com"}, + {Name: "", Address: "test2@example.com"}, + }, + false, + }, + { + "valid multiple addresses with space and name", + `test@test.com, "Toni Tester" `, + []*mail.Address{ + {Name: "", Address: "test@test.com"}, + {Name: "Toni Tester", Address: "test2@example.com"}, + }, + false, + }, + { + "invalid and valid multiple addresses", "test@test.com,test2#example.com", nil, + true, + }, + } + + for _, tt := range tests { + t.Run(tt.n, func(t *testing.T) { + m := NewMsg() + if err := m.BccFromString(tt.v); err != nil && !tt.sf { + t.Errorf("Msg.BccFromString failed: %s", err) + return + } + mto := m.GetBcc() + if len(mto) != len(tt.w) { + t.Errorf("Msg.BccFromString failed, expected len: %d, got: %d", len(tt.w), + len(mto)) + return + } + for i := range mto { + w := tt.w[i] + g := mto[i] + if w.String() != g.String() { + t.Errorf("Msg.BccFromString failed, expected address: %s, got: %s", + w.String(), g.String()) + } + } + }) + } + } + +// TestMsg_checkUserAgent tests the checkUserAgent method of the Msg + + func TestMsg_checkUserAgent(t *testing.T) { + tests := []struct { + name string + noDefaultUserAgent bool + genHeader map[Header][]string + wantUserAgent string + sf bool + }{ + { + name: "check default user agent", + noDefaultUserAgent: false, + wantUserAgent: fmt.Sprintf("go-mail v%s // https://github.com/wneessen/go-mail", VERSION), + sf: false, + }, + { + name: "check no default user agent", + noDefaultUserAgent: true, + wantUserAgent: "", + sf: true, + }, + { + name: "check if ua and xm is already set", + noDefaultUserAgent: false, + genHeader: map[Header][]string{ + HeaderUserAgent: {"custom UA"}, + HeaderXMailer: {"custom XM"}, + }, + wantUserAgent: "custom UA", + sf: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + msg := &Msg{ + noDefaultUserAgent: tt.noDefaultUserAgent, + genHeader: tt.genHeader, + } + msg.checkUserAgent() + gotUserAgent := "" + if val, ok := msg.genHeader[HeaderUserAgent]; ok { + gotUserAgent = val[0] // Assuming the first one is the needed value + } + if gotUserAgent != tt.wantUserAgent && !tt.sf { + t.Errorf("UserAgent got = %v, want = %v", gotUserAgent, tt.wantUserAgent) + } + }) + } + } + +// TestNewMsgWithMIMEVersion tests WithMIMEVersion and Msg.SetMIMEVersion + + func TestNewMsgWithNoDefaultUserAgent(t *testing.T) { + m := NewMsg(WithNoDefaultUserAgent()) + if m.noDefaultUserAgent != true { + t.Errorf("WithNoDefaultUserAgent() failed. Expected: %t, got: %t", true, false) + } + } + +// Fuzzing tests + + func FuzzMsg_Subject(f *testing.F) { + f.Add("Testsubject") + f.Add("") + f.Add("This is a longer test subject.") + f.Add("Let's add some umlauts: üäöß") + f.Add("Or even emojis: ☝️💪👍") + f.Fuzz(func(t *testing.T, data string) { + m := NewMsg() + m.Subject(data) + m.Reset() }) } -} + func FuzzMsg_From(f *testing.F) { + f.Add("Toni Tester ") + f.Add("") + f.Add("mail@server.com") + f.Fuzz(func(t *testing.T, data string) { + m := NewMsg() + if err := m.From(data); err != nil && + !strings.Contains(err.Error(), "failed to parse mail address") { + t.Errorf("failed set set FROM address: %s", err) + } + m.Reset() + }) + } +*/ type uppercaseMiddleware struct{} func (mw uppercaseMiddleware) Handle(m *Msg) *Msg { @@ -244,3032 +3540,3 @@ func (mw encodeMiddleware) Handle(m *Msg) *Msg { func (mw encodeMiddleware) Type() MiddlewareType { return "encode" } - -// TestNewMsgWithMiddleware tests WithMiddleware -func TestNewMsgWithMiddleware(t *testing.T) { - m := NewMsg() - if len(m.middlewares) != 0 { - t.Errorf("empty middlewares failed. m.middlewares expected to be: empty, got: %d middleware", len(m.middlewares)) - } - m = NewMsg(WithMiddleware(uppercaseMiddleware{})) - if len(m.middlewares) != 1 { - t.Errorf("empty middlewares failed. m.middlewares expected to be: 1, got: %d middleware", len(m.middlewares)) - } - m = NewMsg(WithMiddleware(uppercaseMiddleware{}), WithMiddleware(encodeMiddleware{})) - if len(m.middlewares) != 2 { - t.Errorf("empty middlewares failed. m.middlewares expected to be: 2, got: %d middleware", len(m.middlewares)) - } -} - -// TestApplyMiddlewares tests the applyMiddlewares for the Msg object -func TestApplyMiddlewares(t *testing.T) { - tests := []struct { - name string - sub string - want string - }{ - {"normal subject", "This is a test subject", "THIS IS @ TEST SUBJECT"}, - {"subject with one middleware effect", "This is test subject", "THIS IS TEST SUBJECT"}, - {"subject with one middleware effect", "This is A test subject", "THIS IS A TEST SUBJECT"}, - } - m := NewMsg(WithMiddleware(encodeMiddleware{}), WithMiddleware(uppercaseMiddleware{})) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m.Subject(tt.sub) - if m.genHeader[HeaderSubject] == nil { - t.Errorf("Subject() method failed in applyMiddlewares() test. Generic header for subject is empty") - return - } - m = m.applyMiddlewares(m) - s, ok := m.genHeader[HeaderSubject] - if !ok { - t.Errorf("failed to get subject header") - } - if s[0] != tt.want { - t.Errorf("applyMiddlewares() method failed. Expected: %s, got: %s", tt.want, s[0]) - } - }) - } -} - -// TestMsg_SetGenHeader tests Msg.SetGenHeader -func TestMsg_SetGenHeader(t *testing.T) { - tests := []struct { - name string - header Header - values []string - }{ - {"set subject", HeaderSubject, []string{"This is Subject"}}, - {"set content-language", HeaderContentLang, []string{"en", "de", "fr", "es"}}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m := NewMsg() - m.SetGenHeader(tt.header, tt.values...) - if m.genHeader[tt.header] == nil { - t.Errorf("SetGenHeader() failed. Tried to set header %s, but it is empty", tt.header) - return - } - for _, v := range tt.values { - found := false - for _, hv := range m.genHeader[tt.header] { - if hv == v { - found = true - } - } - if !found { - t.Errorf("SetGenHeader() failed. Value %s not found in header field", v) - } - } - }) - } -} - -// TestMsg_SetGenHeaderPreformatted tests Msg.SetGenHeaderPreformatted -func TestMsg_SetGenHeaderPreformatted(t *testing.T) { - tests := []struct { - name string - header Header - value string - }{ - {"set subject", HeaderSubject, "This is Subject"}, - {"set content-language", HeaderContentLang, fmt.Sprintf("%s, %s, %s, %s", - "en", "de", "fr", "es")}, - {"set subject with newline", HeaderSubject, "This is Subject\r\n with 2nd line"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m := &Msg{} - m.SetGenHeaderPreformatted(tt.header, tt.value) - m = NewMsg() - m.SetGenHeaderPreformatted(tt.header, tt.value) - if m.preformHeader[tt.header] == "" { - t.Errorf("SetGenHeaderPreformatted() failed. Tried to set header %s, but it is empty", tt.header) - } - if m.preformHeader[tt.header] != tt.value { - t.Errorf("SetGenHeaderPreformatted() failed. Expected: %q, got: %q", tt.value, - m.preformHeader[tt.header]) - } - buf := bytes.Buffer{} - _, err := m.WriteTo(&buf) - if err != nil { - t.Errorf("failed to write message to memory: %s", err) - return - } - if !strings.Contains(buf.String(), fmt.Sprintf("%s: %s%s", tt.header, tt.value, SingleNewLine)) { - t.Errorf("SetGenHeaderPreformatted() failed. Unable to find correctly formated header in " + - "mail message output") - } - }) - } -} - -// TestMsg_AddTo tests the Msg.AddTo method -func TestMsg_AddTo(t *testing.T) { - a := []string{"address1@example.com", "address2@example.com"} - na := "address3@example.com" - m := NewMsg() - if err := m.To(a...); err != nil { - t.Errorf("failed to set TO addresses: %s", err) - return - } - if err := m.AddTo(na); err != nil { - t.Errorf("AddTo failed: %s", err) - return - } - - atf := false - for _, v := range m.addrHeader[HeaderTo] { - if v.Address == na { - atf = true - } - } - if !atf { - t.Errorf("AddTo() failed. Address %q not found in TO address slice.", na) - } -} - -// TestMsg_From tests the Msg.From and Msg.GetSender methods -func TestMsg_From(t *testing.T) { - a := "toni@example.com" - n := "Toni Tester" - na := fmt.Sprintf(`"%s" <%s>`, n, a) - m := NewMsg() - - _, err := m.GetSender(false) - if err == nil { - t.Errorf("GetSender(false) without a set From address succeeded but was expected to fail") - return - } - - if err := m.From(na); err != nil { - t.Errorf("failed to set FROM addresses: %s", err) - return - } - gs, err := m.GetSender(false) - if err != nil { - t.Errorf("GetSender(false) failed: %s", err) - return - } - if gs != a { - t.Errorf("From() failed. Expected: %s, got: %s", a, gs) - return - } - - gs, err = m.GetSender(true) - if err != nil { - t.Errorf("GetSender(true) failed: %s", err) - return - } - if gs != na { - t.Errorf("From() failed. Expected: %s, got: %s", na, gs) - return - } -} - -// TestMsg_EnvelopeFrom tests the Msg.EnvelopeFrom and Msg.GetSender methods -func TestMsg_EnvelopeFrom(t *testing.T) { - e := "envelope@example.com" - a := "toni@example.com" - n := "Toni Tester" - na := fmt.Sprintf(`"%s" <%s>`, n, a) - ne := fmt.Sprintf(`<%s>`, e) - m := NewMsg() - - _, err := m.GetSender(false) - if err == nil { - t.Errorf("GetSender(false) without a set envelope From address succeeded but was expected to fail") - return - } - - if err := m.EnvelopeFrom(e); err != nil { - t.Errorf("failed to set envelope FROM addresses: %s", err) - return - } - gs, err := m.GetSender(false) - if err != nil { - t.Errorf("GetSender(false) failed: %s", err) - return - } - if gs != e { - t.Errorf("From() failed. Expected: %s, got: %s", e, gs) - return - } - - if err := m.From(na); err != nil { - t.Errorf("failed to set FROM addresses: %s", err) - return - } - gs, err = m.GetSender(false) - if err != nil { - t.Errorf("GetSender(false) failed: %s", err) - return - } - if gs != e { - t.Errorf("From() failed. Expected: %s, got: %s", e, gs) - return - } - - gs, err = m.GetSender(true) - if err != nil { - t.Errorf("GetSender(true) failed: %s", err) - return - } - if gs != ne { - t.Errorf("From() failed. Expected: %s, got: %s", ne, gs) - return - } - m.Reset() - - if err := m.From(na); err != nil { - t.Errorf("failed to set FROM addresses: %s", err) - return - } - gs, err = m.GetSender(false) - if err != nil { - t.Errorf("GetSender(true) failed: %s", err) - return - } - if gs != a { - t.Errorf("From() failed. Expected: %s, got: %s", a, gs) - return - } - gs, err = m.GetSender(true) - if err != nil { - t.Errorf("GetSender(true) failed: %s", err) - return - } - if gs != na { - t.Errorf("From() failed. Expected: %s, got: %s", na, gs) - return - } -} - -// TestMsg_AddToFormat tests the Msg.AddToFormat method -func TestMsg_AddToFormat(t *testing.T) { - a := []string{"address1@example.com", "address2@example.com"} - nn := "Toni Tester" - na := "address3@example.com" - w := `"Toni Tester" ` - m := NewMsg() - if err := m.To(a...); err != nil { - t.Errorf("failed to set TO addresses: %s", err) - return - } - if err := m.AddToFormat(nn, na); err != nil { - t.Errorf("AddToFormat failed: %s", err) - return - } - - atf := false - for _, v := range m.addrHeader[HeaderTo] { - if v.String() == w { - atf = true - } - } - if !atf { - t.Errorf("AddToFormat() failed. Address %q not found in TO address slice.", w) - } -} - -// TestMsg_ToIgnoreInvalid tests the Msg.ToIgnoreInvalid method -func TestMsg_ToIgnoreInvalid(t *testing.T) { - a := []string{"address1@example.com", "address2@example.com"} - fa := []string{"address1@example.com", "address2@example.com", "failedaddress.com"} - m := NewMsg() - - m.ToIgnoreInvalid(a...) - l := len(m.addrHeader[HeaderTo]) - if l != len(a) { - t.Errorf("ToIgnoreInvalid() failed. Expected %d addresses, got: %d", len(a), l) - } - m.ToIgnoreInvalid(fa...) - l = len(m.addrHeader[HeaderTo]) - if l != len(fa)-1 { - t.Errorf("ToIgnoreInvalid() failed. Expected %d addresses, got: %d", len(fa)-1, l) - } -} - -// TestMsg_AddCc tests the Msg.AddCc method -func TestMsg_AddCc(t *testing.T) { - a := []string{"address1@example.com", "address2@example.com"} - na := "address3@example.com" - m := NewMsg() - if err := m.Cc(a...); err != nil { - t.Errorf("failed to set CC addresses: %s", err) - return - } - if err := m.AddCc(na); err != nil { - t.Errorf("AddCc failed: %s", err) - return - } - - atf := false - for _, v := range m.addrHeader[HeaderCc] { - if v.Address == na { - atf = true - } - } - if !atf { - t.Errorf("AddCc() failed. Address %q not found in CC address slice.", na) - } -} - -// TestMsg_AddCcFormat tests the Msg.AddCcFormat method -func TestMsg_AddCcFormat(t *testing.T) { - a := []string{"address1@example.com", "address2@example.com"} - nn := "Toni Tester" - na := "address3@example.com" - w := `"Toni Tester" ` - m := NewMsg() - if err := m.Cc(a...); err != nil { - t.Errorf("failed to set CC addresses: %s", err) - return - } - if err := m.AddCcFormat(nn, na); err != nil { - t.Errorf("AddCcFormat failed: %s", err) - return - } - - atf := false - for _, v := range m.addrHeader[HeaderCc] { - if v.String() == w { - atf = true - } - } - if !atf { - t.Errorf("AddCcFormat() failed. Address %q not found in CC address slice.", w) - } -} - -// TestMsg_CcIgnoreInvalid tests the Msg.CcIgnoreInvalid method -func TestMsg_CcIgnoreInvalid(t *testing.T) { - a := []string{"address1@example.com", "address2@example.com"} - fa := []string{"address1@example.com", "address2@example.com", "failedaddress.com"} - m := NewMsg() - - m.CcIgnoreInvalid(a...) - l := len(m.addrHeader[HeaderCc]) - if l != len(a) { - t.Errorf("CcIgnoreInvalid() failed. Expected %d addresses, got: %d", len(a), l) - } - m.CcIgnoreInvalid(fa...) - l = len(m.addrHeader[HeaderCc]) - if l != len(fa)-1 { - t.Errorf("CcIgnoreInvalid() failed. Expected %d addresses, got: %d", len(fa)-1, l) - } -} - -// TestMsg_AddBcc tests the Msg.AddBcc method -func TestMsg_AddBcc(t *testing.T) { - a := []string{"address1@example.com", "address2@example.com"} - na := "address3@example.com" - m := NewMsg() - if err := m.Bcc(a...); err != nil { - t.Errorf("failed to set BCC addresses: %s", err) - return - } - if err := m.AddBcc(na); err != nil { - t.Errorf("AddBcc failed: %s", err) - return - } - - atf := false - for _, v := range m.addrHeader[HeaderBcc] { - if v.Address == na { - atf = true - } - } - if !atf { - t.Errorf("AddBcc() failed. Address %q not found in BCC address slice.", na) - } -} - -// TestMsg_AddBccFormat tests the Msg.AddBccFormat method -func TestMsg_AddBccFormat(t *testing.T) { - a := []string{"address1@example.com", "address2@example.com"} - nn := "Toni Tester" - na := "address3@example.com" - w := `"Toni Tester" ` - m := NewMsg() - if err := m.Bcc(a...); err != nil { - t.Errorf("failed to set BCC addresses: %s", err) - return - } - if err := m.AddBccFormat(nn, na); err != nil { - t.Errorf("AddBccFormat failed: %s", err) - return - } - - atf := false - for _, v := range m.addrHeader[HeaderBcc] { - if v.String() == w { - atf = true - } - } - if !atf { - t.Errorf("AddBccFormat() failed. Address %q not found in BCC address slice.", w) - } -} - -// TestMsg_BccIgnoreInvalid tests the Msg.BccIgnoreInvalid method -func TestMsg_BccIgnoreInvalid(t *testing.T) { - a := []string{"address1@example.com", "address2@example.com"} - fa := []string{"address1@example.com", "address2@example.com", "failedaddress.com"} - m := NewMsg() - - m.BccIgnoreInvalid(a...) - l := len(m.addrHeader[HeaderBcc]) - if l != len(a) { - t.Errorf("BccIgnoreInvalid() failed. Expected %d addresses, got: %d", len(a), l) - } - m.BccIgnoreInvalid(fa...) - l = len(m.addrHeader[HeaderBcc]) - if l != len(fa)-1 { - t.Errorf("BccIgnoreInvalid() failed. Expected %d addresses, got: %d", len(fa)-1, l) - } -} - -// TestMsg_SetBulk tests the Msg.SetBulk method -func TestMsg_SetBulk(t *testing.T) { - m := NewMsg() - m.SetBulk() - if m.genHeader[HeaderPrecedence] == nil { - t.Errorf("SetBulk() failed. Precedence header is nil") - return - } - if m.genHeader[HeaderPrecedence][0] != "bulk" { - t.Errorf("SetBulk() failed. Expected Precedence header: %q, got: %q", "bulk", - m.genHeader[HeaderPrecedence][0]) - } - if m.genHeader[HeaderXAutoResponseSuppress] == nil { - t.Errorf("SetBulk() failed. X-Auto-Response-Suppress header is nil") - return - } - if m.genHeader[HeaderXAutoResponseSuppress][0] != "All" { - t.Errorf("SetBulk() failed. Expected X-Auto-Response-Suppress header: %q, got: %q", "All", - m.genHeader[HeaderXAutoResponseSuppress][0]) - } -} - -// TestMsg_SetDate tests the Msg.SetDate and Msg.SetDateWithValue method -func TestMsg_SetDate(t *testing.T) { - m := NewMsg() - m.SetDate() - if m.genHeader[HeaderDate] == nil { - t.Errorf("SetDate() failed. Date header is nil") - return - } - d, ok := m.genHeader[HeaderDate] - if !ok { - t.Errorf("failed to get date header") - return - } - _, err := time.Parse(time.RFC1123Z, d[0]) - if err != nil { - t.Errorf("failed to parse time in date header: %s", err) - } - m.genHeader = nil - m.genHeader = make(map[Header][]string) - - now := time.Now() - m.SetDateWithValue(now) - if m.genHeader[HeaderDate] == nil { - t.Errorf("SetDateWithValue() failed. Date header is nil") - return - } - d, ok = m.genHeader[HeaderDate] - if !ok { - t.Errorf("failed to get date header") - return - } - pt, err := time.Parse(time.RFC1123Z, d[0]) - if err != nil { - t.Errorf("failed to parse time in date header: %s", err) - } - if pt.Unix() != now.Unix() { - t.Errorf("SetDateWithValue() failed. Expected time: %d, got: %d", now.Unix(), - pt.Unix()) - } -} - -// TestMsg_SetMessageIDWIthValue tests the Msg.SetMessageIDWithValue and Msg.SetMessageID methods -func TestMsg_SetMessageIDWithValue(t *testing.T) { - m := NewMsg() - m.SetMessageID() - if m.genHeader[HeaderMessageID] == nil { - t.Errorf("SetMessageID() failed. MessageID header is nil") - return - } - if m.genHeader[HeaderMessageID][0] == "" { - t.Errorf("SetMessageID() failed. Expected value, got: empty") - return - } - if _, ok := m.genHeader[HeaderMessageID]; ok { - m.genHeader[HeaderMessageID] = nil - } - v := "This.is.a.message.id" - vf := "" - m.SetMessageIDWithValue(v) - if m.genHeader[HeaderMessageID] == nil { - t.Errorf("SetMessageIDWithValue() failed. MessageID header is nil") - return - } - if m.genHeader[HeaderMessageID][0] != vf { - t.Errorf("SetMessageIDWithValue() failed. Expected: %s, got: %s", vf, m.genHeader[HeaderMessageID][0]) - return - } -} - -// TestMsg_SetMessageIDRandomness tests the randomness of Msg.SetMessageID methods -func TestMsg_SetMessageIDRandomness(t *testing.T) { - var mids []string - m := NewMsg() - for i := 0; i < 50_000; i++ { - m.SetMessageID() - mid := m.GetMessageID() - mids = append(mids, mid) - } - c := make(map[string]int) - for i := range mids { - c[mids[i]]++ - } - for k, v := range c { - if v > 1 { - t.Errorf("MessageID randomness not given. MessageID %q was generated %d times", k, v) - } - } -} - -func TestMsg_GetMessageID(t *testing.T) { - expected := "this.is.a.message.id" - msg := NewMsg() - msg.SetMessageIDWithValue(expected) - val := msg.GetMessageID() - if !strings.EqualFold(val, fmt.Sprintf("<%s>", expected)) { - t.Errorf("GetMessageID() failed. Expected: %s, got: %s", fmt.Sprintf("<%s>", expected), val) - } - msg.genHeader[HeaderMessageID] = nil - val = msg.GetMessageID() - if val != "" { - t.Errorf("GetMessageID() failed. Expected empty string, got: %s", val) - } -} - -// TestMsg_FromFormat tests the FromFormat and EnvelopeFrom methods for the Msg object -func TestMsg_FromFormat(t *testing.T) { - tests := []struct { - tname string - name string - addr string - want string - fail bool - }{ - { - "valid name and addr", "Toni Tester", "tester@example.com", - `"Toni Tester" `, false, - }, - { - "no name with valid addr", "", "tester@example.com", - ``, false, - }, - { - "valid name with invalid addr", "Toni Tester", "@example.com", - ``, true, - }, - } - - m := NewMsg() - for _, tt := range tests { - t.Run(tt.tname, func(t *testing.T) { - if err := m.FromFormat(tt.name, tt.addr); err != nil && !tt.fail { - t.Errorf("failed to FromFormat(): %s", err) - return - } - if err := m.EnvelopeFromFormat(tt.name, tt.addr); err != nil && !tt.fail { - t.Errorf("failed to EnvelopeFromFormat(): %s", err) - return - } - - var fa *mail.Address - f, ok := m.addrHeader[HeaderFrom] - if ok && len(f) > 0 { - fa = f[0] - } - if (!ok || len(f) == 0) && !tt.fail { - t.Errorf(`valid from address expected, but "From:" field is empty`) - return - } - if tt.fail && len(f) > 0 { - t.Errorf("FromFormat() was supposed to failed but got value: %s", fa.String()) - return - } - - if !tt.fail && fa.String() != tt.want { - t.Errorf("wrong result for FromFormat(). Want: %s, got: %s", tt.want, fa.String()) - } - m.addrHeader[HeaderFrom] = nil - }) - } -} - -func TestMsg_GetRecipients(t *testing.T) { - a := []string{"to@example.com", "cc@example.com", "bcc@example.com"} - m := NewMsg() - - _, err := m.GetRecipients() - if err == nil { - t.Errorf("GetRecipients() succeeded but was expected to fail") - return - } - - if err := m.AddTo(a[0]); err != nil { - t.Errorf("AddTo() failed: %s", err) - return - } - if err := m.AddCc(a[1]); err != nil { - t.Errorf("AddCc() failed: %s", err) - return - } - if err := m.AddBcc(a[2]); err != nil { - t.Errorf("AddBcc() failed: %s", err) - return - } - - al, err := m.GetRecipients() - if err != nil { - t.Errorf("GetRecipients() failed: %s", err) - return - } - - tf, cf, bf := false, false, false - for _, r := range al { - if r == a[0] { - tf = true - } - if r == a[1] { - cf = true - } - if r == a[2] { - bf = true - } - } - if !tf { - t.Errorf("GetRecipients() failed. Expected to address %s but was not found", a[0]) - return - } - if !cf { - t.Errorf("GetRecipients() failed. Expected cc address %s but was not found", a[1]) - return - } - if !bf { - t.Errorf("GetRecipients() failed. Expected bcc address %s but was not found", a[2]) - return - } -} - -// TestMsg_ReplyTo tests the Msg.ReplyTo and Msg.ReplyToFormat methods -func TestMsg_ReplyTo(t *testing.T) { - tests := []struct { - tname string - name string - addr string - want string - sf bool - }{ - { - "valid name and addr", "Toni Tester", "tester@example.com", - `"Toni Tester" `, false, - }, - { - "no name with valid addr", "", "tester@example.com", - ``, false, - }, - { - "valid name with invalid addr", "Toni Tester", "@example.com", - ``, true, - }, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.tname, func(t *testing.T) { - if err := m.ReplyTo(tt.want); err != nil && !tt.sf { - t.Errorf("ReplyTo() method failed: %s", err) - } - if !tt.sf { - rt, ok := m.genHeader[HeaderReplyTo] - if !ok { - t.Errorf("ReplyTo() failed: ReplyTo generic header not set") - return - } - if len(rt) <= 0 { - t.Errorf("ReplyTo() failed: length of generic ReplyTo header is zero or less than zero") - return - } - if rt[0] != tt.want { - t.Errorf("ReplyTo() failed: expected value: %s, got: %s", tt.want, rt[0]) - } - } - m.genHeader = nil - m.genHeader = make(map[Header][]string) - if err := m.ReplyToFormat(tt.name, tt.addr); err != nil && !tt.sf { - t.Errorf("ReplyToFormat() method failed: %s", err) - } - if !tt.sf { - rt, ok := m.genHeader[HeaderReplyTo] - if !ok { - t.Errorf("ReplyTo() failed: ReplyTo generic header not set") - return - } - if len(rt) <= 0 { - t.Errorf("ReplyTo() failed: length of generic ReplyTo header is zero or less than zero") - return - } - if rt[0] != tt.want { - t.Errorf("ReplyTo() failed: expected value: %s, got: %s", tt.want, rt[0]) - } - } - m.genHeader = nil - m.genHeader = make(map[Header][]string) - }) - } -} - -// TestMsg_Subject tests the Msg.Subject method -func TestMsg_Subject(t *testing.T) { - tests := []struct { - name string - sub string - want string - }{ - {"normal subject", "This is a test subject", "This is a test subject"}, - { - "subject with umlauts", "This is a test subject with umlauts: üäöß", - "=?UTF-8?q?This_is_a_test_subject_with_umlauts:_=C3=BC=C3=A4=C3=B6=C3=9F?=", - }, - { - "subject with emoji", "This is a test subject with emoji: 📧", - "=?UTF-8?q?This_is_a_test_subject_with_emoji:_=F0=9F=93=A7?=", - }, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m.Subject(tt.sub) - s, ok := m.genHeader[HeaderSubject] - if !ok || len(s) <= 0 { - t.Errorf("Subject() method failed. Generic header for Subject is empty") - return - } - if s[0] != tt.want { - t.Errorf("Subject() method failed. Expected: %s, got: %s", tt.want, s[0]) - } - }) - } -} - -// TestMsg_SetImportance tests the Msg.SetImportance method -func TestMsg_SetImportance(t *testing.T) { - tests := []struct { - name string - imp Importance - wantns string - xprio string - want string - sf bool - }{ - {"Importance: Non-Urgent", ImportanceNonUrgent, "0", "5", "non-urgent", false}, - {"Importance: Low", ImportanceLow, "0", "5", "low", false}, - {"Importance: Normal", ImportanceNormal, "", "", "", true}, - {"Importance: High", ImportanceHigh, "1", "1", "high", false}, - {"Importance: Urgent", ImportanceUrgent, "1", "1", "urgent", false}, - {"Importance: Unknown", 9, "", "", "", true}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m.SetImportance(tt.imp) - hi, ok := m.genHeader[HeaderImportance] - if (!ok || len(hi) <= 0) && !tt.sf { - t.Errorf("SetImportance() method failed. Generic header for Importance is empty") - } - hp, ok := m.genHeader[HeaderPriority] - if (!ok || len(hp) <= 0) && !tt.sf { - t.Errorf("SetImportance() method failed. Generic header for Priority is empty") - } - hx, ok := m.genHeader[HeaderXPriority] - if (!ok || len(hx) <= 0) && !tt.sf { - t.Errorf("SetImportance() method failed. Generic header for X-Priority is empty") - } - hm, ok := m.genHeader[HeaderXMSMailPriority] - if (!ok || len(hm) <= 0) && !tt.sf { - t.Errorf("SetImportance() method failed. Generic header for X-MS-XPriority is empty") - } - if !tt.sf { - if hi[0] != tt.want { - t.Errorf("SetImportance() method failed. Expected Imporance: %s, got: %s", tt.want, hi[0]) - } - if hp[0] != tt.wantns { - t.Errorf("SetImportance() method failed. Expected Priority: %s, got: %s", tt.want, hp[0]) - } - if hx[0] != tt.xprio { - t.Errorf("SetImportance() method failed. Expected X-Priority: %s, got: %s", tt.want, hx[0]) - } - if hm[0] != tt.wantns { - t.Errorf("SetImportance() method failed. Expected X-MS-Priority: %s, got: %s", tt.wantns, hm[0]) - } - } - m.genHeader = nil - m.genHeader = make(map[Header][]string) - }) - } -} - -// TestMsg_SetOrganization tests the Msg.SetOrganization method -func TestMsg_SetOrganization(t *testing.T) { - tests := []struct { - name string - org string - }{ - {"Org: testcorp", "testcorp"}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m.SetOrganization(tt.org) - o, ok := m.genHeader[HeaderOrganization] - if !ok || len(o) <= 0 { - t.Errorf("SetOrganization() method failed. Generic header for Organization is empty") - return - } - if o[0] != tt.org { - t.Errorf("SetOrganization() method failed. Expected: %s, got: %s", tt.org, o[0]) - } - }) - } -} - -// TestMsg_SetUserAgent tests the Msg.SetUserAgent method -func TestMsg_SetUserAgent(t *testing.T) { - tests := []struct { - name string - ua string - }{ - {"UA: Testmail 1.0", "Testmailer 1.0"}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m.SetUserAgent(tt.ua) - xm, ok := m.genHeader[HeaderXMailer] - if !ok || len(xm) <= 0 { - t.Errorf("SetUserAgent() method failed. Generic header for X-Mailer is empty") - return - } - ua, ok := m.genHeader[HeaderUserAgent] - if !ok || len(ua) <= 0 { - t.Errorf("SetUserAgent() method failed. Generic header for UserAgent is empty") - return - } - if xm[0] != tt.ua { - t.Errorf("SetUserAgent() method failed. Expected X-Mailer: %s, got: %s", tt.ua, xm[0]) - } - if ua[0] != tt.ua { - t.Errorf("SetUserAgent() method failed. Expected User-Agent: %s, got: %s", tt.ua, ua[0]) - } - }) - } -} - -// TestMsg_RequestMDN tests the different RequestMDN* related methods of Msg -func TestMsg_RequestMDN(t *testing.T) { - n := "Toni Tester" - n2 := "Melanie Tester" - v := "toni.tester@example.com" - v2 := "melanie.tester@example.com" - iv := "testertest.tld" - vl := []string{v, v2} - m := NewMsg() - - // Single valid address - if err := m.RequestMDNTo(v); err != nil { - t.Errorf("RequestMDNTo with a single valid address failed: %s", err) - } - if val := m.genHeader[HeaderDispositionNotificationTo]; len(val) > 1 { - if val[0] != fmt.Sprintf("<%s>", v) { - t.Errorf("RequestMDNTo with a single valid address failed. Expected: %s, got: %s", v, - val[0]) - } - } - m.Reset() - - // Multiples valid addresses - if err := m.RequestMDNTo(vl...); err != nil { - t.Errorf("RequestMDNTo with a multiple valid address failed: %s", err) - } - if val := m.genHeader[HeaderDispositionNotificationTo]; len(val) > 0 { - if val[0] != fmt.Sprintf("<%s>", v) { - t.Errorf("RequestMDNTo with a multiple valid addresses failed. Expected 0: %s, got 0: %s", v, - val[0]) - } - } - if val := m.genHeader[HeaderDispositionNotificationTo]; len(val) > 1 { - if val[1] != fmt.Sprintf("<%s>", v2) { - t.Errorf("RequestMDNTo with a multiple valid addresses failed. Expected 1: %s, got 1: %s", v2, - val[1]) - } - } - m.Reset() - - // Invalid address - if err := m.RequestMDNTo(iv); err == nil { - t.Errorf("RequestMDNTo with an invalid address was supposed to failed, but didn't") - } - m.Reset() - - // Single valid addresses + AddTo - if err := m.RequestMDNTo(v); err != nil { - t.Errorf("RequestMDNTo with a single valid address failed: %s", err) - } - if err := m.RequestMDNAddTo(v2); err != nil { - t.Errorf("RequestMDNAddTo with a valid address failed: %s", err) - } - if val := m.genHeader[HeaderDispositionNotificationTo]; len(val) > 1 { - if val[1] != fmt.Sprintf("<%s>", v2) { - t.Errorf("RequestMDNTo with a multiple valid addresses failed. Expected 1: %s, got 1: %s", v2, - val[1]) - } - } - m.Reset() - - // Single valid address formated + AddToFromat - if err := m.RequestMDNToFormat(n, v); err != nil { - t.Errorf("RequestMDNToFormat with a single valid address failed: %s", err) - } - if val := m.genHeader[HeaderDispositionNotificationTo]; len(val) > 0 { - if val[0] != fmt.Sprintf(`"%s" <%s>`, n, v) { - t.Errorf(`RequestMDNToFormat with a single valid address failed. Expected: "%s" <%s>, got: %s`, n, v, - val[0]) - } - } - if err := m.RequestMDNAddToFormat(n2, v2); err != nil { - t.Errorf("RequestMDNAddToFormat with a valid address failed: %s", err) - } - if val := m.genHeader[HeaderDispositionNotificationTo]; len(val) > 1 { - if val[1] != fmt.Sprintf(`"%s" <%s>`, n2, v2) { - t.Errorf(`RequestMDNAddToFormat with a single valid address failed. Expected: "%s" <%s>, got: %s`, n2, v2, - val[1]) - } - } - m.Reset() - - // Invalid formated address - if err := m.RequestMDNToFormat(n, iv); err == nil { - t.Errorf("RequestMDNToFormat with an invalid address was supposed to failed, but didn't") - } - - // Invalid address AddTo + AddToFormat - if err := m.RequestMDNAddTo(iv); err == nil { - t.Errorf("RequestMDNAddTo with an invalid address was supposed to failed, but didn't") - } - if err := m.RequestMDNAddToFormat(n, iv); err == nil { - t.Errorf("RequestMDNAddToFormat with an invalid address was supposed to failed, but didn't") - } -} - -// TestMsg_SetBodyString tests the Msg.SetBodyString method -func TestMsg_SetBodyString(t *testing.T) { - tests := []struct { - name string - ct ContentType - value string - want string - sf bool - }{ - {"Body: test", TypeTextPlain, "test", "test", false}, - { - "Body: with Umlauts", TypeTextHTML, "üäöß", - "üäöß", false, - }, - {"Body: with emoji", TypeTextPlain, "📧", "📧", false}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m.SetBodyString(tt.ct, tt.value) - if len(m.parts) != 1 { - t.Errorf("SetBodyString() failed: no mail parts found") - } - part := m.parts[0] - res := bytes.Buffer{} - if _, err := part.writeFunc(&res); err != nil && !tt.sf { - t.Errorf("WriteFunc of part failed: %s", err) - } - if res.String() != tt.want { - t.Errorf("SetBodyString() failed. Expecteding: %s, got: %s", tt.want, res.String()) - } - }) - } -} - -// TestMsg_AddAlternativeString tests the Msg.AddAlternativeString method -func TestMsg_AddAlternativeString(t *testing.T) { - tests := []struct { - name string - value string - want string - sf bool - }{ - {"Body: test", "test", "test", false}, - {"Body: with Umlauts", "üäöß", "üäöß", false}, - {"Body: with emoji", "📧", "📧", false}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m.SetBodyString(TypeTextPlain, tt.value) - if len(m.parts) != 1 { - t.Errorf("AddAlternativeString() => SetBodyString() failed: no mail parts found") - } - m.AddAlternativeString(TypeTextHTML, tt.value) - if len(m.parts) != 2 { - t.Errorf("AddAlternativeString() failed: no alternative mail parts found") - } - apart := m.parts[1] - res := bytes.Buffer{} - if _, err := apart.writeFunc(&res); err != nil && !tt.sf { - t.Errorf("WriteFunc of part failed: %s", err) - } - if res.String() != tt.want { - t.Errorf("AddAlternativeString() failed. Expecteding: %s, got: %s", tt.want, res.String()) - } - }) - } -} - -// TestMsg_AttachFile tests the Msg.AttachFile and the WithFilename FileOption method -func TestMsg_AttachFile(t *testing.T) { - tests := []struct { - name string - file string - fn string - sf bool - }{ - {"File: README.md", "README.md", "README.md", false}, - {"File: doc.go", "doc.go", "foo.go", false}, - {"File: nonexisting", "", "invalid.file", true}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m.AttachFile(tt.file, WithFileName(tt.fn), nil) - if len(m.attachments) != 1 && !tt.sf { - t.Errorf("AttachFile() failed. Number of attachments expected: %d, got: %d", 1, - len(m.attachments)) - return - } - if !tt.sf { - file := m.attachments[0] - if file == nil { - t.Errorf("AttachFile() failed. Attachment file pointer is nil") - return - } - if file.Name != tt.fn { - t.Errorf("AttachFile() failed. Filename of attachment expected: %s, got: %s", tt.fn, - file.Name) - } - buf := bytes.Buffer{} - if _, err := file.Writer(&buf); err != nil { - t.Errorf("failed to execute WriterFunc: %s", err) - return - } - } - m.Reset() - }) - } -} - -// TestMsg_GetAttachments tests the Msg.GetAttachments method -func TestMsg_GetAttachments(t *testing.T) { - tests := []struct { - name string - files []string - }{ - {"File: README.md", []string{"README.md"}}, - {"File: doc.go", []string{"doc.go"}}, - {"File: README.md and doc.go", []string{"README.md", "doc.go"}}, - {"File: nonexisting", nil}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - for _, f := range tt.files { - m.AttachFile(f, WithFileName(f), nil) - } - if len(m.attachments) != len(tt.files) { - t.Errorf("AttachFile() failed. Number of attachments expected: %d, got: %d", len(tt.files), - len(m.attachments)) - return - } - ff := m.GetAttachments() - if len(m.attachments) != len(ff) { - t.Errorf("GetAttachments() failed. Number of attachments expected: %d, got: %d", len(m.attachments), - len(ff)) - return - } - var fn []string - for _, f := range ff { - fn = append(fn, f.Name) - } - sort.Strings(fn) - sort.Strings(tt.files) - for i, f := range tt.files { - if f != fn[i] { - t.Errorf("GetAttachments() failed. Attachment name expected: %s, got: %s", f, - fn[i]) - return - } - } - m.Reset() - }) - } -} - -// TestMsg_SetAttachments tests the Msg.GetAttachments method -func TestMsg_SetAttachments(t *testing.T) { - tests := []struct { - name string - attachments []string - files []string - }{ - {"File: replace README.md with doc.go", []string{"README.md"}, []string{"doc.go"}}, - {"File: add README.md with doc.go ", []string{"doc.go"}, []string{"README.md", "doc.go"}}, - {"File: remove README.md and doc.go", []string{"README.md", "doc.go"}, nil}, - {"File: add README.md and doc.go", nil, []string{"README.md", "doc.go"}}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - sort.Strings(tt.attachments) - sort.Strings(tt.files) - for _, a := range tt.attachments { - m.AttachFile(a, WithFileName(a), nil) - } - if len(m.attachments) != len(tt.attachments) { - t.Errorf("AttachFile() failed. Number of attachments expected: %d, got: %d", len(tt.files), - len(m.attachments)) - return - } - var files []*File - for _, f := range tt.files { - files = append(files, &File{Name: f}) - } - m.SetAttachments(files) - if len(m.attachments) != len(files) { - t.Errorf("SetAttachements() failed. Number of attachments expected: %d, got: %d", len(files), - len(m.attachments)) - return - } - for i, f := range tt.files { - if f != m.attachments[i].Name { - t.Errorf("SetAttachments() failed. Attachment name expected: %s, got: %s", f, - m.attachments[i].Name) - return - } - } - m.Reset() - }) - } -} - -// TestMsg_UnsetAllAttachments tests the Msg.UnsetAllAttachments method -func TestMsg_UnsetAllAttachments(t *testing.T) { - tests := []struct { - name string - attachments []string - }{ - {"File: one file", []string{"README.md"}}, - {"File: two files", []string{"README.md", "doc.go"}}, - {"File: nil", nil}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var files []*File - for _, f := range tt.attachments { - files = append(files, &File{Name: f}) - } - m.SetAttachments(files) - - if len(m.attachments) != len(files) { - t.Errorf("SetAttachements() failed. Number of attachments expected: %d, got: %d", len(files), - len(m.attachments)) - return - } - m.UnsetAllAttachments() - if m.attachments != nil { - t.Errorf("UnsetAllAttachments() failed. The attachments file's pointer is not nil") - return - } - m.Reset() - }) - } -} - -// TestMsg_GetEmbeds tests the Msg.GetEmbeds method -func TestMsg_GetEmbeds(t *testing.T) { - tests := []struct { - name string - files []string - }{ - {"File: README.md", []string{"README.md"}}, - {"File: doc.go", []string{"doc.go"}}, - {"File: README.md and doc.go", []string{"README.md", "doc.go"}}, - {"File: nonexisting", nil}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - for _, f := range tt.files { - m.EmbedFile(f, WithFileName(f), nil) - } - if len(m.embeds) != len(tt.files) { - t.Errorf("EmbedFile() failed. Number of embedded files expected: %d, got: %d", len(tt.files), - len(m.embeds)) - return - } - ff := m.GetEmbeds() - if len(m.embeds) != len(ff) { - t.Errorf("GetEmbeds() failed. Number of embedded files expected: %d, got: %d", len(m.embeds), - len(ff)) - return - } - var fn []string - for _, f := range ff { - fn = append(fn, f.Name) - } - sort.Strings(fn) - sort.Strings(tt.files) - for i, f := range tt.files { - if f != fn[i] { - t.Errorf("GetEmbeds() failed. Embedded file name expected: %s, got: %s", f, - fn[i]) - return - } - } - m.Reset() - }) - } -} - -// TestMsg_SetEmbeds tests the Msg.GetEmbeds method -func TestMsg_SetEmbeds(t *testing.T) { - tests := []struct { - name string - embeds []string - files []string - }{ - {"File: replace README.md with doc.go", []string{"README.md"}, []string{"doc.go"}}, - {"File: add README.md with doc.go ", []string{"doc.go"}, []string{"README.md", "doc.go"}}, - {"File: remove README.md and doc.go", []string{"README.md", "doc.go"}, nil}, - {"File: add README.md and doc.go", nil, []string{"README.md", "doc.go"}}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - sort.Strings(tt.embeds) - sort.Strings(tt.files) - for _, a := range tt.embeds { - m.EmbedFile(a, WithFileName(a), nil) - } - if len(m.embeds) != len(tt.embeds) { - t.Errorf("EmbedFile() failed. Number of embedded files expected: %d, got: %d", len(tt.files), - len(m.embeds)) - return - } - var files []*File - for _, f := range tt.files { - files = append(files, &File{Name: f}) - } - m.SetEmbeds(files) - if len(m.embeds) != len(files) { - t.Errorf("SetEmbeds() failed. Number of embedded files expected: %d, got: %d", len(files), - len(m.embeds)) - return - } - for i, f := range tt.files { - if f != m.embeds[i].Name { - t.Errorf("SetEmbeds() failed. Embedded file name expected: %s, got: %s", f, - m.embeds[i].Name) - return - } - } - m.Reset() - }) - } -} - -// TestMsg_UnsetAllEmbeds tests the Msg.TestMsg_UnsetAllEmbeds method -func TestMsg_UnsetAllEmbeds(t *testing.T) { - tests := []struct { - name string - embeds []string - }{ - {"File: one file", []string{"README.md"}}, - {"File: two files", []string{"README.md", "doc.go"}}, - {"File: nil", nil}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var files []*File - for _, f := range tt.embeds { - files = append(files, &File{Name: f}) - } - m.SetEmbeds(files) - if len(m.embeds) != len(files) { - t.Errorf("SetEmbeds() failed. Number of embedded files expected: %d, got: %d", len(files), - len(m.embeds)) - return - } - m.UnsetAllEmbeds() - if m.embeds != nil { - t.Errorf("UnsetAllEmbeds() failed. The embeds file's point is not nil") - return - } - m.Reset() - }) - } -} - -// TestMsg_UnsetAllParts tests the Msg.TestMsg_UnsetAllParts method -func TestMsg_UnsetAllParts(t *testing.T) { - tests := []struct { - name string - attachments []string - embeds []string - }{ - {"File: both is exist", []string{"README.md"}, []string{"doc.go"}}, - {"File: both is nil", nil, nil}, - {"File: attachment exist, embed nil", []string{"README.md"}, nil}, - {"File: attachment nil, embed exist", nil, []string{"README.md"}}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var attachments []*File - for _, f := range tt.attachments { - attachments = append(attachments, &File{Name: f}) - } - m.SetAttachments(attachments) - if len(m.attachments) != len(attachments) { - t.Errorf("SetAttachements() failed. Number of attachments files expected: %d, got: %d", - len(attachments), len(m.attachments)) - return - } - var embeds []*File - for _, f := range tt.embeds { - embeds = append(embeds, &File{Name: f}) - } - m.SetEmbeds(embeds) - if len(m.embeds) != len(embeds) { - t.Errorf("SetEmbeds() failed. Number of embedded files expected: %d, got: %d", len(embeds), - len(m.embeds)) - return - } - m.UnsetAllParts() - if m.attachments != nil { - t.Errorf("UnsetAllParts() failed. The attachments file's point is not nil") - return - } - if m.embeds != nil { - t.Errorf("UnsetAllParts() failed. The embeds file's point is not nil") - return - } - m.Reset() - }) - } -} - -// TestMsg_AttachFromEmbedFS tests the Msg.AttachFromEmbedFS and the WithFilename FileOption method -func TestMsg_AttachFromEmbedFS(t *testing.T) { - tests := []struct { - name string - file string - fn string - sf bool - }{ - {"File: README.md", "README.md", "README.md", false}, - {"File: nonexisting", "", "invalid.file", true}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := m.AttachFromEmbedFS(tt.file, &efs, WithFileName(tt.fn)); err != nil && !tt.sf { - t.Errorf("AttachFromEmbedFS() failed: %s", err) - return - } - if len(m.attachments) != 1 && !tt.sf { - t.Errorf("AttachFile() failed. Number of attachments expected: %d, got: %d", 1, - len(m.attachments)) - return - } - if !tt.sf { - file := m.attachments[0] - if file == nil { - t.Errorf("AttachFile() failed. Attachment file pointer is nil") - return - } - if file.Name != tt.fn { - t.Errorf("AttachFile() failed. Filename of attachment expected: %s, got: %s", tt.fn, - file.Name) - } - buf := bytes.Buffer{} - if _, err := file.Writer(&buf); err != nil { - t.Errorf("failed to execute WriterFunc: %s", err) - return - } - } - m.Reset() - }) - } -} - -// TestMsg_AttachFileBrokenFunc tests WriterFunc of the Msg.AttachFile method -func TestMsg_AttachFileBrokenFunc(t *testing.T) { - m := NewMsg() - m.AttachFile("README.md") - if len(m.attachments) != 1 { - t.Errorf("AttachFile() failed. Number of attachments expected: %d, got: %d", 1, - len(m.attachments)) - return - } - file := m.attachments[0] - if file == nil { - t.Errorf("AttachFile() failed. Attachment file pointer is nil") - return - } - file.Writer = func(io.Writer) (int64, error) { - return 0, fmt.Errorf("failing intentionally") - } - buf := bytes.Buffer{} - if _, err := file.Writer(&buf); err == nil { - t.Errorf("execute WriterFunc did not fail, but was expected to fail") - } -} - -// TestMsg_AttachReader tests the Msg.AttachReader method -func TestMsg_AttachReader(t *testing.T) { - m := NewMsg() - ts := "This is a test string" - rbuf := bytes.Buffer{} - rbuf.WriteString(ts) - r := bufio.NewReader(&rbuf) - if err := m.AttachReader("testfile.txt", r); err != nil { - t.Errorf("AttachReader() failed. Expected no error, got: %s", err.Error()) - return - } - if len(m.attachments) != 1 { - t.Errorf("AttachReader() failed. Number of attachments expected: %d, got: %d", 1, - len(m.attachments)) - return - } - file := m.attachments[0] - if file == nil { - t.Errorf("AttachReader() failed. Attachment file pointer is nil") - return - } - if file.Name != "testfile.txt" { - t.Errorf("AttachReader() failed. Expected file name: %s, got: %s", "testfile.txt", - file.Name) - } - wbuf := bytes.Buffer{} - if _, err := file.Writer(&wbuf); err != nil { - t.Errorf("execute WriterFunc failed: %s", err) - } - if wbuf.String() != ts { - t.Errorf("AttachReader() failed. Expected string: %q, got: %q", ts, wbuf.String()) - } -} - -// TestMsg_EmbedFile tests the Msg.EmbedFile and the WithFilename FileOption method -func TestMsg_EmbedFile(t *testing.T) { - tests := []struct { - name string - file string - fn string - sf bool - }{ - {"File: README.md", "README.md", "README.md", false}, - {"File: doc.go", "doc.go", "foo.go", false}, - {"File: nonexisting", "", "invalid.file", true}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m.EmbedFile(tt.file, WithFileName(tt.fn), nil) - if len(m.embeds) != 1 && !tt.sf { - t.Errorf("EmbedFile() failed. Number of embeds expected: %d, got: %d", 1, - len(m.embeds)) - return - } - if !tt.sf { - file := m.embeds[0] - if file == nil { - t.Errorf("EmbedFile() failed. Embedded file pointer is nil") - return - } - if file.Name != tt.fn { - t.Errorf("EmbedFile() failed. Filename of embeds expected: %s, got: %s", tt.fn, - file.Name) - } - buf := bytes.Buffer{} - if _, err := file.Writer(&buf); err != nil { - t.Errorf("failed to execute WriterFunc: %s", err) - return - } - } - m.Reset() - }) - } -} - -// TestMsg_EmbedFromEmbedFS tests the Msg.EmbedFromEmbedFS and the WithFilename FileOption method -func TestMsg_EmbedFromEmbedFS(t *testing.T) { - tests := []struct { - name string - file string - fn string - sf bool - }{ - {"File: README.md", "README.md", "README.md", false}, - {"File: nonexisting", "", "invalid.file", true}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := m.EmbedFromEmbedFS(tt.file, &efs, WithFileName(tt.fn)); err != nil && !tt.sf { - t.Errorf("EmbedFromEmbedFS() failed: %s", err) - return - } - if len(m.embeds) != 1 && !tt.sf { - t.Errorf("EmbedFile() failed. Number of embeds expected: %d, got: %d", 1, - len(m.embeds)) - return - } - if !tt.sf { - file := m.embeds[0] - if file == nil { - t.Errorf("EmbedFile() failed. Embedded file pointer is nil") - return - } - if file.Name != tt.fn { - t.Errorf("EmbedFile() failed. Filename of embeds expected: %s, got: %s", tt.fn, - file.Name) - } - buf := bytes.Buffer{} - if _, err := file.Writer(&buf); err != nil { - t.Errorf("failed to execute WriterFunc: %s", err) - return - } - } - m.Reset() - }) - } -} - -// TestMsg_EmbedFileBrokenFunc tests WriterFunc of the Msg.EmbedFile method -func TestMsg_EmbedFileBrokenFunc(t *testing.T) { - m := NewMsg() - m.EmbedFile("README.md") - if len(m.embeds) != 1 { - t.Errorf("EmbedFile() failed. Number of embeds expected: %d, got: %d", 1, - len(m.embeds)) - return - } - file := m.embeds[0] - if file == nil { - t.Errorf("EmbedFile() failed. Embedded file pointer is nil") - return - } - file.Writer = func(io.Writer) (int64, error) { - return 0, fmt.Errorf("failing intentionally") - } - buf := bytes.Buffer{} - if _, err := file.Writer(&buf); err == nil { - t.Errorf("execute WriterFunc did not fail, but was expected to fail") - } -} - -// TestMsg_EmbedReader tests the Msg.EmbedReader method -func TestMsg_EmbedReader(t *testing.T) { - m := NewMsg() - ts := "This is a test string" - rbuf := bytes.Buffer{} - rbuf.WriteString(ts) - r := bufio.NewReader(&rbuf) - if err := m.EmbedReader("testfile.txt", r); err != nil { - t.Errorf("EmbedReader() failed. Expected no error, got: %s", err.Error()) - return - } - if len(m.embeds) != 1 { - t.Errorf("EmbedReader() failed. Number of embeds expected: %d, got: %d", 1, - len(m.embeds)) - return - } - file := m.embeds[0] - if file == nil { - t.Errorf("EmbedReader() failed. Embedded file pointer is nil") - return - } - if file.Name != "testfile.txt" { - t.Errorf("EmbedReader() failed. Expected file name: %s, got: %s", "testfile.txt", - file.Name) - } - wbuf := bytes.Buffer{} - if _, err := file.Writer(&wbuf); err != nil { - t.Errorf("execute WriterFunc failed: %s", err) - } - if wbuf.String() != ts { - t.Errorf("EmbedReader() failed. Expected string: %q, got: %q", ts, wbuf.String()) - } -} - -// TestMsg_hasAlt tests the hasAlt() method of the Msg -func TestMsg_hasAlt(t *testing.T) { - m := NewMsg() - m.SetBodyString(TypeTextPlain, "Plain") - m.AddAlternativeString(TypeTextHTML, "HTML") - if !m.hasAlt() { - t.Errorf("mail has alternative parts but hasAlt() returned true") - } -} - -// TestMsg_hasRelated tests the hasRelated() method of the Msg -func TestMsg_hasRelated(t *testing.T) { - m := NewMsg() - m.SetBodyString(TypeTextPlain, "Plain") - m.EmbedFile("README.md") - if !m.hasRelated() { - t.Errorf("mail has related parts but hasRelated() returned true") - } -} - -// TestMsg_hasMixed tests the hasMixed() method of the Msg -func TestMsg_hasMixed(t *testing.T) { - m := NewMsg() - m.SetBodyString(TypeTextPlain, "Plain") - m.AttachFile("README.md") - if !m.hasMixed() { - t.Errorf("mail has mixed parts but hasMixed() returned true") - } -} - -// TestMsg_WriteTo tests the WriteTo() method of the Msg -func TestMsg_WriteTo(t *testing.T) { - m := NewMsg() - m.SetBodyString(TypeTextPlain, "Plain") - wbuf := bytes.Buffer{} - n, err := m.WriteTo(&wbuf) - if err != nil { - t.Errorf("WriteTo() failed: %s", err) - return - } - if n != int64(wbuf.Len()) { - t.Errorf("WriteTo() failed: expected written byte length: %d, got: %d", n, wbuf.Len()) - } -} - -// TestMsg_WriteToSkipMiddleware tests the WriteTo() method of the Msg -func TestMsg_WriteToSkipMiddleware(t *testing.T) { - m := NewMsg(WithMiddleware(encodeMiddleware{}), WithMiddleware(uppercaseMiddleware{})) - m.Subject("This is a test") - m.SetBodyString(TypeTextPlain, "Plain") - wbuf := bytes.Buffer{} - n, err := m.WriteToSkipMiddleware(&wbuf, "uppercase") - if err != nil { - t.Errorf("WriteToSkipMiddleware() failed: %s", err) - return - } - if n != int64(wbuf.Len()) { - t.Errorf("WriteToSkipMiddleware() failed: expected written byte length: %d, got: %d", n, wbuf.Len()) - } - if !strings.Contains(wbuf.String(), "Subject: This is @ test") { - t.Errorf("WriteToSkipMiddleware failed. Unable to find encoded subject") - } - - wbuf2 := bytes.Buffer{} - n, err = m.WriteTo(&wbuf2) - if err != nil { - t.Errorf("WriteTo() failed: %s", err) - return - } - if n != int64(wbuf2.Len()) { - t.Errorf("WriteTo() failed: expected written byte length: %d, got: %d", n, wbuf2.Len()) - } - if !strings.Contains(wbuf2.String(), "Subject: THIS IS @ TEST") { - t.Errorf("WriteToSkipMiddleware failed. Unable to find encoded and upperchase subject") - } -} - -// TestMsg_WriteTo_fails tests the WriteTo() method of the Msg but with a failing body writer function -func TestMsg_WriteTo_fails(t *testing.T) { - m := NewMsg() - m.SetBodyWriter(TypeTextPlain, func(io.Writer) (int64, error) { - return 0, errors.New("failed") - }) - _, err := m.WriteTo(io.Discard) - if err == nil { - t.Errorf("WriteTo() with failing BodyWriter function was supposed to fail, but didn't") - return - } - - // NoEncoding handles the errors separately - m = NewMsg(WithEncoding(NoEncoding)) - m.SetBodyWriter(TypeTextPlain, func(io.Writer) (int64, error) { - return 0, errors.New("failed") - }) - _, err = m.WriteTo(io.Discard) - if err == nil { - t.Errorf("WriteTo() (no encoding) with failing BodyWriter function was supposed to fail, but didn't") - return - } -} - -// TestMsg_Write tests the Write() method of the Msg -func TestMsg_Write(t *testing.T) { - m := NewMsg() - m.SetBodyString(TypeTextPlain, "Plain") - wbuf := bytes.Buffer{} - n, err := m.Write(&wbuf) - if err != nil { - t.Errorf("WriteTo() failed: %s", err) - return - } - if n != int64(wbuf.Len()) { - t.Errorf("WriteTo() failed: expected written byte length: %d, got: %d", n, wbuf.Len()) - } -} - -// TestMsg_WriteWithLongHeader tests the WriteTo() method of the Msg with a long header -func TestMsg_WriteWithLongHeader(t *testing.T) { - m := NewMsg() - m.SetBodyString(TypeTextPlain, "Plain") - m.SetGenHeader(HeaderContentLang, "de", "en", "fr", "es", "xxxx", "yyyy", "de", "en", "fr", - "es", "xxxx", "yyyy", "de", "en", "fr", "es", "xxxx", "yyyy", "de", "en", "fr") - m.SetGenHeader(HeaderContentID, "XXXXXXXXXXXXXXX XXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXX", - "XXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXX") - wbuf := bytes.Buffer{} - n, err := m.WriteTo(&wbuf) - if err != nil { - t.Errorf("WriteTo() failed: %s", err) - return - } - if n != int64(wbuf.Len()) { - t.Errorf("WriteTo() failed: expected written byte length: %d, got: %d", n, wbuf.Len()) - } -} - -// TestMsg_WriteDiffEncoding tests the WriteTo() method of the Msg with different Encoding -func TestMsg_WriteDiffEncoding(t *testing.T) { - tests := []struct { - name string - ct ContentType - en Encoding - alt bool - wa bool - we bool - }{ - {"Plain/QP/NoAlt/NoAttach/NoEmbed", TypeTextPlain, EncodingQP, false, false, false}, - {"Plain/B64/NoAlt/NoAttach/NoEmbed", TypeTextPlain, EncodingB64, false, false, false}, - {"Plain/No/NoAlt/NoAttach/NoEmbed", TypeTextPlain, NoEncoding, false, false, false}, - {"HTML/QP/NoAlt/NoAttach/NoEmbed", TypeTextHTML, EncodingQP, false, false, false}, - {"HTML/B64/NoAlt/NoAttach/NoEmbed", TypeTextHTML, EncodingB64, false, false, false}, - {"HTML/No/NoAlt/NoAttach/NoEmbed", TypeTextHTML, NoEncoding, false, false, false}, - {"Plain/QP/HTML/NoAttach/NoEmbed", TypeTextPlain, EncodingQP, true, false, false}, - {"Plain/B64/HTML/NoAttach/NoEmbed", TypeTextPlain, EncodingB64, true, false, false}, - {"Plain/No/HTML/NoAttach/NoEmbed", TypeTextPlain, NoEncoding, true, false, false}, - {"Plain/QP/NoAlt/Attach/NoEmbed", TypeTextPlain, EncodingQP, false, true, false}, - {"Plain/B64/NoAlt/Attach/NoEmbed", TypeTextPlain, EncodingB64, false, true, false}, - {"Plain/No/NoAlt/Attach/NoEmbed", TypeTextPlain, NoEncoding, false, true, false}, - {"Plain/QP/NoAlt/NoAttach/Embed", TypeTextPlain, EncodingQP, false, false, true}, - {"Plain/B64/NoAlt/NoAttach/Embed", TypeTextPlain, EncodingB64, false, false, true}, - {"Plain/No/NoAlt/NoAttach/Embed", TypeTextPlain, NoEncoding, false, false, true}, - {"Plain/QP/HTML/Attach/Embed", TypeTextPlain, EncodingQP, true, true, true}, - {"Plain/B64/HTML/Attach/Embed", TypeTextPlain, EncodingB64, true, true, true}, - {"Plain/No/HTML/Attach/Embed", TypeTextPlain, NoEncoding, true, true, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m := NewMsg(WithEncoding(tt.en)) - m.SetBodyString(tt.ct, tt.name) - if tt.alt { - m.AddAlternativeString(TypeTextHTML, fmt.Sprintf("

%s

", tt.name)) - } - if tt.wa { - m.AttachFile("README.md") - } - if tt.we { - m.EmbedFile("README.md") - } - wbuf := bytes.Buffer{} - n, err := m.WriteTo(&wbuf) - if err != nil { - t.Errorf("WriteTo() failed: %s", err) - return - } - if n != int64(wbuf.Len()) { - t.Errorf("WriteTo() failed: expected written byte length: %d, got: %d", n, wbuf.Len()) - } - wbuf.Reset() - }) - } -} - -// TestMsg_appendFile tests the appendFile() method of the Msg -func TestMsg_appendFile(t *testing.T) { - m := NewMsg() - var fl []*File - f := &File{ - Name: "file.txt", - } - fl = m.appendFile(fl, f, nil) - if len(fl) != 1 { - t.Errorf("appendFile() failed. Expected length: %d, got: %d", 1, len(fl)) - } - fl = m.appendFile(fl, f, nil) - if len(fl) != 2 { - t.Errorf("appendFile() failed. Expected length: %d, got: %d", 2, len(fl)) - } -} - -// TestMsg_multipleWrites tests multiple executions of WriteTo on the Msg -func TestMsg_multipleWrites(t *testing.T) { - ts := "XXX_UNIQUE_STRING_XXX" - wbuf := bytes.Buffer{} - m := NewMsg() - m.SetBodyString(TypeTextPlain, ts) - - // First WriteTo() - _, err := m.WriteTo(&wbuf) - if err != nil { - t.Errorf("failed to write body to buffer: %s", err) - } - if !strings.Contains(wbuf.String(), ts) { - t.Errorf("first WriteTo() body does not contain unique string: %s", ts) - } - - // Second WriteTo() - wbuf.Reset() - _, err = m.WriteTo(&wbuf) - if err != nil { - t.Errorf("failed to write body to buffer: %s", err) - } - if !strings.Contains(wbuf.String(), ts) { - t.Errorf("second WriteTo() body does not contain unique string: %s", ts) - } -} - -// TestMsg_NewReader tests the Msg.NewReader method -func TestMsg_NewReader(t *testing.T) { - m := NewMsg() - m.SetBodyString(TypeTextPlain, "TEST123") - mr := m.NewReader() - if mr == nil { - t.Errorf("NewReader failed: Reader is nil") - } - if mr.Error() != nil { - t.Errorf("NewReader failed: %s", mr.Error()) - } -} - -// TestMsg_NewReader_ioCopy tests the Msg.NewReader method using io.Copy -func TestMsg_NewReader_ioCopy(t *testing.T) { - wbuf1 := bytes.Buffer{} - wbuf2 := bytes.Buffer{} - m := NewMsg() - m.SetBodyString(TypeTextPlain, "TEST123") - mr := m.NewReader() - if mr == nil { - t.Errorf("NewReader failed: Reader is nil") - } - - // First we use WriteTo to have something to compare to - _, err := m.WriteTo(&wbuf1) - if err != nil { - t.Errorf("failed to write body to buffer: %s", err) - } - - // Then we write to wbuf2 via io.Copy - n, err := io.Copy(&wbuf2, mr) - if err != nil { - t.Errorf("failed to use io.Copy on Reader: %s", err) - } - if n != int64(wbuf1.Len()) { - t.Errorf("message length of WriteTo and io.Copy differ. Expected: %d, got: %d", wbuf1.Len(), n) - } - if wbuf1.String() != wbuf2.String() { - t.Errorf("message content of WriteTo and io.Copy differ") - } -} - -// TestMsg_UpdateReader tests the Msg.UpdateReader method -func TestMsg_UpdateReader(t *testing.T) { - m := NewMsg() - m.Subject("Subject-Run 1") - mr := m.NewReader() - if mr == nil { - t.Errorf("NewReader failed: Reader is nil") - } - wbuf1 := bytes.Buffer{} - _, err := io.Copy(&wbuf1, mr) - if err != nil { - t.Errorf("io.Copy on Reader failed: %s", err) - } - if !strings.Contains(wbuf1.String(), "Subject: Subject-Run 1") { - t.Errorf("io.Copy on Reader failed. Expected to find %q but string in Subject was not found", - "Subject-Run 1") - } - - m.Subject("Subject-Run 2") - m.UpdateReader(mr) - wbuf2 := bytes.Buffer{} - _, err = io.Copy(&wbuf2, mr) - if err != nil { - t.Errorf("2nd io.Copy on Reader failed: %s", err) - } - if !strings.Contains(wbuf2.String(), "Subject: Subject-Run 2") { - t.Errorf("io.Copy on Reader failed. Expected to find %q but string in Subject was not found", - "Subject-Run 2") - } -} - -// TestMsg_SetBodyTextTemplate tests the Msg.SetBodyTextTemplate method -func TestMsg_SetBodyTextTemplate(t *testing.T) { - tests := []struct { - name string - tpl string - ph string - sf bool - }{ - {"normal text", "This is a {{.Placeholder}}", "TemplateTest", false}, - {"invalid tpl", "This is a {{ foo .Placeholder}}", "TemplateTest", true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data := struct { - Placeholder string - }{Placeholder: tt.ph} - tpl, err := ttpl.New("test").Parse(tt.tpl) - if err != nil && !tt.sf { - t.Errorf("failed to render template: %s", err) - return - } - m := NewMsg() - if err := m.SetBodyTextTemplate(tpl, data); err != nil && !tt.sf { - t.Errorf("failed to set template as body: %s", err) - } - - wbuf := bytes.Buffer{} - _, err = m.WriteTo(&wbuf) - if err != nil { - t.Errorf("failed to write body to buffer: %s", err) - } - if !strings.Contains(wbuf.String(), tt.ph) && !tt.sf { - t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) - } - m.Reset() - }) - } -} - -// TestMsg_SetBodyHTMLTemplate tests the Msg.SetBodyHTMLTemplate method -func TestMsg_SetBodyHTMLTemplate(t *testing.T) { - tests := []struct { - name string - tpl string - ph string - ex string - sf bool - }{ - {"normal HTML", "

This is a {{.Placeholder}}

", "TemplateTest", "TemplateTest", false}, - { - "HTML with HTML", "

This is a {{.Placeholder}}

", "", - "<script>alert(1)</script>", false, - }, - {"invalid tpl", "

This is a {{ foo .Placeholder}}

", "TemplateTest", "", true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data := struct { - Placeholder string - }{Placeholder: tt.ph} - tpl, err := htpl.New("test").Parse(tt.tpl) - if err != nil && !tt.sf { - t.Errorf("failed to render template: %s", err) - return - } - m := NewMsg() - if err := m.SetBodyHTMLTemplate(tpl, data); err != nil && !tt.sf { - t.Errorf("failed to set template as body: %s", err) - } - - wbuf := bytes.Buffer{} - _, err = m.WriteTo(&wbuf) - if err != nil { - t.Errorf("failed to write body to buffer: %s", err) - } - if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf { - t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) - } - m.Reset() - }) - } -} - -// TestMsg_AddAlternativeTextTemplate tests the Msg.AddAlternativeTextTemplate method -func TestMsg_AddAlternativeTextTemplate(t *testing.T) { - tests := []struct { - name string - tpl string - ph string - sf bool - }{ - {"normal text", "This is a {{.Placeholder}}", "TemplateTest", false}, - {"invalid tpl", "This is a {{ foo .Placeholder}}", "TemplateTest", true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data := struct { - Placeholder string - }{Placeholder: tt.ph} - tpl, err := ttpl.New("test").Parse(tt.tpl) - if err != nil && !tt.sf { - t.Errorf("failed to render template: %s", err) - return - } - m := NewMsg() - m.SetBodyString(TypeTextHTML, "") - if err := m.AddAlternativeTextTemplate(tpl, data); err != nil && !tt.sf { - t.Errorf("failed to set template as alternative part: %s", err) - } - - wbuf := bytes.Buffer{} - _, err = m.WriteTo(&wbuf) - if err != nil { - t.Errorf("failed to write body to buffer: %s", err) - } - if !strings.Contains(wbuf.String(), tt.ph) && !tt.sf { - t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) - } - m.Reset() - }) - } -} - -// TestMsg_AddAlternativeHTMLTemplate tests the Msg.AddAlternativeHTMLTemplate method -func TestMsg_AddAlternativeHTMLTemplate(t *testing.T) { - tests := []struct { - name string - tpl string - ph string - ex string - sf bool - }{ - {"normal HTML", "

This is a {{.Placeholder}}

", "TemplateTest", "TemplateTest", false}, - { - "HTML with HTML", "

This is a {{.Placeholder}}

", "", - "<script>alert(1)</script>", false, - }, - {"invalid tpl", "

This is a {{ foo .Placeholder}}

", "TemplateTest", "", true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data := struct { - Placeholder string - }{Placeholder: tt.ph} - tpl, err := htpl.New("test").Parse(tt.tpl) - if err != nil && !tt.sf { - t.Errorf("failed to render template: %s", err) - return - } - m := NewMsg() - m.SetBodyString(TypeTextPlain, "") - if err := m.AddAlternativeHTMLTemplate(tpl, data); err != nil && !tt.sf { - t.Errorf("failed to set template as alternative part: %s", err) - } - - wbuf := bytes.Buffer{} - _, err = m.WriteTo(&wbuf) - if err != nil { - t.Errorf("failed to write body to buffer: %s", err) - } - if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf { - t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) - } - m.Reset() - }) - } -} - -// TestMsg_AttachTextTemplate tests the Msg.AttachTextTemplate method -func TestMsg_AttachTextTemplate(t *testing.T) { - tests := []struct { - name string - tpl string - ph string - ex string - ac int - sf bool - }{ - { - "normal text", "This is a {{.Placeholder}}", "TemplateTest", - "VGhpcyBpcyBhIFRlbXBsYXRlVGVzdA==", 1, false, - }, - {"invalid tpl", "This is a {{ foo .Placeholder}}", "TemplateTest", "", 0, true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data := struct { - Placeholder string - }{Placeholder: tt.ph} - tpl, err := ttpl.New("test").Parse(tt.tpl) - if err != nil && !tt.sf { - t.Errorf("failed to render template: %s", err) - return - } - m := NewMsg() - m.SetBodyString(TypeTextPlain, "This is the body") - if err := m.AttachTextTemplate("attachment.txt", tpl, data); err != nil && !tt.sf { - t.Errorf("failed to attach template: %s", err) - } - - wbuf := bytes.Buffer{} - _, err = m.WriteTo(&wbuf) - if err != nil { - t.Errorf("failed to write body to buffer: %s", err) - } - if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf { - t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) - } - if len(m.attachments) != tt.ac { - t.Errorf("wrong number of attachments. Expected: %d, got: %d", tt.ac, len(m.attachments)) - } - m.Reset() - }) - } -} - -// TestMsg_AttachHTMLTemplate tests the Msg.AttachHTMLTemplate method -func TestMsg_AttachHTMLTemplate(t *testing.T) { - tests := []struct { - name string - tpl string - ph string - ex string - ac int - sf bool - }{ - { - "normal HTML", "

This is a {{.Placeholder}}

", "TemplateTest", - "PHA+VGhpcyBpcyBhIFRlbXBsYXRlVGVzdDwvcD4=", 1, false, - }, - { - "HTML with HTML", "

This is a {{.Placeholder}}

", "", - "PHA+VGhpcyBpcyBhICZsdDtzY3JpcHQmZ3Q7YWxlcnQoMSkmbHQ7L3NjcmlwdCZndDs8L3A+", 1, false, - }, - {"invalid tpl", "

This is a {{ foo .Placeholder}}

", "TemplateTest", "", 0, true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data := struct { - Placeholder string - }{Placeholder: tt.ph} - tpl, err := htpl.New("test").Parse(tt.tpl) - if err != nil && !tt.sf { - t.Errorf("failed to render template: %s", err) - return - } - m := NewMsg() - m.SetBodyString(TypeTextPlain, "") - if err := m.AttachHTMLTemplate("attachment.html", tpl, data); err != nil && !tt.sf { - t.Errorf("failed to set template as alternative part: %s", err) - } - - wbuf := bytes.Buffer{} - _, err = m.WriteTo(&wbuf) - if err != nil { - t.Errorf("failed to write body to buffer: %s", err) - } - if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf { - t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) - } - if len(m.attachments) != tt.ac { - t.Errorf("wrong number of attachments. Expected: %d, got: %d", tt.ac, len(m.attachments)) - } - m.Reset() - }) - } -} - -// TestMsg_EmbedTextTemplate tests the Msg.EmbedTextTemplate method -func TestMsg_EmbedTextTemplate(t *testing.T) { - tests := []struct { - name string - tpl string - ph string - ex string - ec int - sf bool - }{ - { - "normal text", "This is a {{.Placeholder}}", "TemplateTest", - "VGhpcyBpcyBhIFRlbXBsYXRlVGVzdA==", 1, false, - }, - {"invalid tpl", "This is a {{ foo .Placeholder}}", "TemplateTest", "", 0, true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data := struct { - Placeholder string - }{Placeholder: tt.ph} - tpl, err := ttpl.New("test").Parse(tt.tpl) - if err != nil && !tt.sf { - t.Errorf("failed to render template: %s", err) - return - } - m := NewMsg() - m.SetBodyString(TypeTextPlain, "This is the body") - if err := m.EmbedTextTemplate("attachment.txt", tpl, data); err != nil && !tt.sf { - t.Errorf("failed to attach template: %s", err) - } - - wbuf := bytes.Buffer{} - _, err = m.WriteTo(&wbuf) - if err != nil { - t.Errorf("failed to write body to buffer: %s", err) - } - if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf { - t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) - } - if len(m.embeds) != tt.ec { - t.Errorf("wrong number of attachments. Expected: %d, got: %d", tt.ec, len(m.attachments)) - } - m.Reset() - }) - } -} - -// TestMsg_EmbedHTMLTemplate tests the Msg.EmbedHTMLTemplate method -func TestMsg_EmbedHTMLTemplate(t *testing.T) { - tests := []struct { - name string - tpl string - ph string - ex string - ec int - sf bool - }{ - { - "normal HTML", "

This is a {{.Placeholder}}

", "TemplateTest", - "PHA+VGhpcyBpcyBhIFRlbXBsYXRlVGVzdDwvcD4=", 1, false, - }, - { - "HTML with HTML", "

This is a {{.Placeholder}}

", "", - "PHA+VGhpcyBpcyBhICZsdDtzY3JpcHQmZ3Q7YWxlcnQoMSkmbHQ7L3NjcmlwdCZndDs8L3A+", 1, false, - }, - {"invalid tpl", "

This is a {{ foo .Placeholder}}

", "TemplateTest", "", 0, true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data := struct { - Placeholder string - }{Placeholder: tt.ph} - tpl, err := htpl.New("test").Parse(tt.tpl) - if err != nil && !tt.sf { - t.Errorf("failed to render template: %s", err) - return - } - m := NewMsg() - m.SetBodyString(TypeTextPlain, "") - if err := m.EmbedHTMLTemplate("attachment.html", tpl, data); err != nil && !tt.sf { - t.Errorf("failed to set template as alternative part: %s", err) - } - - wbuf := bytes.Buffer{} - _, err = m.WriteTo(&wbuf) - if err != nil { - t.Errorf("failed to write body to buffer: %s", err) - } - if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf { - t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) - } - if len(m.embeds) != tt.ec { - t.Errorf("wrong number of attachments. Expected: %d, got: %d", tt.ec, len(m.attachments)) - } - m.Reset() - }) - } -} - -// TestMsg_WriteToTempFile will test the output to temporary files -func TestMsg_WriteToTempFile(t *testing.T) { - m := NewMsg() - _ = m.From("Toni Tester ") - _ = m.To("Ellenor Tester ") - m.SetBodyString(TypeTextPlain, "This is a test") - f, err := m.WriteToTempFile() - if err != nil { - t.Errorf("failed to write message to temporary output file: %s", err) - } - _ = os.Remove(f) -} - -// TestMsg_WriteToFile will test the output to a file -func TestMsg_WriteToFile(t *testing.T) { - f, err := os.CreateTemp("", "go-mail-test_*.eml") - if err != nil { - t.Errorf("failed to create temporary output file: %s", err) - } - defer func() { - _ = f.Close() - _ = os.Remove(f.Name()) - }() - - m := NewMsg() - _ = m.From("Toni Tester ") - _ = m.To("Ellenor Tester ") - m.SetBodyString(TypeTextPlain, "This is a test") - if err := m.WriteToFile(f.Name()); err != nil { - t.Errorf("failed to write to output file: %s", err) - } - fi, err := os.Stat(f.Name()) - if err != nil { - t.Errorf("failed to stat output file: %s", err) - } - if fi == nil { - t.Errorf("received empty file handle") - return - } - if fi.Size() <= 0 { - t.Errorf("output file is expected to contain data but its size is zero") - } -} - -// TestMsg_GetGenHeader will test the GetGenHeader method of the Msg -func TestMsg_GetGenHeader(t *testing.T) { - m := NewMsg() - m.Subject("this is a test") - sa := m.GetGenHeader(HeaderSubject) - if len(sa) <= 0 { - t.Errorf("GetGenHeader on subject failed. Got empty slice") - return - } - if sa[0] == "" { - t.Errorf("GetGenHeader on subject failed. Got empty value") - } - if sa[0] != "this is a test" { - t.Errorf("GetGenHeader on subject failed. Expected: %q, got: %q", "this is a test", sa[0]) - } -} - -// TestMsg_GetAddrHeader will test the Msg.GetAddrHeader method -func TestMsg_GetAddrHeader(t *testing.T) { - m := NewMsg() - if err := m.FromFormat("Toni Sender", "sender@example.com"); err != nil { - t.Errorf("failed to set FROM address: %s", err) - } - if err := m.AddToFormat("Toni To", "to@example.com"); err != nil { - t.Errorf("failed to set TO address: %s", err) - } - if err := m.AddCcFormat("Toni Cc", "cc@example.com"); err != nil { - t.Errorf("failed to set CC address: %s", err) - } - if err := m.AddBccFormat("Toni Bcc", "bcc@example.com"); err != nil { - t.Errorf("failed to set BCC address: %s", err) - } - fh := m.GetAddrHeader(HeaderFrom) - if len(fh) <= 0 { - t.Errorf("GetAddrHeader on FROM failed. Got empty slice") - return - } - if fh[0].String() == "" { - t.Errorf("GetAddrHeader on FROM failed. Got empty value") - } - if fh[0].String() != `"Toni Sender" ` { - t.Errorf("GetAddrHeader on FROM failed. Expected: %q, got: %q", - `"Toni Sender" "`, fh[0].String()) - } - th := m.GetAddrHeader(HeaderTo) - if len(th) <= 0 { - t.Errorf("GetAddrHeader on TO failed. Got empty slice") - return - } - if th[0].String() == "" { - t.Errorf("GetAddrHeader on TO failed. Got empty value") - } - if th[0].String() != `"Toni To" ` { - t.Errorf("GetAddrHeader on TO failed. Expected: %q, got: %q", - `"Toni To" "`, th[0].String()) - } - ch := m.GetAddrHeader(HeaderCc) - if len(ch) <= 0 { - t.Errorf("GetAddrHeader on CC failed. Got empty slice") - return - } - if ch[0].String() == "" { - t.Errorf("GetAddrHeader on CC failed. Got empty value") - } - if ch[0].String() != `"Toni Cc" ` { - t.Errorf("GetAddrHeader on CC failed. Expected: %q, got: %q", - `"Toni Cc" "`, ch[0].String()) - } - bh := m.GetAddrHeader(HeaderBcc) - if len(bh) <= 0 { - t.Errorf("GetAddrHeader on BCC failed. Got empty slice") - return - } - if bh[0].String() == "" { - t.Errorf("GetAddrHeader on BCC failed. Got empty value") - } - if bh[0].String() != `"Toni Bcc" ` { - t.Errorf("GetAddrHeader on BCC failed. Expected: %q, got: %q", - `"Toni Bcc" "`, bh[0].String()) - } -} - -// TestMsg_GetFrom will test the Msg.GetFrom method -func TestMsg_GetFrom(t *testing.T) { - m := NewMsg() - if err := m.FromFormat("Toni Sender", "sender@example.com"); err != nil { - t.Errorf("failed to set FROM address: %s", err) - } - fh := m.GetFrom() - if len(fh) <= 0 { - t.Errorf("GetFrom failed. Got empty slice") - return - } - if fh[0].String() == "" { - t.Errorf("GetFrom failed. Got empty value") - } - if fh[0].String() != `"Toni Sender" ` { - t.Errorf("GetFrom failed. Expected: %q, got: %q", - `"Toni Sender" "`, fh[0].String()) - } -} - -// TestMsg_GetFromString will test the Msg.GetFromString method -func TestMsg_GetFromString(t *testing.T) { - m := NewMsg() - if err := m.FromFormat("Toni Sender", "sender@example.com"); err != nil { - t.Errorf("failed to set FROM address: %s", err) - } - fh := m.GetFromString() - if len(fh) <= 0 { - t.Errorf("GetFromString failed. Got empty slice") - return - } - if fh[0] == "" { - t.Errorf("GetFromString failed. Got empty value") - } - if fh[0] != `"Toni Sender" ` { - t.Errorf("GetFromString failed. Expected: %q, got: %q", - `"Toni Sender" "`, fh[0]) - } -} - -// TestMsg_GetTo will test the Msg.GetTo method -func TestMsg_GetTo(t *testing.T) { - m := NewMsg() - if err := m.AddToFormat("Toni To", "to@example.com"); err != nil { - t.Errorf("failed to set TO address: %s", err) - } - fh := m.GetTo() - if len(fh) <= 0 { - t.Errorf("GetTo failed. Got empty slice") - return - } - if fh[0].String() == "" { - t.Errorf("GetTo failed. Got empty value") - } - if fh[0].String() != `"Toni To" ` { - t.Errorf("GetTo failed. Expected: %q, got: %q", - `"Toni To" "`, fh[0].String()) - } -} - -// TestMsg_GetToString will test the Msg.GetToString method -func TestMsg_GetToString(t *testing.T) { - m := NewMsg() - if err := m.AddToFormat("Toni To", "to@example.com"); err != nil { - t.Errorf("failed to set TO address: %s", err) - } - fh := m.GetToString() - if len(fh) <= 0 { - t.Errorf("GetToString failed. Got empty slice") - return - } - if fh[0] == "" { - t.Errorf("GetToString failed. Got empty value") - } - if fh[0] != `"Toni To" ` { - t.Errorf("GetToString failed. Expected: %q, got: %q", - `"Toni To" "`, fh[0]) - } -} - -// TestMsg_GetCc will test the Msg.GetCc method -func TestMsg_GetCc(t *testing.T) { - m := NewMsg() - if err := m.AddCcFormat("Toni Cc", "cc@example.com"); err != nil { - t.Errorf("failed to set TO address: %s", err) - } - fh := m.GetCc() - if len(fh) <= 0 { - t.Errorf("GetCc failed. Got empty slice") - return - } - if fh[0].String() == "" { - t.Errorf("GetCc failed. Got empty value") - } - if fh[0].String() != `"Toni Cc" ` { - t.Errorf("GetCc failed. Expected: %q, got: %q", - `"Toni Cc" "`, fh[0].String()) - } -} - -// TestMsg_GetCcString will test the Msg.GetCcString method -func TestMsg_GetCcString(t *testing.T) { - m := NewMsg() - if err := m.AddCcFormat("Toni Cc", "cc@example.com"); err != nil { - t.Errorf("failed to set TO address: %s", err) - } - fh := m.GetCcString() - if len(fh) <= 0 { - t.Errorf("GetCcString failed. Got empty slice") - return - } - if fh[0] == "" { - t.Errorf("GetCcString failed. Got empty value") - } - if fh[0] != `"Toni Cc" ` { - t.Errorf("GetCcString failed. Expected: %q, got: %q", - `"Toni Cc" "`, fh[0]) - } -} - -// TestMsg_GetBcc will test the Msg.GetBcc method -func TestMsg_GetBcc(t *testing.T) { - m := NewMsg() - if err := m.AddBccFormat("Toni Bcc", "bcc@example.com"); err != nil { - t.Errorf("failed to set TO address: %s", err) - } - fh := m.GetBcc() - if len(fh) <= 0 { - t.Errorf("GetBcc failed. Got empty slice") - return - } - if fh[0].String() == "" { - t.Errorf("GetBcc failed. Got empty value") - } - if fh[0].String() != `"Toni Bcc" ` { - t.Errorf("GetBcc failed. Expected: %q, got: %q", - `"Toni Cc" "`, fh[0].String()) - } -} - -// TestMsg_GetBccString will test the Msg.GetBccString method -func TestMsg_GetBccString(t *testing.T) { - m := NewMsg() - if err := m.AddBccFormat("Toni Bcc", "bcc@example.com"); err != nil { - t.Errorf("failed to set TO address: %s", err) - } - fh := m.GetBccString() - if len(fh) <= 0 { - t.Errorf("GetBccString failed. Got empty slice") - return - } - if fh[0] == "" { - t.Errorf("GetBccString failed. Got empty value") - } - if fh[0] != `"Toni Bcc" ` { - t.Errorf("GetBccString failed. Expected: %q, got: %q", - `"Toni Cc" "`, fh[0]) - } -} - -// TestMsg_GetBoundary will test the Msg.GetBoundary method -func TestMsg_GetBoundary(t *testing.T) { - b := "random_boundary_string" - m := NewMsg() - if boundary := m.GetBoundary(); boundary != "" { - t.Errorf("GetBoundary failed. Expected empty string, but got: %s", boundary) - } - m = NewMsg(WithBoundary(b)) - if boundary := m.GetBoundary(); boundary != b { - t.Errorf("GetBoundary failed. Expected boundary: %s, got: %s", b, boundary) - } -} - -// TestMsg_AttachEmbedReader_consecutive tests the Msg.AttachReader and Msg.EmbedReader -// methods with consecutive calls to Msg.WriteTo to make sure the attachments are not -// lost (see Github issue #110) -func TestMsg_AttachEmbedReader_consecutive(t *testing.T) { - ts1 := "This is a test string" - ts2 := "Another test string" - m := NewMsg() - if err := m.AttachReader("attachment.txt", bytes.NewBufferString(ts1)); err != nil { - t.Errorf("AttachReader() failed. Expected no error, got: %s", err.Error()) - return - } - if err := m.EmbedReader("embedded.txt", bytes.NewBufferString(ts2)); err != nil { - t.Errorf("EmbedReader() failed. Expected no error, got: %s", err.Error()) - return - } - obuf1 := &bytes.Buffer{} - obuf2 := &bytes.Buffer{} - _, err := m.WriteTo(obuf1) - if err != nil { - t.Errorf("WriteTo to first output buffer failed: %s", err) - } - _, err = m.WriteTo(obuf2) - if err != nil { - t.Errorf("WriteTo to second output buffer failed: %s", err) - } - if !strings.Contains(obuf1.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { - t.Errorf("Expected file attachment string not found in first output buffer") - } - if !strings.Contains(obuf2.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { - t.Errorf("Expected file attachment string not found in second output buffer") - } - if !strings.Contains(obuf1.String(), "QW5vdGhlciB0ZXN0IHN0cmluZw==") { - t.Errorf("Expected embedded file string not found in first output buffer") - } - if !strings.Contains(obuf2.String(), "QW5vdGhlciB0ZXN0IHN0cmluZw==") { - t.Errorf("Expected embded file string not found in second output buffer") - } -} - -// TestMsg_AttachEmbedReadSeeker_consecutive tests the Msg.AttachReadSeeker and -// Msg.EmbedReadSeeker methods with consecutive calls to Msg.WriteTo to make -// sure the attachments are not lost (see Github issue #110) -func TestMsg_AttachEmbedReadSeeker_consecutive(t *testing.T) { - ts1 := []byte("This is a test string") - ts2 := []byte("Another test string") - m := NewMsg() - m.AttachReadSeeker("attachment.txt", bytes.NewReader(ts1)) - m.EmbedReadSeeker("embedded.txt", bytes.NewReader(ts2)) - obuf1 := &bytes.Buffer{} - obuf2 := &bytes.Buffer{} - _, err := m.WriteTo(obuf1) - if err != nil { - t.Errorf("WriteTo to first output buffer failed: %s", err) - } - _, err = m.WriteTo(obuf2) - if err != nil { - t.Errorf("WriteTo to second output buffer failed: %s", err) - } - if !strings.Contains(obuf1.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { - t.Errorf("Expected file attachment string not found in first output buffer") - } - if !strings.Contains(obuf2.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { - t.Errorf("Expected file attachment string not found in second output buffer") - } - if !strings.Contains(obuf1.String(), "QW5vdGhlciB0ZXN0IHN0cmluZw==") { - t.Errorf("Expected embedded file string not found in first output buffer") - } - if !strings.Contains(obuf2.String(), "QW5vdGhlciB0ZXN0IHN0cmluZw==") { - t.Errorf("Expected embded file string not found in second output buffer") - } -} - -// TestMsg_AttachReadSeeker tests the Msg.AttachReadSeeker method -func TestMsg_AttachReadSeeker(t *testing.T) { - m := NewMsg() - ts := []byte("This is a test string") - r := bytes.NewReader(ts) - m.AttachReadSeeker("testfile.txt", r) - if len(m.attachments) != 1 { - t.Errorf("AttachReadSeeker() failed. Number of attachments expected: %d, got: %d", 1, - len(m.attachments)) - return - } - file := m.attachments[0] - if file == nil { - t.Errorf("AttachReadSeeker() failed. Attachment file pointer is nil") - return - } - if file.Name != "testfile.txt" { - t.Errorf("AttachReadSeeker() failed. Expected file name: %s, got: %s", "testfile.txt", - file.Name) - } - wbuf := bytes.Buffer{} - if _, err := file.Writer(&wbuf); err != nil { - t.Errorf("execute WriterFunc failed: %s", err) - } - if wbuf.String() != string(ts) { - t.Errorf("AttachReadSeeker() failed. Expected string: %q, got: %q", ts, wbuf.String()) - } -} - -// TestMsg_EmbedReadSeeker tests the Msg.EmbedReadSeeker method -func TestMsg_EmbedReadSeeker(t *testing.T) { - m := NewMsg() - ts := []byte("This is a test string") - r := bytes.NewReader(ts) - m.EmbedReadSeeker("testfile.txt", r) - if len(m.embeds) != 1 { - t.Errorf("EmbedReadSeeker() failed. Number of attachments expected: %d, got: %d", 1, - len(m.embeds)) - return - } - file := m.embeds[0] - if file == nil { - t.Errorf("EmbedReadSeeker() failed. Embedded file pointer is nil") - return - } - if file.Name != "testfile.txt" { - t.Errorf("EmbedReadSeeker() failed. Expected file name: %s, got: %s", "testfile.txt", - file.Name) - } - wbuf := bytes.Buffer{} - if _, err := file.Writer(&wbuf); err != nil { - t.Errorf("execute WriterFunc failed: %s", err) - } - if wbuf.String() != string(ts) { - t.Errorf("EmbedReadSeeker() failed. Expected string: %q, got: %q", ts, wbuf.String()) - } -} - -// TestMsg_ToFromString tests Msg.ToFromString in different scenarios -func TestMsg_ToFromString(t *testing.T) { - tests := []struct { - n string - v string - w []*mail.Address - sf bool - }{ - {"valid single address", "test@test.com", []*mail.Address{ - {Name: "", Address: "test@test.com"}, - }, false}, - { - "valid multiple addresses", "test@test.com,test2@example.com", - []*mail.Address{ - {Name: "", Address: "test@test.com"}, - {Name: "", Address: "test2@example.com"}, - }, - false, - }, - { - "valid multiple addresses with space and name", - `test@test.com, "Toni Tester" `, - []*mail.Address{ - {Name: "", Address: "test@test.com"}, - {Name: "Toni Tester", Address: "test2@example.com"}, - }, - false, - }, - { - "invalid and valid multiple addresses", "test@test.com,test2#example.com", nil, - true, - }, - } - - for _, tt := range tests { - t.Run(tt.n, func(t *testing.T) { - m := NewMsg() - if err := m.ToFromString(tt.v); err != nil && !tt.sf { - t.Errorf("Msg.ToFromString failed: %s", err) - return - } - mto := m.GetTo() - if len(mto) != len(tt.w) { - t.Errorf("Msg.ToFromString failed, expected len: %d, got: %d", len(tt.w), - len(mto)) - return - } - for i := range mto { - w := tt.w[i] - g := mto[i] - if w.String() != g.String() { - t.Errorf("Msg.ToFromString failed, expected address: %s, got: %s", - w.String(), g.String()) - } - } - }) - } -} - -// TestMsg_CcFromString tests Msg.CcFromString in different scenarios -func TestMsg_CcFromString(t *testing.T) { - tests := []struct { - n string - v string - w []*mail.Address - sf bool - }{ - {"valid single address", "test@test.com", []*mail.Address{ - {Name: "", Address: "test@test.com"}, - }, false}, - { - "valid multiple addresses", "test@test.com,test2@example.com", - []*mail.Address{ - {Name: "", Address: "test@test.com"}, - {Name: "", Address: "test2@example.com"}, - }, - false, - }, - { - "valid multiple addresses with space and name", - `test@test.com, "Toni Tester" `, - []*mail.Address{ - {Name: "", Address: "test@test.com"}, - {Name: "Toni Tester", Address: "test2@example.com"}, - }, - false, - }, - { - "invalid and valid multiple addresses", "test@test.com,test2#example.com", nil, - true, - }, - } - - for _, tt := range tests { - t.Run(tt.n, func(t *testing.T) { - m := NewMsg() - if err := m.CcFromString(tt.v); err != nil && !tt.sf { - t.Errorf("Msg.CcFromString failed: %s", err) - return - } - mto := m.GetCc() - if len(mto) != len(tt.w) { - t.Errorf("Msg.CcFromString failed, expected len: %d, got: %d", len(tt.w), - len(mto)) - return - } - for i := range mto { - w := tt.w[i] - g := mto[i] - if w.String() != g.String() { - t.Errorf("Msg.CcFromString failed, expected address: %s, got: %s", - w.String(), g.String()) - } - } - }) - } -} - -// TestMsg_BccFromString tests Msg.BccFromString in different scenarios -func TestMsg_BccFromString(t *testing.T) { - tests := []struct { - n string - v string - w []*mail.Address - sf bool - }{ - {"valid single address", "test@test.com", []*mail.Address{ - {Name: "", Address: "test@test.com"}, - }, false}, - { - "valid multiple addresses", "test@test.com,test2@example.com", - []*mail.Address{ - {Name: "", Address: "test@test.com"}, - {Name: "", Address: "test2@example.com"}, - }, - false, - }, - { - "valid multiple addresses with space and name", - `test@test.com, "Toni Tester" `, - []*mail.Address{ - {Name: "", Address: "test@test.com"}, - {Name: "Toni Tester", Address: "test2@example.com"}, - }, - false, - }, - { - "invalid and valid multiple addresses", "test@test.com,test2#example.com", nil, - true, - }, - } - - for _, tt := range tests { - t.Run(tt.n, func(t *testing.T) { - m := NewMsg() - if err := m.BccFromString(tt.v); err != nil && !tt.sf { - t.Errorf("Msg.BccFromString failed: %s", err) - return - } - mto := m.GetBcc() - if len(mto) != len(tt.w) { - t.Errorf("Msg.BccFromString failed, expected len: %d, got: %d", len(tt.w), - len(mto)) - return - } - for i := range mto { - w := tt.w[i] - g := mto[i] - if w.String() != g.String() { - t.Errorf("Msg.BccFromString failed, expected address: %s, got: %s", - w.String(), g.String()) - } - } - }) - } -} - -// TestMsg_checkUserAgent tests the checkUserAgent method of the Msg -func TestMsg_checkUserAgent(t *testing.T) { - tests := []struct { - name string - noDefaultUserAgent bool - genHeader map[Header][]string - wantUserAgent string - sf bool - }{ - { - name: "check default user agent", - noDefaultUserAgent: false, - wantUserAgent: fmt.Sprintf("go-mail v%s // https://github.com/wneessen/go-mail", VERSION), - sf: false, - }, - { - name: "check no default user agent", - noDefaultUserAgent: true, - wantUserAgent: "", - sf: true, - }, - { - name: "check if ua and xm is already set", - noDefaultUserAgent: false, - genHeader: map[Header][]string{ - HeaderUserAgent: {"custom UA"}, - HeaderXMailer: {"custom XM"}, - }, - wantUserAgent: "custom UA", - sf: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - msg := &Msg{ - noDefaultUserAgent: tt.noDefaultUserAgent, - genHeader: tt.genHeader, - } - msg.checkUserAgent() - gotUserAgent := "" - if val, ok := msg.genHeader[HeaderUserAgent]; ok { - gotUserAgent = val[0] // Assuming the first one is the needed value - } - if gotUserAgent != tt.wantUserAgent && !tt.sf { - t.Errorf("UserAgent got = %v, want = %v", gotUserAgent, tt.wantUserAgent) - } - }) - } -} - -// TestNewMsgWithMIMEVersion tests WithMIMEVersion and Msg.SetMIMEVersion -func TestNewMsgWithNoDefaultUserAgent(t *testing.T) { - m := NewMsg(WithNoDefaultUserAgent()) - if m.noDefaultUserAgent != true { - t.Errorf("WithNoDefaultUserAgent() failed. Expected: %t, got: %t", true, false) - } -} - -// Fuzzing tests -func FuzzMsg_Subject(f *testing.F) { - f.Add("Testsubject") - f.Add("") - f.Add("This is a longer test subject.") - f.Add("Let's add some umlauts: üäöß") - f.Add("Or even emojis: ☝️💪👍") - f.Fuzz(func(t *testing.T, data string) { - m := NewMsg() - m.Subject(data) - m.Reset() - }) -} - -func FuzzMsg_From(f *testing.F) { - f.Add("Toni Tester ") - f.Add("") - f.Add("mail@server.com") - f.Fuzz(func(t *testing.T, data string) { - m := NewMsg() - if err := m.From(data); err != nil && - !strings.Contains(err.Error(), "failed to parse mail address") { - t.Errorf("failed set set FROM address: %s", err) - } - m.Reset() - }) -} From 120c2efd08923c35bda9b4deaab46fb5ead1084a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 11:18:09 +0200 Subject: [PATCH 057/188] 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. --- msg_test.go | 286 ++++++++++++++++++++++++++-------------------------- 1 file changed, 144 insertions(+), 142 deletions(-) diff --git a/msg_test.go b/msg_test.go index 1d4cf58..cc3ec1b 100644 --- a/msg_test.go +++ b/msg_test.go @@ -12,17 +12,83 @@ import ( "testing" ) +var ( + charsetTests = []struct { + name string + value Charset + want Charset + }{ + {"charset is UTF-7", CharsetUTF7, "UTF-7"}, + {"charset is UTF-8", CharsetUTF8, "UTF-8"}, + {"charset is US-ASCII", CharsetASCII, "US-ASCII"}, + {"charset is ISO-8859-1", CharsetISO88591, "ISO-8859-1"}, + {"charset is ISO-8859-2", CharsetISO88592, "ISO-8859-2"}, + {"charset is ISO-8859-3", CharsetISO88593, "ISO-8859-3"}, + {"charset is ISO-8859-4", CharsetISO88594, "ISO-8859-4"}, + {"charset is ISO-8859-5", CharsetISO88595, "ISO-8859-5"}, + {"charset is ISO-8859-6", CharsetISO88596, "ISO-8859-6"}, + {"charset is ISO-8859-7", CharsetISO88597, "ISO-8859-7"}, + {"charset is ISO-8859-9", CharsetISO88599, "ISO-8859-9"}, + {"charset is ISO-8859-13", CharsetISO885913, "ISO-8859-13"}, + {"charset is ISO-8859-14", CharsetISO885914, "ISO-8859-14"}, + {"charset is ISO-8859-15", CharsetISO885915, "ISO-8859-15"}, + {"charset is ISO-8859-16", CharsetISO885916, "ISO-8859-16"}, + {"charset is ISO-2022-JP", CharsetISO2022JP, "ISO-2022-JP"}, + {"charset is ISO-2022-KR", CharsetISO2022KR, "ISO-2022-KR"}, + {"charset is windows-1250", CharsetWindows1250, "windows-1250"}, + {"charset is windows-1251", CharsetWindows1251, "windows-1251"}, + {"charset is windows-1252", CharsetWindows1252, "windows-1252"}, + {"charset is windows-1255", CharsetWindows1255, "windows-1255"}, + {"charset is windows-1256", CharsetWindows1256, "windows-1256"}, + {"charset is KOI8-R", CharsetKOI8R, "KOI8-R"}, + {"charset is KOI8-U", CharsetKOI8U, "KOI8-U"}, + {"charset is Big5", CharsetBig5, "Big5"}, + {"charset is GB18030", CharsetGB18030, "GB18030"}, + {"charset is GB2312", CharsetGB2312, "GB2312"}, + {"charset is TIS-620", CharsetTIS620, "TIS-620"}, + {"charset is EUC-KR", CharsetEUCKR, "EUC-KR"}, + {"charset is Shift_JIS", CharsetShiftJIS, "Shift_JIS"}, + {"charset is GBK", CharsetGBK, "GBK"}, + {"charset is Unknown", CharsetUnknown, "Unknown"}, + } + encodingTests = []struct { + name string + value Encoding + want Encoding + }{ + {"encoding is Quoted-Printable", EncodingQP, "quoted-printable"}, + {"encoding is Base64", EncodingB64, "base64"}, + {"encoding is Unencoded 8-Bit", NoEncoding, "8bit"}, + {"encoding is US-ASCII 7-Bit", EncodingUSASCII, "7bit"}, + } + pgpTests = []struct { + name string + value PGPType + }{ + {"Not a PGP encoded message", NoPGP}, + {"PGP encrypted message", PGPEncrypt}, + {"PGP signed message", PGPSignature}, + } + boundaryTests = []struct { + name string + value string + }{ + {"boundary: test123", "test123"}, + {"boundary is empty", ""}, + } + mimeTests = []struct { + name string + value MIMEVersion + want MIMEVersion + }{ + {"MIME version: 1.0", MIME10, "1.0"}, + {"MIME version: 1.1", MIMEVersion("1.1"), "1.1"}, + } +) + //go:embed README.md var efs embed.FS -/* -addrHeader: make(map[AddrHeader][]*mail.Address), -charset: CharsetUTF8, -encoding: EncodingQP, -genHeader: make(map[Header][]string), -preformHeader: make(map[Header]string), -mimever: MIME10, -*/ func TestNewMsg(t *testing.T) { t.Run("create new message", func(t *testing.T) { message := NewMsg() @@ -67,45 +133,7 @@ func TestNewMsg(t *testing.T) { } }) t.Run("new message with custom charsets", func(t *testing.T) { - tests := []struct { - name string - value Charset - want Charset - }{ - {"charset is UTF-7", CharsetUTF7, "UTF-7"}, - {"charset is UTF-8", CharsetUTF8, "UTF-8"}, - {"charset is US-ASCII", CharsetASCII, "US-ASCII"}, - {"charset is ISO-8859-1", CharsetISO88591, "ISO-8859-1"}, - {"charset is ISO-8859-2", CharsetISO88592, "ISO-8859-2"}, - {"charset is ISO-8859-3", CharsetISO88593, "ISO-8859-3"}, - {"charset is ISO-8859-4", CharsetISO88594, "ISO-8859-4"}, - {"charset is ISO-8859-5", CharsetISO88595, "ISO-8859-5"}, - {"charset is ISO-8859-6", CharsetISO88596, "ISO-8859-6"}, - {"charset is ISO-8859-7", CharsetISO88597, "ISO-8859-7"}, - {"charset is ISO-8859-9", CharsetISO88599, "ISO-8859-9"}, - {"charset is ISO-8859-13", CharsetISO885913, "ISO-8859-13"}, - {"charset is ISO-8859-14", CharsetISO885914, "ISO-8859-14"}, - {"charset is ISO-8859-15", CharsetISO885915, "ISO-8859-15"}, - {"charset is ISO-8859-16", CharsetISO885916, "ISO-8859-16"}, - {"charset is ISO-2022-JP", CharsetISO2022JP, "ISO-2022-JP"}, - {"charset is ISO-2022-KR", CharsetISO2022KR, "ISO-2022-KR"}, - {"charset is windows-1250", CharsetWindows1250, "windows-1250"}, - {"charset is windows-1251", CharsetWindows1251, "windows-1251"}, - {"charset is windows-1252", CharsetWindows1252, "windows-1252"}, - {"charset is windows-1255", CharsetWindows1255, "windows-1255"}, - {"charset is windows-1256", CharsetWindows1256, "windows-1256"}, - {"charset is KOI8-R", CharsetKOI8R, "KOI8-R"}, - {"charset is KOI8-U", CharsetKOI8U, "KOI8-U"}, - {"charset is Big5", CharsetBig5, "Big5"}, - {"charset is GB18030", CharsetGB18030, "GB18030"}, - {"charset is GB2312", CharsetGB2312, "GB2312"}, - {"charset is TIS-620", CharsetTIS620, "TIS-620"}, - {"charset is EUC-KR", CharsetEUCKR, "EUC-KR"}, - {"charset is Shift_JIS", CharsetShiftJIS, "Shift_JIS"}, - {"charset is GBK", CharsetGBK, "GBK"}, - {"charset is Unknown", CharsetUnknown, "Unknown"}, - } - for _, tt := range tests { + for _, tt := range charsetTests { t.Run(tt.name, func(t *testing.T) { message := NewMsg(WithCharset(tt.value), nil) if message == nil { @@ -119,17 +147,7 @@ func TestNewMsg(t *testing.T) { } }) t.Run("new message with custom encoding", func(t *testing.T) { - tests := []struct { - name string - value Encoding - want Encoding - }{ - {"encoding is Quoted-Printable", EncodingQP, "quoted-printable"}, - {"encoding is Base64", EncodingB64, "base64"}, - {"encoding is Unencoded 8-Bit", NoEncoding, "8bit"}, - {"encoding is US-ASCII 7-Bit", EncodingUSASCII, "7bit"}, - } - for _, tt := range tests { + for _, tt := range encodingTests { t.Run(tt.name, func(t *testing.T) { message := NewMsg(WithEncoding(tt.value), nil) if message == nil { @@ -143,15 +161,7 @@ func TestNewMsg(t *testing.T) { } }) t.Run("new message with custom MIME version", func(t *testing.T) { - tests := []struct { - name string - value MIMEVersion - want MIMEVersion - }{ - {"MIME version: 1.0", MIME10, "1.0"}, - {"MIME version: 1.1", MIMEVersion("1.1"), "1.1"}, - } - for _, tt := range tests { + for _, tt := range mimeTests { t.Run(tt.name, func(t *testing.T) { message := NewMsg(WithMIMEVersion(tt.value)) if message == nil { @@ -165,14 +175,7 @@ func TestNewMsg(t *testing.T) { } }) t.Run("new message with custom boundary", func(t *testing.T) { - tests := []struct { - name string - value string - }{ - {"boundary: test123", "test123"}, - {"boundary is empty", ""}, - } - for _, tt := range tests { + for _, tt := range boundaryTests { t.Run(tt.name, func(t *testing.T) { message := NewMsg(WithBoundary(tt.value)) if message == nil { @@ -186,15 +189,7 @@ func TestNewMsg(t *testing.T) { } }) t.Run("new message with custom PGP type", func(t *testing.T) { - tests := []struct { - name string - value PGPType - }{ - {"Not a PGP encoded message", NoPGP}, - {"PGP encrypted message", PGPEncrypt}, - {"PGP signed message", PGPSignature}, - } - for _, tt := range tests { + for _, tt := range pgpTests { t.Run(tt.name, func(t *testing.T) { message := NewMsg(WithPGPType(tt.value)) if message == nil { @@ -240,46 +235,8 @@ func TestNewMsg(t *testing.T) { } func TestMsg_SetCharset(t *testing.T) { - tests := []struct { - name string - value Charset - want Charset - }{ - {"charset is UTF-7", CharsetUTF7, "UTF-7"}, - {"charset is UTF-8", CharsetUTF8, "UTF-8"}, - {"charset is US-ASCII", CharsetASCII, "US-ASCII"}, - {"charset is ISO-8859-1", CharsetISO88591, "ISO-8859-1"}, - {"charset is ISO-8859-2", CharsetISO88592, "ISO-8859-2"}, - {"charset is ISO-8859-3", CharsetISO88593, "ISO-8859-3"}, - {"charset is ISO-8859-4", CharsetISO88594, "ISO-8859-4"}, - {"charset is ISO-8859-5", CharsetISO88595, "ISO-8859-5"}, - {"charset is ISO-8859-6", CharsetISO88596, "ISO-8859-6"}, - {"charset is ISO-8859-7", CharsetISO88597, "ISO-8859-7"}, - {"charset is ISO-8859-9", CharsetISO88599, "ISO-8859-9"}, - {"charset is ISO-8859-13", CharsetISO885913, "ISO-8859-13"}, - {"charset is ISO-8859-14", CharsetISO885914, "ISO-8859-14"}, - {"charset is ISO-8859-15", CharsetISO885915, "ISO-8859-15"}, - {"charset is ISO-8859-16", CharsetISO885916, "ISO-8859-16"}, - {"charset is ISO-2022-JP", CharsetISO2022JP, "ISO-2022-JP"}, - {"charset is ISO-2022-KR", CharsetISO2022KR, "ISO-2022-KR"}, - {"charset is windows-1250", CharsetWindows1250, "windows-1250"}, - {"charset is windows-1251", CharsetWindows1251, "windows-1251"}, - {"charset is windows-1252", CharsetWindows1252, "windows-1252"}, - {"charset is windows-1255", CharsetWindows1255, "windows-1255"}, - {"charset is windows-1256", CharsetWindows1256, "windows-1256"}, - {"charset is KOI8-R", CharsetKOI8R, "KOI8-R"}, - {"charset is KOI8-U", CharsetKOI8U, "KOI8-U"}, - {"charset is Big5", CharsetBig5, "Big5"}, - {"charset is GB18030", CharsetGB18030, "GB18030"}, - {"charset is GB2312", CharsetGB2312, "GB2312"}, - {"charset is TIS-620", CharsetTIS620, "TIS-620"}, - {"charset is EUC-KR", CharsetEUCKR, "EUC-KR"}, - {"charset is Shift_JIS", CharsetShiftJIS, "Shift_JIS"}, - {"charset is GBK", CharsetGBK, "GBK"}, - {"charset is Unknown", CharsetUnknown, "Unknown"}, - } t.Run("SetCharset on new message", func(t *testing.T) { - for _, tt := range tests { + for _, tt := range charsetTests { t.Run(tt.name, func(t *testing.T) { message := NewMsg() if message == nil { @@ -309,18 +266,8 @@ func TestMsg_SetCharset(t *testing.T) { } func TestMsg_SetEncoding(t *testing.T) { - tests := []struct { - name string - value Encoding - want Encoding - }{ - {"encoding is Quoted-Printable", EncodingQP, "quoted-printable"}, - {"encoding is Base64", EncodingB64, "base64"}, - {"encoding is Unencoded 8-Bit", NoEncoding, "8bit"}, - {"encoding is US-ASCII 7-Bit", EncodingUSASCII, "7bit"}, - } t.Run("SetEncoding on new message", func(t *testing.T) { - for _, tt := range tests { + for _, tt := range encodingTests { t.Run(tt.name, func(t *testing.T) { message := NewMsg() if message == nil { @@ -350,15 +297,8 @@ func TestMsg_SetEncoding(t *testing.T) { } func TestMsg_SetBoundary(t *testing.T) { - tests := []struct { - name string - value string - }{ - {"boundary: test123", "test123"}, - {"boundary is empty", ""}, - } t.Run("SetBoundary on new message", func(t *testing.T) { - for _, tt := range tests { + for _, tt := range boundaryTests { t.Run(tt.name, func(t *testing.T) { message := NewMsg() if message == nil { @@ -387,6 +327,68 @@ func TestMsg_SetBoundary(t *testing.T) { }) } +func TestMsg_SetMIMEVersion(t *testing.T) { + t.Run("SetMIMEVersion on new message", func(t *testing.T) { + for _, tt := range mimeTests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetMIMEVersion(tt.value) + if message.mimever != tt.value { + t.Errorf("failed to set mime version. Expected: %s, got: %s", tt.value, message.mimever) + } + }) + } + }) + t.Run("SetMIMEVersion to override WithMIMEVersion", func(t *testing.T) { + message := NewMsg(WithMIMEVersion("1.1")) + if message == nil { + t.Fatal("message is nil") + } + if message.mimever != "1.1" { + t.Errorf("failed to set mime version on message creation. Expected: %s, got: %s", "1.1", + message.mimever) + } + message.SetMIMEVersion(MIME10) + if message.mimever != MIME10 { + t.Errorf("failed to set mime version. Expected: %s, got: %s", MIME10, message.mimever) + } + }) +} + +func TestMsg_SetPGPType(t *testing.T) { + t.Run("SetPGPType on new message", func(t *testing.T) { + for _, tt := range pgpTests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetPGPType(tt.value) + if message.pgptype != tt.value { + t.Errorf("failed to set pgp type. Expected: %d, got: %d", tt.value, message.pgptype) + } + }) + } + }) + t.Run("SetPGPType to override WithPGPType", func(t *testing.T) { + message := NewMsg(WithPGPType(PGPSignature)) + if message == nil { + t.Fatal("message is nil") + } + if message.pgptype != PGPSignature { + t.Errorf("failed to set pgp type on message creation. Expected: %d, got: %d", PGPSignature, + message.pgptype) + } + message.SetPGPType(PGPEncrypt) + if message.pgptype != PGPEncrypt { + t.Errorf("failed to set pgp type. Expected: %d, got: %d", PGPEncrypt, message.pgptype) + } + }) +} + /* // TestNewMsgWithMiddleware tests WithMiddleware From 1dba76948fd6c91129b419ad18d02b7d3a7972a7 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 11:24:35 +0200 Subject: [PATCH 058/188] 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. --- msg_test.go | 103 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 60 insertions(+), 43 deletions(-) diff --git a/msg_test.go b/msg_test.go index cc3ec1b..85f94b5 100644 --- a/msg_test.go +++ b/msg_test.go @@ -18,71 +18,71 @@ var ( value Charset want Charset }{ - {"charset is UTF-7", CharsetUTF7, "UTF-7"}, - {"charset is UTF-8", CharsetUTF8, "UTF-8"}, - {"charset is US-ASCII", CharsetASCII, "US-ASCII"}, - {"charset is ISO-8859-1", CharsetISO88591, "ISO-8859-1"}, - {"charset is ISO-8859-2", CharsetISO88592, "ISO-8859-2"}, - {"charset is ISO-8859-3", CharsetISO88593, "ISO-8859-3"}, - {"charset is ISO-8859-4", CharsetISO88594, "ISO-8859-4"}, - {"charset is ISO-8859-5", CharsetISO88595, "ISO-8859-5"}, - {"charset is ISO-8859-6", CharsetISO88596, "ISO-8859-6"}, - {"charset is ISO-8859-7", CharsetISO88597, "ISO-8859-7"}, - {"charset is ISO-8859-9", CharsetISO88599, "ISO-8859-9"}, - {"charset is ISO-8859-13", CharsetISO885913, "ISO-8859-13"}, - {"charset is ISO-8859-14", CharsetISO885914, "ISO-8859-14"}, - {"charset is ISO-8859-15", CharsetISO885915, "ISO-8859-15"}, - {"charset is ISO-8859-16", CharsetISO885916, "ISO-8859-16"}, - {"charset is ISO-2022-JP", CharsetISO2022JP, "ISO-2022-JP"}, - {"charset is ISO-2022-KR", CharsetISO2022KR, "ISO-2022-KR"}, - {"charset is windows-1250", CharsetWindows1250, "windows-1250"}, - {"charset is windows-1251", CharsetWindows1251, "windows-1251"}, - {"charset is windows-1252", CharsetWindows1252, "windows-1252"}, - {"charset is windows-1255", CharsetWindows1255, "windows-1255"}, - {"charset is windows-1256", CharsetWindows1256, "windows-1256"}, - {"charset is KOI8-R", CharsetKOI8R, "KOI8-R"}, - {"charset is KOI8-U", CharsetKOI8U, "KOI8-U"}, - {"charset is Big5", CharsetBig5, "Big5"}, - {"charset is GB18030", CharsetGB18030, "GB18030"}, - {"charset is GB2312", CharsetGB2312, "GB2312"}, - {"charset is TIS-620", CharsetTIS620, "TIS-620"}, - {"charset is EUC-KR", CharsetEUCKR, "EUC-KR"}, - {"charset is Shift_JIS", CharsetShiftJIS, "Shift_JIS"}, - {"charset is GBK", CharsetGBK, "GBK"}, - {"charset is Unknown", CharsetUnknown, "Unknown"}, + {"UTF-7", CharsetUTF7, "UTF-7"}, + {"UTF-8", CharsetUTF8, "UTF-8"}, + {"US-ASCII", CharsetASCII, "US-ASCII"}, + {"ISO-8859-1", CharsetISO88591, "ISO-8859-1"}, + {"ISO-8859-2", CharsetISO88592, "ISO-8859-2"}, + {"ISO-8859-3", CharsetISO88593, "ISO-8859-3"}, + {"ISO-8859-4", CharsetISO88594, "ISO-8859-4"}, + {"ISO-8859-5", CharsetISO88595, "ISO-8859-5"}, + {"ISO-8859-6", CharsetISO88596, "ISO-8859-6"}, + {"ISO-8859-7", CharsetISO88597, "ISO-8859-7"}, + {"ISO-8859-9", CharsetISO88599, "ISO-8859-9"}, + {"ISO-8859-13", CharsetISO885913, "ISO-8859-13"}, + {"ISO-8859-14", CharsetISO885914, "ISO-8859-14"}, + {"ISO-8859-15", CharsetISO885915, "ISO-8859-15"}, + {"ISO-8859-16", CharsetISO885916, "ISO-8859-16"}, + {"ISO-2022-JP", CharsetISO2022JP, "ISO-2022-JP"}, + {"ISO-2022-KR", CharsetISO2022KR, "ISO-2022-KR"}, + {"windows-1250", CharsetWindows1250, "windows-1250"}, + {"windows-1251", CharsetWindows1251, "windows-1251"}, + {"windows-1252", CharsetWindows1252, "windows-1252"}, + {"windows-1255", CharsetWindows1255, "windows-1255"}, + {"windows-1256", CharsetWindows1256, "windows-1256"}, + {"KOI8-R", CharsetKOI8R, "KOI8-R"}, + {"KOI8-U", CharsetKOI8U, "KOI8-U"}, + {"Big5", CharsetBig5, "Big5"}, + {"GB18030", CharsetGB18030, "GB18030"}, + {"GB2312", CharsetGB2312, "GB2312"}, + {"TIS-620", CharsetTIS620, "TIS-620"}, + {"EUC-KR", CharsetEUCKR, "EUC-KR"}, + {"Shift_JIS", CharsetShiftJIS, "Shift_JIS"}, + {"GBK", CharsetGBK, "GBK"}, + {"Unknown", CharsetUnknown, "Unknown"}, } encodingTests = []struct { name string value Encoding want Encoding }{ - {"encoding is Quoted-Printable", EncodingQP, "quoted-printable"}, - {"encoding is Base64", EncodingB64, "base64"}, - {"encoding is Unencoded 8-Bit", NoEncoding, "8bit"}, - {"encoding is US-ASCII 7-Bit", EncodingUSASCII, "7bit"}, + {"Quoted-Printable", EncodingQP, "quoted-printable"}, + {"Base64", EncodingB64, "base64"}, + {"Unencoded (8-Bit)", NoEncoding, "8bit"}, + {"US-ASCII (7-Bit)", EncodingUSASCII, "7bit"}, } pgpTests = []struct { name string value PGPType }{ - {"Not a PGP encoded message", NoPGP}, - {"PGP encrypted message", PGPEncrypt}, - {"PGP signed message", PGPSignature}, + {"No PGP encoding", NoPGP}, + {"PGP encrypted", PGPEncrypt}, + {"PGP signed", PGPSignature}, } boundaryTests = []struct { name string value string }{ - {"boundary: test123", "test123"}, - {"boundary is empty", ""}, + {"test123", "test123"}, + {"empty string", ""}, } mimeTests = []struct { name string value MIMEVersion want MIMEVersion }{ - {"MIME version: 1.0", MIME10, "1.0"}, - {"MIME version: 1.1", MIMEVersion("1.1"), "1.1"}, + {"1.0", MIME10, "1.0"}, + {"1.1 (not a valid version at this time)", MIMEVersion("1.1"), "1.1"}, } ) @@ -389,6 +389,23 @@ func TestMsg_SetPGPType(t *testing.T) { }) } +func TestMsg_Encoding(t *testing.T) { + t.Run("Encoding returns expected string", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + for _, tt := range encodingTests { + t.Run(tt.name, func(t *testing.T) { + message.SetEncoding(tt.value) + if message.Encoding() != tt.want.String() { + t.Errorf("failed to get encoding. Expected: %s, got: %s", tt.want.String(), message.Encoding()) + } + }) + } + }) +} + /* // TestNewMsgWithMiddleware tests WithMiddleware From 64aeb683baf43244863ab836712213137fd4ea12 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 11:35:25 +0200 Subject: [PATCH 059/188] 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. --- client_test.go | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/client_test.go b/client_test.go index c391c20..79488c8 100644 --- a/client_test.go +++ b/client_test.go @@ -1448,20 +1448,32 @@ func TestClient_SetSMTPAuthCustom(t *testing.T) { want string }{ {"CRAM-MD5", smtp.CRAMMD5Auth("", ""), "*smtp.cramMD5Auth"}, - {"LOGIN", smtp.LoginAuth("", "", "", false), - "*smtp.loginAuth"}, - {"LOGIN-NOENC", smtp.LoginAuth("", "", "", true), - "*smtp.loginAuth"}, - {"PLAIN", smtp.PlainAuth("", "", "", "", false), - "*smtp.plainAuth"}, - {"PLAIN-NOENC", smtp.PlainAuth("", "", "", "", true), - "*smtp.plainAuth"}, + { + "LOGIN", smtp.LoginAuth("", "", "", false), + "*smtp.loginAuth", + }, + { + "LOGIN-NOENC", smtp.LoginAuth("", "", "", true), + "*smtp.loginAuth", + }, + { + "PLAIN", smtp.PlainAuth("", "", "", "", false), + "*smtp.plainAuth", + }, + { + "PLAIN-NOENC", smtp.PlainAuth("", "", "", "", true), + "*smtp.plainAuth", + }, {"SCRAM-SHA-1", smtp.ScramSHA1Auth("", ""), "*smtp.scramAuth"}, - {"SCRAM-SHA-1-PLUS", smtp.ScramSHA1PlusAuth("", "", nil), - "*smtp.scramAuth"}, + { + "SCRAM-SHA-1-PLUS", smtp.ScramSHA1PlusAuth("", "", nil), + "*smtp.scramAuth", + }, {"SCRAM-SHA-256", smtp.ScramSHA256Auth("", ""), "*smtp.scramAuth"}, - {"SCRAM-SHA-256-PLUS", smtp.ScramSHA256PlusAuth("", "", nil), - "*smtp.scramAuth"}, + { + "SCRAM-SHA-256-PLUS", smtp.ScramSHA256PlusAuth("", "", nil), + "*smtp.scramAuth", + }, {"XOAUTH2", smtp.XOAuth2Auth("", ""), "*smtp.xoauth2Auth"}, } for _, tt := range tests { @@ -1483,7 +1495,6 @@ func TestClient_SetSMTPAuthCustom(t *testing.T) { t.Errorf("failed to set custom SMTP auth, expected auth method type: %s, got: %s", tt.want, authType) } - }) } }) @@ -2305,7 +2316,6 @@ func TestClient_auth(t *testing.T) { if err := client.Close(); err != nil { t.Errorf("failed to close client connection: %s", err) } - }) t.Run(tt.name+" should fail", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) From 425a190eb11c3fc9a97925bfcf8daade623bdb63 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 11:36:05 +0200 Subject: [PATCH 060/188] fumpt'ed formatting --- eml_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eml_test.go b/eml_test.go index b00098c..f89801a 100644 --- a/eml_test.go +++ b/eml_test.go @@ -901,7 +901,8 @@ func TestEMLToMsgFromReader(t *testing.T) { }{ { "RFC5322 A1.1 example mail", exampleMailRFC5322A11, EncodingUSASCII, - "Saying Hello"}, + "Saying Hello", + }, { "Plain text no encoding (7bit)", exampleMailPlain7Bit, EncodingUSASCII, "Example mail // plain text without encoding", @@ -1158,7 +1159,6 @@ func TestEMLToMsgFromFile(t *testing.T) { 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 { From 69c5f43cbf697ec4ddaf465fd423ce1fbf393838 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 11:36:17 +0200 Subject: [PATCH 061/188] 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. --- header_test.go | 99 ++++++++++++++++++++++++++------------------------ 1 file changed, 52 insertions(+), 47 deletions(-) diff --git a/header_test.go b/header_test.go index bfe1d12..02bab82 100644 --- a/header_test.go +++ b/header_test.go @@ -8,6 +8,54 @@ import ( "testing" ) +var ( + genHeaderTests = []struct { + name string + header 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"}, + {"Header: Content-Location", HeaderContentLocation, "Content-Location"}, + {"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"}, + {"Header: List-Unsubscribe-Post", HeaderListUnsubscribePost, "List-Unsubscribe-Post"}, + {"Header: Message-ID", HeaderMessageID, "Message-ID"}, + {"Header: MIME-Version", HeaderMIMEVersion, "MIME-Version"}, + {"Header: Organization", HeaderOrganization, "Organization"}, + {"Header: Precedence", HeaderPrecedence, "Precedence"}, + {"Header: Priority", HeaderPriority, "Priority"}, + {"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"}, + } + addrHeaderTests = []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"}, + } +) + func TestImportance_Stringer(t *testing.T) { tests := []struct { name string @@ -55,17 +103,7 @@ func TestImportance_Stringer(t *testing.T) { } func TestAddrHeader_Stringer(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 { + for _, tt := range addrHeaderTests { 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", @@ -76,44 +114,11 @@ func TestAddrHeader_Stringer(t *testing.T) { } func TestHeader_Stringer(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"}, - {"Header: Content-Location", HeaderContentLocation, "Content-Location"}, - {"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"}, - {"Header: List-Unsubscribe-Post", HeaderListUnsubscribePost, "List-Unsubscribe-Post"}, - {"Header: Message-ID", HeaderMessageID, "Message-ID"}, - {"Header: MIME-Version", HeaderMIMEVersion, "MIME-Version"}, - {"Header: Organization", HeaderOrganization, "Organization"}, - {"Header: Precedence", HeaderPrecedence, "Precedence"}, - {"Header: Priority", HeaderPriority, "Priority"}, - {"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 { + for _, tt := range genHeaderTests { t.Run(tt.name, func(t *testing.T) { - if tt.h.String() != tt.want { + if tt.header.String() != tt.want { t.Errorf("wrong string for Header returned. Expected: %s, got: %s", - tt.want, tt.h.String()) + tt.want, tt.header.String()) } }) } From a2e9dbae11deb1e7c4a08c8938b0f91809ffe11e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 11:36:26 +0200 Subject: [PATCH 062/188] 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. --- msg_test.go | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/msg_test.go b/msg_test.go index 85f94b5..c162947 100644 --- a/msg_test.go +++ b/msg_test.go @@ -230,7 +230,6 @@ func TestNewMsg(t *testing.T) { t.Errorf("NewMsg(WithNoDefaultUserAgent()) failed. Expected noDefaultUserAgent to be true, got: %t", message.noDefaultUserAgent) } - }) } @@ -406,6 +405,57 @@ func TestMsg_Encoding(t *testing.T) { }) } +func TestMsg_Charset(t *testing.T) { + t.Run("Charset returns expected string", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + for _, tt := range charsetTests { + t.Run(tt.name, func(t *testing.T) { + message.SetCharset(tt.value) + if message.Charset() != tt.want.String() { + t.Errorf("failed to get charset. Expected: %s, got: %s", tt.want.String(), message.Charset()) + } + }) + } + }) +} + +func TestMsg_SetHeader(t *testing.T) { + t.Run("SetHeader on new message", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + for _, tt := range genHeaderTests { + t.Run(tt.name, func(t *testing.T) { + message.SetHeader(tt.header, "test", "foo", "bar") + values, ok := message.genHeader[tt.header] + if !ok { + t.Fatalf("failed to set header, genHeader field for %s is not set", tt.header) + } + if len(values) != 3 { + t.Fatalf("failed to set header, genHeader value count for %s is %d, want: 3", + tt.header, len(values)) + } + if values[0] != "test" { + t.Errorf("failed to set header, genHeader value for %s is %s, want: %s", tt.header, + values[0], "test") + } + if values[1] != "foo" { + t.Errorf("failed to set header, genHeader value for %s is %s, want: %s", tt.header, + values[1], "foo") + } + if values[2] != "bar" { + t.Errorf("failed to set header, genHeader value for %s is %s, want: %s", tt.header, + values[1], "bar") + } + }) + } + }) +} + /* // TestNewMsgWithMiddleware tests WithMiddleware From 143e3b5b4fffe7a65fe4611ef3242a598895689c Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 11:46:48 +0200 Subject: [PATCH 063/188] 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. --- client_test.go | 71 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 9 deletions(-) diff --git a/client_test.go b/client_test.go index 79488c8..98a46b6 100644 --- a/client_test.go +++ b/client_test.go @@ -1796,7 +1796,7 @@ func TestClient_DialWithContext(t *testing.T) { } }) t.Run("connect should fail on HELO", func(t *testing.T) { - ctxFail, cancelFail := context.WithCancel(context.Background()) + ctxFail, cancelFail := context.WithCancel(ctx) defer cancelFail() PortAdder.Add(1) failServerPort := int(TestServerPortBase + PortAdder.Load()) @@ -1831,7 +1831,7 @@ func TestClient_DialWithContext(t *testing.T) { } }) t.Run("connect with failing auth", func(t *testing.T) { - ctxAuth, cancelAuth := context.WithCancel(context.Background()) + ctxAuth, cancelAuth := context.WithCancel(ctx) defer cancelAuth() PortAdder.Add(1) authServerPort := int(TestServerPortBase + PortAdder.Load()) @@ -1861,7 +1861,7 @@ func TestClient_DialWithContext(t *testing.T) { } }) t.Run("connect with STARTTLS", func(t *testing.T) { - ctxTLS, cancelTLS := context.WithCancel(context.Background()) + ctxTLS, cancelTLS := context.WithCancel(ctx) defer cancelTLS() PortAdder.Add(1) tlsServerPort := int(TestServerPortBase + PortAdder.Load()) @@ -1891,7 +1891,7 @@ func TestClient_DialWithContext(t *testing.T) { } }) t.Run("connect with STARTTLS Opportunisticly", func(t *testing.T) { - ctxTLS, cancelTLS := context.WithCancel(context.Background()) + ctxTLS, cancelTLS := context.WithCancel(ctx) defer cancelTLS() PortAdder.Add(1) tlsServerPort := int(TestServerPortBase + PortAdder.Load()) @@ -1921,7 +1921,7 @@ func TestClient_DialWithContext(t *testing.T) { } }) t.Run("connect with STARTTLS but fail", func(t *testing.T) { - ctxTLS, cancelTLS := context.WithCancel(context.Background()) + ctxTLS, cancelTLS := context.WithCancel(ctx) defer cancelTLS() PortAdder.Add(1) tlsServerPort := int(TestServerPortBase + PortAdder.Load()) @@ -1952,7 +1952,7 @@ func TestClient_DialWithContext(t *testing.T) { } }) t.Run("want STARTTLS, but server does not support it", func(t *testing.T) { - ctxTLS, cancelTLS := context.WithCancel(context.Background()) + ctxTLS, cancelTLS := context.WithCancel(ctx) defer cancelTLS() PortAdder.Add(1) tlsServerPort := int(TestServerPortBase + PortAdder.Load()) @@ -1982,7 +1982,7 @@ func TestClient_DialWithContext(t *testing.T) { } }) t.Run("connect with SSL", func(t *testing.T) { - ctxSSL, cancelSSL := context.WithCancel(context.Background()) + ctxSSL, cancelSSL := context.WithCancel(ctx) defer cancelSSL() PortAdder.Add(1) sslServerPort := int(TestServerPortBase + PortAdder.Load()) @@ -2494,6 +2494,9 @@ func TestClient_Send(t *testing.T) { t.Cleanup(cancelDial) client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } if err = client.DialWithContext(ctxDial); err != nil { t.Fatalf("failed to connect to test server: %s", err) } @@ -2508,6 +2511,9 @@ func TestClient_Send(t *testing.T) { }) t.Run("send with no connection should fail", func(t *testing.T) { client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } if err = client.Send(message); err == nil { t.Errorf("client should have failed to send email with no connection") } @@ -2540,6 +2546,9 @@ func TestClient_Send(t *testing.T) { t.Cleanup(cancelDial) client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } if err = client.DialWithContext(ctxDial); err != nil { t.Fatalf("failed to connect to test server: %s", err) } @@ -2594,6 +2603,9 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Cleanup(cancelDial) client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } if err = client.DialWithContext(ctxDial); err != nil { t.Fatalf("failed to connect to test server: %s", err) } @@ -2630,6 +2642,9 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Cleanup(cancelDial) client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } if err = client.DialWithContext(ctxDial); err != nil { t.Fatalf("failed to connect to test server: %s", err) } @@ -2668,6 +2683,9 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Cleanup(cancelDial) client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } if err = client.DialWithContext(ctxDial); err != nil { t.Fatalf("failed to connect to test server: %s", err) } @@ -2711,6 +2729,9 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Cleanup(cancelDial) client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } if err = client.DialWithContext(ctxDial); err != nil { t.Fatalf("failed to connect to test server: %s", err) } @@ -2754,6 +2775,9 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Cleanup(cancelDial) client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } if err = client.DialWithContext(ctxDial); err != nil { t.Fatalf("failed to connect to test server: %s", err) } @@ -2797,6 +2821,9 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Cleanup(cancelDial) client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS), WithDSN()) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } if err = client.DialWithContext(ctxDial); err != nil { t.Fatalf("failed to connect to test server: %s", err) } @@ -2833,6 +2860,9 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Cleanup(cancelDial) client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } if err = client.DialWithContext(ctxDial); err != nil { t.Fatalf("failed to connect to test server: %s", err) } @@ -2877,6 +2907,9 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Cleanup(cancelDial) client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } if err = client.DialWithContext(ctxDial); err != nil { t.Fatalf("failed to connect to test server: %s", err) } @@ -2921,6 +2954,9 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Cleanup(cancelDial) client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } if err = client.DialWithContext(ctxDial); err != nil { t.Fatalf("failed to connect to test server: %s", err) } @@ -2964,6 +3000,9 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Cleanup(cancelDial) client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } if err = client.DialWithContext(ctxDial); err != nil { t.Fatalf("failed to connect to test server: %s", err) } @@ -3007,6 +3046,9 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Cleanup(cancelDial) client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } if err = client.DialWithContext(ctxDial); err != nil { t.Fatalf("failed to connect to test server: %s", err) } @@ -3050,6 +3092,9 @@ func TestClient_checkConn(t *testing.T) { t.Cleanup(cancelDial) client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } if err = client.DialWithContext(ctxDial); err != nil { t.Fatalf("failed to connect to test server: %s", err) } @@ -3084,6 +3129,9 @@ func TestClient_checkConn(t *testing.T) { t.Cleanup(cancelDial) client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } if err = client.DialWithContext(ctxDial); err != nil { t.Fatalf("failed to connect to test server: %s", err) } @@ -3101,6 +3149,9 @@ func TestClient_checkConn(t *testing.T) { }) t.Run("connection should fail on no connection", func(t *testing.T) { client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } if err = client.checkConn(); err == nil { t.Errorf("client should have failed on connection check") } @@ -3429,11 +3480,14 @@ func simpleSMTPServer(ctx context.Context, t *testing.T, props *serverProps) err if props.SSLListener { keypair, err := tls.X509KeyPair(localhostCert, localhostKey) if err != nil { - return fmt.Errorf("failed to read TLS keypair: %s", err) + return fmt.Errorf("failed to read TLS keypair: %w", err) } tlsConfig := &tls.Config{Certificates: []tls.Certificate{keypair}} listener, err = tls.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, props.ListenPort), tlsConfig) + if err != nil { + t.Fatalf("failed to create TLS listener: %s", err) + } } else { listener, err = net.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, props.ListenPort)) } @@ -3514,7 +3568,6 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server break } writeLine("250-localhost.localdomain\r\n" + props.FeatureSet) - break case strings.HasPrefix(data, "MAIL FROM:"): if props.FailOnMailFrom { writeLine("500 5.5.2 Error: fail on MAIL FROM") From 9505f94e3d4a0a458eda491fec78722d4441bca8 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 12:18:12 +0200 Subject: [PATCH 064/188] 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. --- header_test.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/header_test.go b/header_test.go index 02bab82..a554936 100644 --- a/header_test.go +++ b/header_test.go @@ -45,14 +45,14 @@ var ( {"Header: X-Priority", HeaderXPriority, "X-Priority"}, } addrHeaderTests = []struct { - name string - ah AddrHeader - want string + name string + header AddrHeader + want string }{ - {"Address header: From", HeaderFrom, "From"}, - {"Address header: To", HeaderTo, "To"}, - {"Address header: Cc", HeaderCc, "Cc"}, - {"Address header: Bcc", HeaderBcc, "Bcc"}, + {"From", HeaderFrom, "From"}, + {"To", HeaderTo, "To"}, + {"Cc", HeaderCc, "Cc"}, + {"Bcc", HeaderBcc, "Bcc"}, } ) @@ -64,12 +64,12 @@ func TestImportance_Stringer(t *testing.T) { xprio string 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, "", "", ""}, + {"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 { @@ -105,9 +105,9 @@ func TestImportance_Stringer(t *testing.T) { func TestAddrHeader_Stringer(t *testing.T) { for _, tt := range addrHeaderTests { t.Run(tt.name, func(t *testing.T) { - if tt.ah.String() != tt.want { + if tt.header.String() != tt.want { t.Errorf("wrong string for AddrHeader returned. Expected: %s, got: %s", - tt.want, tt.ah.String()) + tt.want, tt.header.String()) } }) } From 7d352bc58ec7e75ef8cbe16fff742005b373b61e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 12:18:26 +0200 Subject: [PATCH 065/188] 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. --- msg_test.go | 303 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 303 insertions(+) diff --git a/msg_test.go b/msg_test.go index c162947..bbbd4a4 100644 --- a/msg_test.go +++ b/msg_test.go @@ -456,6 +456,309 @@ func TestMsg_SetHeader(t *testing.T) { }) } +func TestMsg_SetGenHeader(t *testing.T) { + t.Run("SetGenHeader on new message", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + for _, tt := range genHeaderTests { + t.Run(tt.name, func(t *testing.T) { + message.SetGenHeader(tt.header, "test", "foo", "bar") + values, ok := message.genHeader[tt.header] + if !ok { + t.Fatalf("failed to set header, genHeader field for %s is not set", tt.header) + } + if len(values) != 3 { + t.Fatalf("failed to set header, genHeader value count for %s is %d, want: 3", + tt.header, len(values)) + } + if values[0] != "test" { + t.Errorf("failed to set header, genHeader value for %s is %s, want: %s", tt.header, + values[0], "test") + } + if values[1] != "foo" { + t.Errorf("failed to set header, genHeader value for %s is %s, want: %s", tt.header, + values[1], "foo") + } + if values[2] != "bar" { + t.Errorf("failed to set header, genHeader value for %s is %s, want: %s", tt.header, + values[1], "bar") + } + }) + } + }) + t.Run("SetGenHeader with empty genHeaderMap", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.genHeader = nil + message.SetGenHeader(HeaderSubject, "test", "foo", "bar") + values, ok := message.genHeader[HeaderSubject] + if !ok { + t.Fatalf("failed to set header, genHeader field for %s is not set", HeaderSubject) + } + if len(values) != 3 { + t.Fatalf("failed to set header, genHeader value count for %s is %d, want: 3", + HeaderSubject, len(values)) + } + if values[0] != "test" { + t.Errorf("failed to set header, genHeader value for %s is %s, want: %s", HeaderSubject, + values[0], "test") + } + if values[1] != "foo" { + t.Errorf("failed to set header, genHeader value for %s is %s, want: %s", HeaderSubject, + values[1], "foo") + } + if values[2] != "bar" { + t.Errorf("failed to set header, genHeader value for %s is %s, want: %s", HeaderSubject, + values[1], "bar") + } + }) +} + +func TestMsg_SetHeaderPreformatted(t *testing.T) { + t.Run("SetHeaderPreformatted on new message", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + for _, tt := range genHeaderTests { + t.Run(tt.name, func(t *testing.T) { + message.SetHeaderPreformatted(tt.header, "test") + value, ok := message.preformHeader[tt.header] + if !ok { + t.Fatalf("failed to set header, genHeader field for %s is not set", tt.header) + } + if value != "test" { + t.Errorf("failed to set header, genHeader value for %s is %s, want: %s", tt.header, + value, "test") + } + }) + } + }) +} + +func TestMsg_SetGenHeaderPreformatted(t *testing.T) { + t.Run("SetGenHeaderPreformatted on new message", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + for _, tt := range genHeaderTests { + t.Run(tt.name, func(t *testing.T) { + message.SetGenHeaderPreformatted(tt.header, "test") + value, ok := message.preformHeader[tt.header] + if !ok { + t.Fatalf("failed to set header, genHeader field for %s is not set", tt.header) + } + if value != "test" { + t.Errorf("failed to set header, genHeader value for %s is %s, want: %s", tt.header, + value, "test") + } + }) + } + }) + t.Run("SetGenHeaderPreformatted with empty preformHeader map", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.preformHeader = nil + message.SetGenHeaderPreformatted(HeaderSubject, "test") + value, ok := message.preformHeader[HeaderSubject] + if !ok { + t.Fatalf("failed to set header, genHeader field for %s is not set", HeaderSubject) + } + if value != "test" { + t.Errorf("failed to set header, genHeader value for %s is %s, want: %s", HeaderSubject, + value, "test") + } + }) +} + +func TestMsg_SetAddrHeader(t *testing.T) { + t.Run("SetAddrHeader with valid address without <>", func(t *testing.T) { + for _, tt := range addrHeaderTests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.SetAddrHeader(tt.header, "toni.tester@example.com"); err != nil { + t.Fatalf("failed to set address header, err: %s", err) + } + addresses, ok := message.addrHeader[tt.header] + if !ok { + t.Fatalf("failed to set address header, addrHeader field for %s is not set", tt.header) + } + if len(addresses) != 1 { + t.Fatalf("failed to set address header, addrHeader value count for %s is %d, want: 1", + tt.header, len(addresses)) + } + if addresses[0].Address != "toni.tester@example.com" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, + addresses[0].Address, "toni.tester@example.com") + } + if addresses[0].String() != "" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, + addresses[0].String(), "") + } + if addresses[0].Name != "" { + t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ + "got: %s", tt.header, addresses[0].Name) + } + }) + } + }) + t.Run("SetAddrHeader with valid address with <>", func(t *testing.T) { + for _, tt := range addrHeaderTests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.SetAddrHeader(tt.header, ""); err != nil { + t.Fatalf("failed to set address header, err: %s", err) + } + addresses, ok := message.addrHeader[tt.header] + if !ok { + t.Fatalf("failed to set address header, addrHeader field for %s is not set", tt.header) + } + if len(addresses) != 1 { + t.Fatalf("failed to set address header, addrHeader value count for %s is %d, want: 1", + tt.header, len(addresses)) + } + if addresses[0].Address != "toni.tester@example.com" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, + addresses[0].Address, "toni.tester@example.com") + } + if addresses[0].String() != "" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, + addresses[0].String(), "") + } + if addresses[0].Name != "" { + t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ + "got: %s", tt.header, addresses[0].Name) + } + }) + } + }) + t.Run("SetAddrHeader with valid address with multiple addresses", func(t *testing.T) { + for _, tt := range addrHeaderTests { + t.Run(tt.name, func(t *testing.T) { + // From must only have one address + if tt.header == HeaderFrom { + return + } + + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.SetAddrHeader(tt.header, "toni.tester@example.com", + "tina.tester@example.com"); err != nil { + t.Fatalf("failed to set address header, err: %s", err) + } + addresses, ok := message.addrHeader[tt.header] + if !ok { + t.Fatalf("failed to set address header, addrHeader field for %s is not set", tt.header) + } + if len(addresses) != 2 { + t.Fatalf("failed to set address header, addrHeader value count for %s is %d, want: 2", + tt.header, len(addresses)) + } + if addresses[0].Address != "toni.tester@example.com" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, + addresses[0].Address, "toni.tester@example.com") + } + if addresses[0].String() != "" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, + addresses[0].String(), "") + } + if addresses[0].Name != "" { + t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ + "got: %s", tt.header, addresses[0].Name) + } + if addresses[1].Address != "tina.tester@example.com" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, + addresses[1].Address, "tina.tester@example.com") + } + if addresses[1].String() != "" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, + addresses[1].String(), "") + } + if addresses[1].Name != "" { + t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ + "got: %s", tt.header, addresses[1].Name) + } + }) + } + }) + t.Run("SetAddrHeader with multiple from addresses should only return the first one", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.SetAddrHeader(HeaderFrom, "toni.tester@example.com", + "tina.tester@example.com"); err != nil { + t.Fatalf("failed to set address header, err: %s", err) + } + addresses, ok := message.addrHeader[HeaderFrom] + if !ok { + t.Fatalf("failed to set address header, addrHeader field for %s is not set", HeaderFrom) + } + if len(addresses) != 1 { + t.Fatalf("failed to set address header, addrHeader value count for From is: %d, want: 1", + len(addresses)) + } + if addresses[0].Address != "toni.tester@example.com" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", HeaderFrom, + addresses[0].Address, "toni.tester@example.com") + } + if addresses[0].String() != "" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", HeaderFrom, + addresses[0].String(), "") + } + if addresses[0].Name != "" { + t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ + "got: %s", HeaderFrom, addresses[0].Name) + } + }) + t.Run("SetAddrHeader with addrHeader map is nil", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.addrHeader = nil + if err := message.SetAddrHeader(HeaderFrom, "toni.tester@example.com", + "tina.tester@example.com"); err != nil { + t.Fatalf("failed to set address header, err: %s", err) + } + addresses, ok := message.addrHeader[HeaderFrom] + if !ok { + t.Fatalf("failed to set address header, addrHeader field for %s is not set", HeaderFrom) + } + if len(addresses) != 1 { + t.Fatalf("failed to set address header, addrHeader value count for From is: %d, want: 1", + len(addresses)) + } + if addresses[0].Address != "toni.tester@example.com" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", HeaderFrom, + addresses[0].Address, "toni.tester@example.com") + } + if addresses[0].String() != "" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", HeaderFrom, + addresses[0].String(), "") + } + if addresses[0].Name != "" { + t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ + "got: %s", HeaderFrom, addresses[0].Name) + } + }) +} + /* // TestNewMsgWithMiddleware tests WithMiddleware From 08fe44c051c842669f9caab615bb3d4343d5490e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 14:57:36 +0200 Subject: [PATCH 066/188] 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 --- msg.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/msg.go b/msg.go index 61feda1..c3085c3 100644 --- a/msg.go +++ b/msg.go @@ -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. From c8dbc9a73539f630c74fd4d60ee72895ac7ab340 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 15:00:53 +0200 Subject: [PATCH 067/188] 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. --- msg_test.go | 227 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 225 insertions(+), 2 deletions(-) diff --git a/msg_test.go b/msg_test.go index bbbd4a4..115874a 100644 --- a/msg_test.go +++ b/msg_test.go @@ -645,7 +645,7 @@ func TestMsg_SetAddrHeader(t *testing.T) { }) } }) - t.Run("SetAddrHeader with valid address with multiple addresses", func(t *testing.T) { + t.Run("SetAddrHeader with multiple addresses", func(t *testing.T) { for _, tt := range addrHeaderTests { t.Run(tt.name, func(t *testing.T) { // From must only have one address @@ -696,7 +696,7 @@ func TestMsg_SetAddrHeader(t *testing.T) { }) } }) - t.Run("SetAddrHeader with multiple from addresses should only return the first one", func(t *testing.T) { + t.Run("SetAddrHeader with multiple addresses but from addresses should only return the first one", func(t *testing.T) { message := NewMsg() if message == nil { t.Fatal("message is nil") @@ -757,6 +757,229 @@ func TestMsg_SetAddrHeader(t *testing.T) { "got: %s", HeaderFrom, addresses[0].Name) } }) + t.Run("SetAddrHeader with invalid address", func(t *testing.T) { + for _, tt := range addrHeaderTests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.SetAddrHeader(HeaderFrom, "invalid"); err == nil { + t.Fatalf("SetAddrHeader with invalid address should fail") + } + }) + } + }) +} + +func TestMsg_SetAddrHeaderIgnoreInvalid(t *testing.T) { + t.Run("SetAddrHeaderIgnoreInvalid with valid address without <>", func(t *testing.T) { + for _, tt := range addrHeaderTests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetAddrHeaderIgnoreInvalid(tt.header, "toni.tester@example.com") + addresses, ok := message.addrHeader[tt.header] + if !ok { + t.Fatalf("failed to set address header, addrHeader field for %s is not set", tt.header) + } + if len(addresses) != 1 { + t.Fatalf("failed to set address header, addrHeader value count for %s is %d, want: 1", + tt.header, len(addresses)) + } + if addresses[0].Address != "toni.tester@example.com" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, + addresses[0].Address, "toni.tester@example.com") + } + if addresses[0].String() != "" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, + addresses[0].String(), "") + } + if addresses[0].Name != "" { + t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ + "got: %s", tt.header, addresses[0].Name) + } + }) + } + }) + t.Run("SetAddrHeaderIgnoreInvalid with valid address with <>", func(t *testing.T) { + for _, tt := range addrHeaderTests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetAddrHeaderIgnoreInvalid(tt.header, "") + addresses, ok := message.addrHeader[tt.header] + if !ok { + t.Fatalf("failed to set address header, addrHeader field for %s is not set", tt.header) + } + if len(addresses) != 1 { + t.Fatalf("failed to set address header, addrHeader value count for %s is %d, want: 1", + tt.header, len(addresses)) + } + if addresses[0].Address != "toni.tester@example.com" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, + addresses[0].Address, "toni.tester@example.com") + } + if addresses[0].String() != "" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, + addresses[0].String(), "") + } + if addresses[0].Name != "" { + t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ + "got: %s", tt.header, addresses[0].Name) + } + }) + } + }) + t.Run("SetAddrHeaderIgnoreInvalid with multiple valid addresses", func(t *testing.T) { + for _, tt := range addrHeaderTests { + t.Run(tt.name, func(t *testing.T) { + // From must only have one address + if tt.header == HeaderFrom { + return + } + + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetAddrHeaderIgnoreInvalid(tt.header, "toni.tester@example.com", + "tina.tester@example.com") + addresses, ok := message.addrHeader[tt.header] + if !ok { + t.Fatalf("failed to set address header, addrHeader field for %s is not set", tt.header) + } + if len(addresses) != 2 { + t.Fatalf("failed to set address header, addrHeader value count for %s is %d, want: 2", + tt.header, len(addresses)) + } + if addresses[0].Address != "toni.tester@example.com" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, + addresses[0].Address, "toni.tester@example.com") + } + if addresses[0].String() != "" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, + addresses[0].String(), "") + } + if addresses[0].Name != "" { + t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ + "got: %s", tt.header, addresses[0].Name) + } + if addresses[1].Address != "tina.tester@example.com" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, + addresses[1].Address, "tina.tester@example.com") + } + if addresses[1].String() != "" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, + addresses[1].String(), "") + } + if addresses[1].Name != "" { + t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ + "got: %s", tt.header, addresses[1].Name) + } + }) + } + }) + t.Run("SetAddrHeaderIgnoreInvalid with multiple addresses valid and invalid", func(t *testing.T) { + for _, tt := range addrHeaderTests { + t.Run(tt.name, func(t *testing.T) { + // From must only have one address + if tt.header == HeaderFrom { + return + } + + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetAddrHeaderIgnoreInvalid(tt.header, "toni.tester@example.com", + "invalid", "valid@address.tld") + addresses, ok := message.addrHeader[tt.header] + if !ok { + t.Fatalf("failed to set address header, addrHeader field for %s is not set", tt.header) + } + if len(addresses) != 2 { + t.Fatalf("failed to set address header, addrHeader value count for %s is %d, want: 2", + tt.header, len(addresses)) + } + if addresses[0].Address != "toni.tester@example.com" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, + addresses[0].Address, "toni.tester@example.com") + } + if addresses[0].String() != "" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, + addresses[0].String(), "") + } + if addresses[0].Name != "" { + t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ + "got: %s", tt.header, addresses[0].Name) + } + if addresses[1].Address != "valid@address.tld" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, + addresses[1].Address, "valid@address.tld") + } + if addresses[1].String() != "" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, + addresses[1].String(), "") + } + if addresses[1].Name != "" { + t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ + "got: %s", tt.header, addresses[1].Name) + } + }) + } + }) + t.Run("SetAddrHeaderIgnoreInvalid with addrHeader map is nil", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.addrHeader = nil + message.SetAddrHeaderIgnoreInvalid(HeaderFrom, "toni.tester@example.com", "tina.tester@example.com") + addresses, ok := message.addrHeader[HeaderFrom] + if !ok { + t.Fatalf("failed to set address header, addrHeader field for %s is not set", HeaderFrom) + } + if len(addresses) != 1 { + t.Fatalf("failed to set address header, addrHeader value count for From is: %d, want: 1", + len(addresses)) + } + if addresses[0].Address != "toni.tester@example.com" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", HeaderFrom, + addresses[0].Address, "toni.tester@example.com") + } + if addresses[0].String() != "" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", HeaderFrom, + addresses[0].String(), "") + } + if addresses[0].Name != "" { + t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ + "got: %s", HeaderFrom, addresses[0].Name) + } + }) + t.Run("SetAddrHeaderIgnoreInvalid with invalid addresses only", func(t *testing.T) { + for _, tt := range addrHeaderTests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetAddrHeaderIgnoreInvalid(HeaderTo, "invalid", "foo") + addresses, ok := message.addrHeader[HeaderTo] + if !ok { + t.Fatalf("failed to set address header, addrHeader field for %s is not set", HeaderTo) + } + if len(addresses) != 0 { + t.Fatalf("failed to set address header, addrHeader value count for To is: %d, want: 0", + len(addresses)) + } + }) + } + }) } /* From 1caa2cfb9225cc452cdc1fb9c4f923dbda50dcc1 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 15:14:06 +0200 Subject: [PATCH 068/188] 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. --- msg_test.go | 131 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/msg_test.go b/msg_test.go index 115874a..a9e54e6 100644 --- a/msg_test.go +++ b/msg_test.go @@ -645,6 +645,40 @@ func TestMsg_SetAddrHeader(t *testing.T) { }) } }) + t.Run("SetAddrHeader with valid address and name", func(t *testing.T) { + for _, tt := range addrHeaderTests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.SetAddrHeader(tt.header, fmt.Sprintf("%q <%s>", "Toni Tester", + "toni.tester@example.com")); err != nil { + t.Fatalf("failed to set address header, err: %s", err) + } + addresses, ok := message.addrHeader[tt.header] + if !ok { + t.Fatalf("failed to set address header, addrHeader field for %s is not set", tt.header) + } + if len(addresses) != 1 { + t.Fatalf("failed to set address header, addrHeader value count for %s is %d, want: 1", + tt.header, len(addresses)) + } + if addresses[0].Address != "toni.tester@example.com" { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, + addresses[0].Address, "toni.tester@example.com") + } + if addresses[0].String() != `"Toni Tester" ` { + t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, + addresses[0].String(), `"Toni Tester" `) + } + if addresses[0].Name != "Toni Tester" { + t.Errorf("failed to set address header, addrHeader name for %s expected to be %s, "+ + "got: %s", tt.header, "Toni Tester", addresses[0].Name) + } + }) + } + }) t.Run("SetAddrHeader with multiple addresses", func(t *testing.T) { for _, tt := range addrHeaderTests { t.Run(tt.name, func(t *testing.T) { @@ -982,6 +1016,103 @@ func TestMsg_SetAddrHeaderIgnoreInvalid(t *testing.T) { }) } +func TestMsg_EnvelopeFrom(t *testing.T) { + t.Run("EnvelopeFrom with valid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.EnvelopeFrom("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set envelope from: %s", err) + } + addresses, ok := message.addrHeader[HeaderEnvelopeFrom] + if !ok { + t.Fatalf("failed to set envelope from, addrHeader field is not set") + } + if len(addresses) != 1 { + t.Errorf("failed to set envelope from, addrHeader value count is: %d, want: 1", len(addresses)) + } + if addresses[0].Address != "toni.tester@example.com" { + t.Errorf("failed to set envelope from, addrHeader value is %s, want: %s", addresses[0].Address, + "toni.tester@example.com") + } + if addresses[0].String() != "" { + t.Errorf("failed to set envelope from, addrHeader value is %s, want: %s", addresses[0].String(), + "") + } + if addresses[0].Name != "" { + t.Errorf("failed to set envelope from, addrHeader name is %s, want: empty", addresses[0].Name) + } + }) + t.Run("EnvelopeFrom with invalid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.EnvelopeFrom("invalid"); err == nil { + t.Fatalf("EnvelopeFrom should fail with invalid address") + } + }) + t.Run("EnvelopeFrom with empty string should fail", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.EnvelopeFrom(""); err == nil { + t.Fatalf("EnvelopeFrom should fail with invalid address") + } + }) +} + +func TestMsg_EnvelopeFromFormat(t *testing.T) { + t.Run("EnvelopeFromFormat with valid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.EnvelopeFromFormat("Toni Tester", "toni.tester@example.com"); err != nil { + t.Fatalf("failed to set envelope from: %s", err) + } + addresses, ok := message.addrHeader[HeaderEnvelopeFrom] + if !ok { + t.Fatalf("failed to set envelope from, addrHeader field is not set") + } + if len(addresses) != 1 { + t.Errorf("failed to set envelope from, addrHeader value count is: %d, want: 1", len(addresses)) + } + if addresses[0].Address != "toni.tester@example.com" { + t.Errorf("failed to set envelope from, addrHeader value is %s, want: %s", addresses[0].Address, + "toni.tester@example.com") + } + if addresses[0].String() != `"Toni Tester" ` { + t.Errorf("failed to set envelope from, addrHeader value is %s, want: %s", addresses[0].String(), + "") + } + if addresses[0].Name != "Toni Tester" { + t.Errorf("failed to set envelope from, addrHeader name is %s, want: %s", addresses[0].Name, + "Toni Tester") + } + }) + t.Run("EnvelopeFromFormat with invalid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.EnvelopeFromFormat("Toni Tester", "invalid"); err == nil { + t.Fatalf("EnvelopeFromFormat should fail with invalid address") + } + }) + t.Run("EnvelopeFromFormat with empty string should fail", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.EnvelopeFromFormat("", ""); err == nil { + t.Fatalf("EnvelopeFromFormat should fail with invalid address") + } + }) +} + /* // TestNewMsgWithMiddleware tests WithMiddleware From 5d85be068daf6bd1b7786d2100a7715050c5aca8 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 16:36:34 +0200 Subject: [PATCH 069/188] 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. --- msg_test.go | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/msg_test.go b/msg_test.go index a9e54e6..3f8946e 100644 --- a/msg_test.go +++ b/msg_test.go @@ -84,6 +84,41 @@ var ( {"1.0", MIME10, "1.0"}, {"1.1 (not a valid version at this time)", MIMEVersion("1.1"), "1.1"}, } + // Inspired by https://www.youtube.com/watch?v=xxX81WmXjPg&t=623s, yet, some assumptions in that video are + // incorrect for RFC5321/RFC5322 but rely on deprecated information from RFC822. The tests have been + // adjusted accordingly. + rfc5322Test = []struct { + value string + valid bool + }{ + {"hi@domain.tld", true}, + {"hi@", false}, + {`hi+there@domain.tld`, true}, + {"hi.there@domain.tld", true}, + {"hi.@domain.tld", false}, // Point at the end of localpart is not allowed + {"hi..there@domain.tld", false}, // Double point is not allowed + {`!#$%&'(-/=?'@domain.tld`, false}, // Invalid characters + {"hi*there@domain.tld", true}, // * is allowed in localpart + {`#$%!^/&@domain.tld`, true}, // Allowed localpart characters + {"h(a)i@domain.tld", false}, // Not allowed to use parenthesis + {"(hi)there@domain.tld", false}, // The (hi) at the start is a comment which is allowed in RFC822 but not in RFC5322 anymore + {"hithere@domain.tld(tld)", true}, // The (tld) at the end is also a comment + {"hi@there@domain.tld", false}, // Can't have two @ signs + {`"hi@there"@domain.tld`, true}, // Quoted @-signs are allowed + {`"hi there"@domain.tld`, true}, // Quoted whitespaces are allowed + {`" "@domain.tld`, true}, // Still valid, since quoted + {`"<\"@\".!#%$@domain.tld"`, false}, // Quoting with illegal characters is not allowed + {`<\"@\\".!#%$@domain.tld`, false}, // Still a bunch of random illegal characters + {`hi"@"there@domain.tld`, false}, // Quotes must be dot-seperated + {`"<\"@\\".!.#%$@domain.tld`, false}, // Quote is escaped and dot-seperated which would be RFC822 compliant, but not RFC5322 compliant + {`hi\ there@domain.tld`, false}, // Spaces must be quoted + {`cow@[dead::beef]`, true}, // IPv6 is fine + {"hello@tld", true}, // TLD is enough + {`你好@域名.顶级域名`, true}, // We speak RFC6532 + {"1@23456789", true}, // Hypothetically valid, if somebody registers that TLD + {"1@[23456789]", false}, // While 23456789 is decimal for 1.101.236.21 it is not RFC5322 compliant + {"1@[1.101.236.21]", true}, // IPv4 is fine + } ) //go:embed README.md @@ -1113,6 +1148,71 @@ func TestMsg_EnvelopeFromFormat(t *testing.T) { }) } +func TestMsg_From(t *testing.T) { + t.Run("From with valid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.From("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set envelope from: %s", err) + } + addresses, ok := message.addrHeader[HeaderFrom] + if !ok { + t.Fatalf("failed to set envelope from, addrHeader field is not set") + } + if len(addresses) != 1 { + t.Errorf("failed to set envelope from, addrHeader value count is: %d, want: 1", len(addresses)) + } + if addresses[0].Address != "toni.tester@example.com" { + t.Errorf("failed to set envelope from, addrHeader value is %s, want: %s", addresses[0].Address, + "toni.tester@example.com") + } + if addresses[0].String() != "" { + t.Errorf("failed to set envelope from, addrHeader value is %s, want: %s", addresses[0].String(), + "") + } + if addresses[0].Name != "" { + t.Errorf("failed to set envelope from, addrHeader name is %s, want: empty", addresses[0].Name) + } + }) + t.Run("From with invalid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.From("invalid"); err == nil { + t.Fatalf("From should fail with invalid address") + } + }) + t.Run("From with empty string should fail", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.From(""); err == nil { + t.Fatalf("From should fail with invalid address") + } + }) + t.Run("From with different RFC5321 addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + for _, tt := range rfc5322Test { + t.Run(tt.value, func(t *testing.T) { + err := message.From(tt.value) + if err != nil && tt.valid { + t.Errorf("From on address %s should succeed, but failed with: %s", tt.value, err) + } + if err == nil && !tt.valid { + t.Errorf("From on address %s should fail, but succeeded", tt.value) + } + }) + } + }) +} + /* // TestNewMsgWithMiddleware tests WithMiddleware From aa46b408ad9ad8dcefa5f81c9a4281b161792a91 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 17:03:57 +0200 Subject: [PATCH 070/188] 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. --- msg_test.go | 184 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 168 insertions(+), 16 deletions(-) diff --git a/msg_test.go b/msg_test.go index 3f8946e..f33d1bd 100644 --- a/msg_test.go +++ b/msg_test.go @@ -112,12 +112,12 @@ var ( {`hi"@"there@domain.tld`, false}, // Quotes must be dot-seperated {`"<\"@\\".!.#%$@domain.tld`, false}, // Quote is escaped and dot-seperated which would be RFC822 compliant, but not RFC5322 compliant {`hi\ there@domain.tld`, false}, // Spaces must be quoted - {`cow@[dead::beef]`, true}, // IPv6 is fine {"hello@tld", true}, // TLD is enough {`你好@域名.顶级域名`, true}, // We speak RFC6532 + {`cow@[dead::beef]`, true}, // IPv6 is fine + {"1@[1.101.236.21]", true}, // IPv4 is fine {"1@23456789", true}, // Hypothetically valid, if somebody registers that TLD {"1@[23456789]", false}, // While 23456789 is decimal for 1.101.236.21 it is not RFC5322 compliant - {"1@[1.101.236.21]", true}, // IPv4 is fine } ) @@ -1065,7 +1065,7 @@ func TestMsg_EnvelopeFrom(t *testing.T) { t.Fatalf("failed to set envelope from, addrHeader field is not set") } if len(addresses) != 1 { - t.Errorf("failed to set envelope from, addrHeader value count is: %d, want: 1", len(addresses)) + t.Fatalf("failed to set envelope from, addrHeader value count is: %d, want: 1", len(addresses)) } if addresses[0].Address != "toni.tester@example.com" { t.Errorf("failed to set envelope from, addrHeader value is %s, want: %s", addresses[0].Address, @@ -1106,25 +1106,25 @@ func TestMsg_EnvelopeFromFormat(t *testing.T) { t.Fatal("message is nil") } if err := message.EnvelopeFromFormat("Toni Tester", "toni.tester@example.com"); err != nil { - t.Fatalf("failed to set envelope from: %s", err) + t.Fatalf("failed to set envelope From: %s", err) } addresses, ok := message.addrHeader[HeaderEnvelopeFrom] if !ok { - t.Fatalf("failed to set envelope from, addrHeader field is not set") + t.Fatalf("failed to set envelope From, addrHeader field is not set") } if len(addresses) != 1 { - t.Errorf("failed to set envelope from, addrHeader value count is: %d, want: 1", len(addresses)) + t.Fatalf("failed to set envelope From, addrHeader value count is: %d, want: 1", len(addresses)) } if addresses[0].Address != "toni.tester@example.com" { - t.Errorf("failed to set envelope from, addrHeader value is %s, want: %s", addresses[0].Address, + t.Errorf("failed to set envelope From, addrHeader value is %s, want: %s", addresses[0].Address, "toni.tester@example.com") } if addresses[0].String() != `"Toni Tester" ` { - t.Errorf("failed to set envelope from, addrHeader value is %s, want: %s", addresses[0].String(), + t.Errorf("failed to set envelope From, addrHeader value is %s, want: %s", addresses[0].String(), "") } if addresses[0].Name != "Toni Tester" { - t.Errorf("failed to set envelope from, addrHeader name is %s, want: %s", addresses[0].Name, + t.Errorf("failed to set envelope From, addrHeader name is %s, want: %s", addresses[0].Name, "Toni Tester") } }) @@ -1155,25 +1155,25 @@ func TestMsg_From(t *testing.T) { t.Fatal("message is nil") } if err := message.From("toni.tester@example.com"); err != nil { - t.Fatalf("failed to set envelope from: %s", err) + t.Fatalf("failed to set From: %s", err) } addresses, ok := message.addrHeader[HeaderFrom] if !ok { - t.Fatalf("failed to set envelope from, addrHeader field is not set") + t.Fatalf("failed to set From, addrHeader field is not set") } if len(addresses) != 1 { - t.Errorf("failed to set envelope from, addrHeader value count is: %d, want: 1", len(addresses)) + t.Fatalf("failed to set From, addrHeader value count is: %d, want: 1", len(addresses)) } if addresses[0].Address != "toni.tester@example.com" { - t.Errorf("failed to set envelope from, addrHeader value is %s, want: %s", addresses[0].Address, + t.Errorf("failed to set From, addrHeader value is %s, want: %s", addresses[0].Address, "toni.tester@example.com") } if addresses[0].String() != "" { - t.Errorf("failed to set envelope from, addrHeader value is %s, want: %s", addresses[0].String(), + t.Errorf("failed to set From, addrHeader value is %s, want: %s", addresses[0].String(), "") } if addresses[0].Name != "" { - t.Errorf("failed to set envelope from, addrHeader name is %s, want: empty", addresses[0].Name) + t.Errorf("failed to set From, addrHeader name is %s, want: empty", addresses[0].Name) } }) t.Run("From with invalid address", func(t *testing.T) { @@ -1194,7 +1194,7 @@ func TestMsg_From(t *testing.T) { t.Fatalf("From should fail with invalid address") } }) - t.Run("From with different RFC5321 addresses", func(t *testing.T) { + t.Run("From with different RFC5322 addresses", func(t *testing.T) { message := NewMsg() if message == nil { t.Fatal("message is nil") @@ -1213,6 +1213,158 @@ func TestMsg_From(t *testing.T) { }) } +func TestMsg_FromFormat(t *testing.T) { + t.Run("FromFormat with valid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.FromFormat("Toni Tester", "toni.tester@example.com"); err != nil { + t.Fatalf("failed to set From: %s", err) + } + addresses, ok := message.addrHeader[HeaderFrom] + if !ok { + t.Fatalf("failed to set From, addrHeader field is not set") + } + if len(addresses) != 1 { + t.Fatalf("failed to set From, addrHeader value count is: %d, want: 1", len(addresses)) + } + if addresses[0].Address != "toni.tester@example.com" { + t.Errorf("failed to set From, addrHeader value is %s, want: %s", addresses[0].Address, + "toni.tester@example.com") + } + if addresses[0].String() != `"Toni Tester" ` { + t.Errorf("failed to set From, addrHeader value is %s, want: %s", addresses[0].String(), + "") + } + if addresses[0].Name != "Toni Tester" { + t.Errorf("failed to set From, addrHeader name is %s, want: %s", addresses[0].Name, + "Toni Tester") + } + }) + t.Run("FromFormat with invalid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.FromFormat("Toni Tester", "invalid"); err == nil { + t.Fatalf("FromFormat should fail with invalid address") + } + }) + t.Run("FromFormat with empty string should fail", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.FromFormat("", ""); err == nil { + t.Fatalf("FromFormat should fail with invalid address") + } + }) +} + +func TestMsg_To(t *testing.T) { + t.Run("To with valid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.To("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set To: %s", err) + } + addresses, ok := message.addrHeader[HeaderTo] + if !ok { + t.Fatalf("failed to set To, addrHeader field is not set") + } + if len(addresses) != 1 { + t.Fatalf("failed to set To, addrHeader value count is: %d, want: 1", len(addresses)) + } + if addresses[0].Address != "toni.tester@example.com" { + t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[0].Address, + "toni.tester@example.com") + } + if addresses[0].String() != "" { + t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[0].String(), + "") + } + if addresses[0].Name != "" { + t.Errorf("failed to set To, addrHeader name is %s, want: empty", addresses[0].Name) + } + }) + t.Run("To with multiple valid addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.To("toni.tester@example.com", "tina.tester@example.com"); err != nil { + t.Fatalf("failed to set To: %s", err) + } + addresses, ok := message.addrHeader[HeaderTo] + if !ok { + t.Fatalf("failed to set To, addrHeader field is not set") + } + if len(addresses) != 2 { + t.Fatalf("failed to set To, addrHeader value count is: %d, want: 2", len(addresses)) + } + if addresses[0].Address != "toni.tester@example.com" { + t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[0].Address, + "toni.tester@example.com") + } + if addresses[0].String() != "" { + t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[0].String(), + "") + } + if addresses[0].Name != "" { + t.Errorf("failed to set To, addrHeader name is %s, want: empty", addresses[0].Name) + } + if addresses[1].Address != "tina.tester@example.com" { + t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[0].Address, + "tina.tester@example.com") + } + if addresses[1].String() != "" { + t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[0].String(), + "") + } + if addresses[1].Name != "" { + t.Errorf("failed to set To, addrHeader name is %s, want: empty", addresses[0].Name) + } + }) + t.Run("To with invalid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.To("invalid"); err == nil { + t.Fatalf("To should fail with invalid address") + } + }) + t.Run("To with empty string should fail", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.To(""); err == nil { + t.Fatalf("To should fail with invalid address") + } + }) + t.Run("To with different RFC5322 addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + for _, tt := range rfc5322Test { + t.Run(tt.value, func(t *testing.T) { + err := message.To(tt.value) + if err != nil && tt.valid { + t.Errorf("To on address %s should succeed, but failed with: %s", tt.value, err) + } + if err == nil && !tt.valid { + t.Errorf("To on address %s should fail, but succeeded", tt.value) + } + }) + } + }) +} + /* // TestNewMsgWithMiddleware tests WithMiddleware From cb85a136c39f622d65a2333a2beebbe665665d9f Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 17:05:22 +0200 Subject: [PATCH 071/188] 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. --- msg_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/msg_test.go b/msg_test.go index f33d1bd..b5534ac 100644 --- a/msg_test.go +++ b/msg_test.go @@ -465,6 +465,7 @@ func TestMsg_SetHeader(t *testing.T) { } for _, tt := range genHeaderTests { t.Run(tt.name, func(t *testing.T) { + //goland:noinspection GoDeprecation message.SetHeader(tt.header, "test", "foo", "bar") values, ok := message.genHeader[tt.header] if !ok { @@ -561,6 +562,7 @@ func TestMsg_SetHeaderPreformatted(t *testing.T) { } for _, tt := range genHeaderTests { t.Run(tt.name, func(t *testing.T) { + //goland:noinspection GoDeprecation message.SetHeaderPreformatted(tt.header, "test") value, ok := message.preformHeader[tt.header] if !ok { From a7f81baa4b51a6a1192cfb4152cc18fe3dfbc780 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 17:13:22 +0200 Subject: [PATCH 072/188] 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. --- msg_test.go | 263 +++++++++++++--------------------------------------- 1 file changed, 66 insertions(+), 197 deletions(-) diff --git a/msg_test.go b/msg_test.go index b5534ac..38ff685 100644 --- a/msg_test.go +++ b/msg_test.go @@ -1319,15 +1319,15 @@ func TestMsg_To(t *testing.T) { t.Errorf("failed to set To, addrHeader name is %s, want: empty", addresses[0].Name) } if addresses[1].Address != "tina.tester@example.com" { - t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[0].Address, + t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[1].Address, "tina.tester@example.com") } if addresses[1].String() != "" { - t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[0].String(), + t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[1].String(), "") } if addresses[1].Name != "" { - t.Errorf("failed to set To, addrHeader name is %s, want: empty", addresses[0].Name) + t.Errorf("failed to set To, addrHeader name is %s, want: empty", addresses[1].Name) } }) t.Run("To with invalid address", func(t *testing.T) { @@ -1367,6 +1367,69 @@ func TestMsg_To(t *testing.T) { }) } +func TestMsg_AddTo(t *testing.T) { + t.Run("AddTo with valid addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.To("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set To: %s", err) + } + if err := message.AddTo("tina.tester@example.com"); err != nil { + t.Fatalf("failed to set additional To: %s", err) + } + addresses, ok := message.addrHeader[HeaderTo] + if !ok { + t.Fatalf("failed to set To, addrHeader field is not set") + } + if len(addresses) != 2 { + t.Fatalf("failed to set additional To, addrHeader value count is: %d, want: 1", len(addresses)) + } + if addresses[0].Address != "toni.tester@example.com" { + t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[0].Address, + "toni.tester@example.com") + } + if addresses[0].String() != "" { + t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[0].String(), + "") + } + if addresses[0].Name != "" { + t.Errorf("failed to set To, addrHeader name is %s, want: empty", addresses[0].Name) + } + if addresses[1].Address != "tina.tester@example.com" { + t.Errorf("failed to set additional To, addrHeader value is %s, want: %s", addresses[1].Address, + "tina.tester@example.com") + } + if addresses[1].String() != "" { + t.Errorf("failed to set additional To, addrHeader value is %s, want: %s", addresses[1].String(), + "") + } + if addresses[1].Name != "" { + t.Errorf("failed to set additional To, addrHeader name is %s, want: empty", addresses[1].Name) + } + }) + t.Run("AddTo with invalid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.To("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set To: %s", err) + } + if err := message.AddTo("invalid"); err == nil { + t.Errorf("AddTo should fail with invalid address") + } + addresses, ok := message.addrHeader[HeaderTo] + if !ok { + t.Fatalf("failed to set To, addrHeader field is not set") + } + if len(addresses) != 1 { + t.Fatalf("failed to set To, addrHeader value count is: %d, want: 1", len(addresses)) + } + }) +} + /* // TestNewMsgWithMiddleware tests WithMiddleware @@ -1417,82 +1480,6 @@ func TestMsg_To(t *testing.T) { } } -// TestMsg_SetGenHeader tests Msg.SetGenHeader - - func TestMsg_SetGenHeader(t *testing.T) { - tests := []struct { - name string - header Header - values []string - }{ - {"set subject", HeaderSubject, []string{"This is Subject"}}, - {"set content-language", HeaderContentLang, []string{"en", "de", "fr", "es"}}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m := NewMsg() - m.SetGenHeader(tt.header, tt.values...) - if m.genHeader[tt.header] == nil { - t.Errorf("SetGenHeader() failed. Tried to set header %s, but it is empty", tt.header) - return - } - for _, v := range tt.values { - found := false - for _, hv := range m.genHeader[tt.header] { - if hv == v { - found = true - } - } - if !found { - t.Errorf("SetGenHeader() failed. Value %s not found in header field", v) - } - } - }) - } - } - -// TestMsg_SetGenHeaderPreformatted tests Msg.SetGenHeaderPreformatted - - func TestMsg_SetGenHeaderPreformatted(t *testing.T) { - tests := []struct { - name string - header Header - value string - }{ - {"set subject", HeaderSubject, "This is Subject"}, - {"set content-language", HeaderContentLang, fmt.Sprintf("%s, %s, %s, %s", - "en", "de", "fr", "es")}, - {"set subject with newline", HeaderSubject, "This is Subject\r\n with 2nd line"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m := &Msg{} - m.SetGenHeaderPreformatted(tt.header, tt.value) - m = NewMsg() - m.SetGenHeaderPreformatted(tt.header, tt.value) - if m.preformHeader[tt.header] == "" { - t.Errorf("SetGenHeaderPreformatted() failed. Tried to set header %s, but it is empty", tt.header) - } - if m.preformHeader[tt.header] != tt.value { - t.Errorf("SetGenHeaderPreformatted() failed. Expected: %q, got: %q", tt.value, - m.preformHeader[tt.header]) - } - buf := bytes.Buffer{} - _, err := m.WriteTo(&buf) - if err != nil { - t.Errorf("failed to write message to memory: %s", err) - return - } - if !strings.Contains(buf.String(), fmt.Sprintf("%s: %s%s", tt.header, tt.value, SingleNewLine)) { - t.Errorf("SetGenHeaderPreformatted() failed. Unable to find correctly formated header in " + - "mail message output") - } - }) - } - } - // TestMsg_AddTo tests the Msg.AddTo method func TestMsg_AddTo(t *testing.T) { @@ -1519,124 +1506,6 @@ func TestMsg_To(t *testing.T) { } } -// TestMsg_From tests the Msg.From and Msg.GetSender methods - - func TestMsg_From(t *testing.T) { - a := "toni@example.com" - n := "Toni Tester" - na := fmt.Sprintf(`"%s" <%s>`, n, a) - m := NewMsg() - - _, err := m.GetSender(false) - if err == nil { - t.Errorf("GetSender(false) without a set From address succeeded but was expected to fail") - return - } - - if err := m.From(na); err != nil { - t.Errorf("failed to set FROM addresses: %s", err) - return - } - gs, err := m.GetSender(false) - if err != nil { - t.Errorf("GetSender(false) failed: %s", err) - return - } - if gs != a { - t.Errorf("From() failed. Expected: %s, got: %s", a, gs) - return - } - - gs, err = m.GetSender(true) - if err != nil { - t.Errorf("GetSender(true) failed: %s", err) - return - } - if gs != na { - t.Errorf("From() failed. Expected: %s, got: %s", na, gs) - return - } - } - -// TestMsg_EnvelopeFrom tests the Msg.EnvelopeFrom and Msg.GetSender methods - - func TestMsg_EnvelopeFrom(t *testing.T) { - e := "envelope@example.com" - a := "toni@example.com" - n := "Toni Tester" - na := fmt.Sprintf(`"%s" <%s>`, n, a) - ne := fmt.Sprintf(`<%s>`, e) - m := NewMsg() - - _, err := m.GetSender(false) - if err == nil { - t.Errorf("GetSender(false) without a set envelope From address succeeded but was expected to fail") - return - } - - if err := m.EnvelopeFrom(e); err != nil { - t.Errorf("failed to set envelope FROM addresses: %s", err) - return - } - gs, err := m.GetSender(false) - if err != nil { - t.Errorf("GetSender(false) failed: %s", err) - return - } - if gs != e { - t.Errorf("From() failed. Expected: %s, got: %s", e, gs) - return - } - - if err := m.From(na); err != nil { - t.Errorf("failed to set FROM addresses: %s", err) - return - } - gs, err = m.GetSender(false) - if err != nil { - t.Errorf("GetSender(false) failed: %s", err) - return - } - if gs != e { - t.Errorf("From() failed. Expected: %s, got: %s", e, gs) - return - } - - gs, err = m.GetSender(true) - if err != nil { - t.Errorf("GetSender(true) failed: %s", err) - return - } - if gs != ne { - t.Errorf("From() failed. Expected: %s, got: %s", ne, gs) - return - } - m.Reset() - - if err := m.From(na); err != nil { - t.Errorf("failed to set FROM addresses: %s", err) - return - } - gs, err = m.GetSender(false) - if err != nil { - t.Errorf("GetSender(true) failed: %s", err) - return - } - if gs != a { - t.Errorf("From() failed. Expected: %s, got: %s", a, gs) - return - } - gs, err = m.GetSender(true) - if err != nil { - t.Errorf("GetSender(true) failed: %s", err) - return - } - if gs != na { - t.Errorf("From() failed. Expected: %s, got: %s", na, gs) - return - } - } - // TestMsg_AddToFormat tests the Msg.AddToFormat method func TestMsg_AddToFormat(t *testing.T) { From 1ea7b173c623eedd88d406479b21226c23cbd806 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 17:16:46 +0200 Subject: [PATCH 073/188] 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. --- msg_test.go | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/msg_test.go b/msg_test.go index 38ff685..75c5ca3 100644 --- a/msg_test.go +++ b/msg_test.go @@ -1430,6 +1430,70 @@ func TestMsg_AddTo(t *testing.T) { }) } +func TestMsg_AddToFormat(t *testing.T) { + t.Run("AddToFormat with valid addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.To("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set To: %s", err) + } + if err := message.AddToFormat("Tina Tester", "tina.tester@example.com"); err != nil { + t.Fatalf("failed to set additional To: %s", err) + } + addresses, ok := message.addrHeader[HeaderTo] + if !ok { + t.Fatalf("failed to set To, addrHeader field is not set") + } + if len(addresses) != 2 { + t.Fatalf("failed to set additional To, addrHeader value count is: %d, want: 1", len(addresses)) + } + if addresses[0].Address != "toni.tester@example.com" { + t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[0].Address, + "toni.tester@example.com") + } + if addresses[0].String() != "" { + t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[0].String(), + "") + } + if addresses[0].Name != "" { + t.Errorf("failed to set To, addrHeader name is %s, want: empty", addresses[0].Name) + } + if addresses[1].Address != "tina.tester@example.com" { + t.Errorf("failed to set additional To, addrHeader value is %s, want: %s", addresses[1].Address, + "tina.tester@example.com") + } + if addresses[1].String() != `"Tina Tester" ` { + t.Errorf("failed to set additional To, addrHeader value is %s, want: %s", addresses[1].String(), + "") + } + if addresses[1].Name != "Tina Tester" { + t.Errorf("failed to set additional To, addrHeader name is %s, want: %s", addresses[1].Name, + "Tina Tester") + } + }) + t.Run("AddToFormat with invalid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.To("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set To: %s", err) + } + if err := message.AddToFormat("Invalid", "invalid"); err == nil { + t.Errorf("AddToFormat should fail with invalid address") + } + addresses, ok := message.addrHeader[HeaderTo] + if !ok { + t.Fatalf("failed to set To, addrHeader field is not set") + } + if len(addresses) != 1 { + t.Fatalf("failed to set To, addrHeader value count is: %d, want: 1", len(addresses)) + } + }) +} + /* // TestNewMsgWithMiddleware tests WithMiddleware From 8b6a7927efaabdb05102696b8925864a4d9e4f9e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 17:43:59 +0200 Subject: [PATCH 074/188] 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. --- msg_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/msg_test.go b/msg_test.go index 75c5ca3..565eaff 100644 --- a/msg_test.go +++ b/msg_test.go @@ -1494,6 +1494,10 @@ func TestMsg_AddToFormat(t *testing.T) { }) } +func TestMsg_ToIgnoreInvalid(t *testing.T) { + +} + /* // TestNewMsgWithMiddleware tests WithMiddleware From 03da20fc3949c85508fc4d60a18e7a7b448130fa Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 19:43:05 +0200 Subject: [PATCH 075/188] 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. --- msg_test.go | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/msg_test.go b/msg_test.go index 565eaff..45935b3 100644 --- a/msg_test.go +++ b/msg_test.go @@ -1495,7 +1495,81 @@ func TestMsg_AddToFormat(t *testing.T) { } func TestMsg_ToIgnoreInvalid(t *testing.T) { - + t.Run("ToIgnoreInvalid with valid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.ToIgnoreInvalid("toni.tester@example.com") + addresses, ok := message.addrHeader[HeaderTo] + if !ok { + t.Fatalf("failed to set ToIgnoreInvalid, addrHeader field is not set") + } + if len(addresses) != 1 { + t.Fatalf("failed to set ToIgnoreInvalid, addrHeader value count is: %d, want: 1", len(addresses)) + } + if addresses[0].Address != "toni.tester@example.com" { + t.Errorf("failed to set ToIgnoreInvalid, addrHeader value is %s, want: %s", addresses[0].Address, + "toni.tester@example.com") + } + if addresses[0].String() != "" { + t.Errorf("failed to set ToIgnoreInvalid, addrHeader value is %s, want: %s", addresses[0].String(), + "") + } + if addresses[0].Name != "" { + t.Errorf("failed to set ToIgnoreInvalid, addrHeader name is %s, want: empty", addresses[0].Name) + } + }) + t.Run("ToIgnoreInvalid with invalid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.ToIgnoreInvalid("invalid") + addresses, ok := message.addrHeader[HeaderTo] + if !ok { + t.Fatalf("failed to set ToIgnoreInvalid, addrHeader field is not set") + } + if len(addresses) != 0 { + t.Fatalf("failed to set ToIgnoreInvalid, addrHeader value count is: %d, want: 0", len(addresses)) + } + }) + t.Run("ToIgnoreInvalid with valid and invalid addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.ToIgnoreInvalid("toni.tester@example.com", "invalid", "tina.tester@example.com") + addresses, ok := message.addrHeader[HeaderTo] + if !ok { + t.Fatalf("failed to set ToIgnoreInvalid, addrHeader field is not set") + } + if len(addresses) != 2 { + t.Fatalf("failed to set ToIgnoreInvalid, addrHeader value count is: %d, want: 2", len(addresses)) + } + if addresses[0].Address != "toni.tester@example.com" { + t.Errorf("failed to set ToIgnoreInvalid, addrHeader value is %s, want: %s", addresses[0].Address, + "toni.tester@example.com") + } + if addresses[0].String() != "" { + t.Errorf("failed to set ToIgnoreInvalid, addrHeader value is %s, want: %s", addresses[0].String(), + "") + } + if addresses[0].Name != "" { + t.Errorf("failed to set ToIgnoreInvalid, addrHeader name is %s, want: empty", addresses[0].Name) + } + if addresses[1].Address != "tina.tester@example.com" { + t.Errorf("failed to set ToIgnoreInvalid, addrHeader value is %s, want: %s", addresses[1].Address, + "tina.tester@example.com") + } + if addresses[1].String() != "" { + t.Errorf("failed to set ToIgnoreInvalid, addrHeader value is %s, want: %s", addresses[1].String(), + "") + } + if addresses[1].Name != "" { + t.Errorf("failed to set ToIgnoreInvalid, addrHeader name is %s, want: empty", addresses[1].Name) + } + }) } /* From c99b6c3f142863fc4c0d9c9dcd51999ebc3e4a7a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 19:54:41 +0200 Subject: [PATCH 076/188] 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. --- msg.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/msg.go b/msg.go index c3085c3..bd771b0 100644 --- a/msg.go +++ b/msg.go @@ -753,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. From e08d36d0b8f750bb1ef4fdd1962586262b1468b0 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 25 Oct 2024 20:21:17 +0200 Subject: [PATCH 077/188] 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. --- msg_test.go | 196 ++++++++++++++++++++-------------------------------- 1 file changed, 75 insertions(+), 121 deletions(-) diff --git a/msg_test.go b/msg_test.go index 45935b3..3caf2c3 100644 --- a/msg_test.go +++ b/msg_test.go @@ -5,7 +5,6 @@ package mail import ( - "embed" "fmt" "reflect" "strings" @@ -121,8 +120,10 @@ var ( } ) +/* //go:embed README.md var efs embed.FS +*/ func TestNewMsg(t *testing.T) { t.Run("create new message", func(t *testing.T) { @@ -1379,35 +1380,10 @@ func TestMsg_AddTo(t *testing.T) { if err := message.AddTo("tina.tester@example.com"); err != nil { t.Fatalf("failed to set additional To: %s", err) } - addresses, ok := message.addrHeader[HeaderTo] - if !ok { - t.Fatalf("failed to set To, addrHeader field is not set") - } - if len(addresses) != 2 { - t.Fatalf("failed to set additional To, addrHeader value count is: %d, want: 1", len(addresses)) - } - if addresses[0].Address != "toni.tester@example.com" { - t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[0].Address, - "toni.tester@example.com") - } - if addresses[0].String() != "" { - t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[0].String(), - "") - } - if addresses[0].Name != "" { - t.Errorf("failed to set To, addrHeader name is %s, want: empty", addresses[0].Name) - } - if addresses[1].Address != "tina.tester@example.com" { - t.Errorf("failed to set additional To, addrHeader value is %s, want: %s", addresses[1].Address, - "tina.tester@example.com") - } - if addresses[1].String() != "" { - t.Errorf("failed to set additional To, addrHeader value is %s, want: %s", addresses[1].String(), - "") - } - if addresses[1].Name != "" { - t.Errorf("failed to set additional To, addrHeader name is %s, want: empty", addresses[1].Name) - } + checkAddrHeader(t, message, HeaderTo, "AddTo", 0, 2, + "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderTo, "AddTo", 1, 2, + "tina.tester@example.com", "") }) t.Run("AddTo with invalid address", func(t *testing.T) { message := NewMsg() @@ -1420,13 +1396,8 @@ func TestMsg_AddTo(t *testing.T) { if err := message.AddTo("invalid"); err == nil { t.Errorf("AddTo should fail with invalid address") } - addresses, ok := message.addrHeader[HeaderTo] - if !ok { - t.Fatalf("failed to set To, addrHeader field is not set") - } - if len(addresses) != 1 { - t.Fatalf("failed to set To, addrHeader value count is: %d, want: 1", len(addresses)) - } + checkAddrHeader(t, message, HeaderTo, "AddTo", 0, 1, + "toni.tester@example.com", "") }) } @@ -1442,36 +1413,10 @@ func TestMsg_AddToFormat(t *testing.T) { if err := message.AddToFormat("Tina Tester", "tina.tester@example.com"); err != nil { t.Fatalf("failed to set additional To: %s", err) } - addresses, ok := message.addrHeader[HeaderTo] - if !ok { - t.Fatalf("failed to set To, addrHeader field is not set") - } - if len(addresses) != 2 { - t.Fatalf("failed to set additional To, addrHeader value count is: %d, want: 1", len(addresses)) - } - if addresses[0].Address != "toni.tester@example.com" { - t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[0].Address, - "toni.tester@example.com") - } - if addresses[0].String() != "" { - t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[0].String(), - "") - } - if addresses[0].Name != "" { - t.Errorf("failed to set To, addrHeader name is %s, want: empty", addresses[0].Name) - } - if addresses[1].Address != "tina.tester@example.com" { - t.Errorf("failed to set additional To, addrHeader value is %s, want: %s", addresses[1].Address, - "tina.tester@example.com") - } - if addresses[1].String() != `"Tina Tester" ` { - t.Errorf("failed to set additional To, addrHeader value is %s, want: %s", addresses[1].String(), - "") - } - if addresses[1].Name != "Tina Tester" { - t.Errorf("failed to set additional To, addrHeader name is %s, want: %s", addresses[1].Name, - "Tina Tester") - } + checkAddrHeader(t, message, HeaderTo, "AddToFormat", 0, 2, + "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderTo, "AddToFormat", 1, 2, + "tina.tester@example.com", "Tina Tester") }) t.Run("AddToFormat with invalid address", func(t *testing.T) { message := NewMsg() @@ -1484,13 +1429,8 @@ func TestMsg_AddToFormat(t *testing.T) { if err := message.AddToFormat("Invalid", "invalid"); err == nil { t.Errorf("AddToFormat should fail with invalid address") } - addresses, ok := message.addrHeader[HeaderTo] - if !ok { - t.Fatalf("failed to set To, addrHeader field is not set") - } - if len(addresses) != 1 { - t.Fatalf("failed to set To, addrHeader value count is: %d, want: 1", len(addresses)) - } + checkAddrHeader(t, message, HeaderTo, "AddToFormat", 0, 1, + "toni.tester@example.com", "") }) } @@ -1501,24 +1441,8 @@ func TestMsg_ToIgnoreInvalid(t *testing.T) { t.Fatal("message is nil") } message.ToIgnoreInvalid("toni.tester@example.com") - addresses, ok := message.addrHeader[HeaderTo] - if !ok { - t.Fatalf("failed to set ToIgnoreInvalid, addrHeader field is not set") - } - if len(addresses) != 1 { - t.Fatalf("failed to set ToIgnoreInvalid, addrHeader value count is: %d, want: 1", len(addresses)) - } - if addresses[0].Address != "toni.tester@example.com" { - t.Errorf("failed to set ToIgnoreInvalid, addrHeader value is %s, want: %s", addresses[0].Address, - "toni.tester@example.com") - } - if addresses[0].String() != "" { - t.Errorf("failed to set ToIgnoreInvalid, addrHeader value is %s, want: %s", addresses[0].String(), - "") - } - if addresses[0].Name != "" { - t.Errorf("failed to set ToIgnoreInvalid, addrHeader name is %s, want: empty", addresses[0].Name) - } + checkAddrHeader(t, message, HeaderTo, "ToIgnoreInvalid", 0, 1, + "toni.tester@example.com", "") }) t.Run("ToIgnoreInvalid with invalid address", func(t *testing.T) { message := NewMsg() @@ -1540,38 +1464,68 @@ func TestMsg_ToIgnoreInvalid(t *testing.T) { t.Fatal("message is nil") } message.ToIgnoreInvalid("toni.tester@example.com", "invalid", "tina.tester@example.com") - addresses, ok := message.addrHeader[HeaderTo] - if !ok { - t.Fatalf("failed to set ToIgnoreInvalid, addrHeader field is not set") - } - if len(addresses) != 2 { - t.Fatalf("failed to set ToIgnoreInvalid, addrHeader value count is: %d, want: 2", len(addresses)) - } - if addresses[0].Address != "toni.tester@example.com" { - t.Errorf("failed to set ToIgnoreInvalid, addrHeader value is %s, want: %s", addresses[0].Address, - "toni.tester@example.com") - } - if addresses[0].String() != "" { - t.Errorf("failed to set ToIgnoreInvalid, addrHeader value is %s, want: %s", addresses[0].String(), - "") - } - if addresses[0].Name != "" { - t.Errorf("failed to set ToIgnoreInvalid, addrHeader name is %s, want: empty", addresses[0].Name) - } - if addresses[1].Address != "tina.tester@example.com" { - t.Errorf("failed to set ToIgnoreInvalid, addrHeader value is %s, want: %s", addresses[1].Address, - "tina.tester@example.com") - } - if addresses[1].String() != "" { - t.Errorf("failed to set ToIgnoreInvalid, addrHeader value is %s, want: %s", addresses[1].String(), - "") - } - if addresses[1].Name != "" { - t.Errorf("failed to set ToIgnoreInvalid, addrHeader name is %s, want: empty", addresses[1].Name) - } + checkAddrHeader(t, message, HeaderTo, "ToIgnoreInvalid", 0, 2, + "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderTo, "ToIgnoreInvalid", 1, 2, + "tina.tester@example.com", "") }) } +func TestMsg_ToFromString(t *testing.T) { + t.Run("ToFromString with valid addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.ToFromString(`toni.tester@example.com,`); err != nil { + t.Fatalf("failed to set ToFromString: %s", err) + } + checkAddrHeader(t, message, HeaderTo, "ToFromString", 0, 2, + "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderTo, "ToFromString", 1, 2, + "tina.tester@example.com", "") + }) + t.Run("ToFromString with valid addresses and empty fields", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.ToFromString(`toni.tester@example.com ,,`); err != nil { + t.Fatalf("failed to set ToFromString: %s", err) + } + checkAddrHeader(t, message, HeaderTo, "ToFromString", 0, 2, + "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderTo, "ToFromString", 1, 2, + "tina.tester@example.com", "") + }) +} + +// checkAddrHeader validates a single email address in the AddrHeader of a message. +func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, + wantMail, wantName string) { + t.Helper() + addresses, ok := message.addrHeader[header] + if !ok { + t.Fatalf("failed to set %s, addrHeader field is not set", fn) + } + if len(addresses) != wantFields { + t.Fatalf("failed to set %s, addrHeader value count is: %d, want: %d", fn, len(addresses), field) + } + if addresses[field].Address != wantMail { + t.Errorf("failed to set %s, addrHeader value is %s, want: %s", fn, addresses[field].Address, wantMail) + } + wantString := fmt.Sprintf(`<%s>`, wantMail) + if wantName != "" { + wantString = fmt.Sprintf(`%q <%s>`, wantName, wantMail) + } + if addresses[field].String() != wantString { + t.Errorf("failed to set %s, addrHeader value is %s, want: %s", fn, addresses[field].String(), wantString) + } + if addresses[field].Name != wantName { + t.Errorf("failed to set %s, addrHeader name is %s, want: %s", fn, addresses[field].Name, wantName) + } +} + /* // TestNewMsgWithMiddleware tests WithMiddleware From 3a3eaed348c5fc2f42c5d424a8192704d5226fdf Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 02:07:44 +0200 Subject: [PATCH 078/188] 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. --- msg_test.go | 446 +++++----------------------------------------------- 1 file changed, 42 insertions(+), 404 deletions(-) diff --git a/msg_test.go b/msg_test.go index 3caf2c3..57abebb 100644 --- a/msg_test.go +++ b/msg_test.go @@ -627,26 +627,7 @@ func TestMsg_SetAddrHeader(t *testing.T) { if err := message.SetAddrHeader(tt.header, "toni.tester@example.com"); err != nil { t.Fatalf("failed to set address header, err: %s", err) } - addresses, ok := message.addrHeader[tt.header] - if !ok { - t.Fatalf("failed to set address header, addrHeader field for %s is not set", tt.header) - } - if len(addresses) != 1 { - t.Fatalf("failed to set address header, addrHeader value count for %s is %d, want: 1", - tt.header, len(addresses)) - } - if addresses[0].Address != "toni.tester@example.com" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, - addresses[0].Address, "toni.tester@example.com") - } - if addresses[0].String() != "" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, - addresses[0].String(), "") - } - if addresses[0].Name != "" { - t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ - "got: %s", tt.header, addresses[0].Name) - } + checkAddrHeader(t, message, tt.header, "SetAddrHeader", 0, 1, "toni.tester@example.com", "") }) } }) @@ -660,26 +641,7 @@ func TestMsg_SetAddrHeader(t *testing.T) { if err := message.SetAddrHeader(tt.header, ""); err != nil { t.Fatalf("failed to set address header, err: %s", err) } - addresses, ok := message.addrHeader[tt.header] - if !ok { - t.Fatalf("failed to set address header, addrHeader field for %s is not set", tt.header) - } - if len(addresses) != 1 { - t.Fatalf("failed to set address header, addrHeader value count for %s is %d, want: 1", - tt.header, len(addresses)) - } - if addresses[0].Address != "toni.tester@example.com" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, - addresses[0].Address, "toni.tester@example.com") - } - if addresses[0].String() != "" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, - addresses[0].String(), "") - } - if addresses[0].Name != "" { - t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ - "got: %s", tt.header, addresses[0].Name) - } + checkAddrHeader(t, message, tt.header, "SetAddrHeader", 0, 1, "toni.tester@example.com", "") }) } }) @@ -694,26 +656,8 @@ func TestMsg_SetAddrHeader(t *testing.T) { "toni.tester@example.com")); err != nil { t.Fatalf("failed to set address header, err: %s", err) } - addresses, ok := message.addrHeader[tt.header] - if !ok { - t.Fatalf("failed to set address header, addrHeader field for %s is not set", tt.header) - } - if len(addresses) != 1 { - t.Fatalf("failed to set address header, addrHeader value count for %s is %d, want: 1", - tt.header, len(addresses)) - } - if addresses[0].Address != "toni.tester@example.com" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, - addresses[0].Address, "toni.tester@example.com") - } - if addresses[0].String() != `"Toni Tester" ` { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, - addresses[0].String(), `"Toni Tester" `) - } - if addresses[0].Name != "Toni Tester" { - t.Errorf("failed to set address header, addrHeader name for %s expected to be %s, "+ - "got: %s", tt.header, "Toni Tester", addresses[0].Name) - } + checkAddrHeader(t, message, tt.header, "SetAddrHeader", 0, 1, + "toni.tester@example.com", "Toni Tester") }) } }) @@ -733,38 +677,8 @@ func TestMsg_SetAddrHeader(t *testing.T) { "tina.tester@example.com"); err != nil { t.Fatalf("failed to set address header, err: %s", err) } - addresses, ok := message.addrHeader[tt.header] - if !ok { - t.Fatalf("failed to set address header, addrHeader field for %s is not set", tt.header) - } - if len(addresses) != 2 { - t.Fatalf("failed to set address header, addrHeader value count for %s is %d, want: 2", - tt.header, len(addresses)) - } - if addresses[0].Address != "toni.tester@example.com" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, - addresses[0].Address, "toni.tester@example.com") - } - if addresses[0].String() != "" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, - addresses[0].String(), "") - } - if addresses[0].Name != "" { - t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ - "got: %s", tt.header, addresses[0].Name) - } - if addresses[1].Address != "tina.tester@example.com" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, - addresses[1].Address, "tina.tester@example.com") - } - if addresses[1].String() != "" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, - addresses[1].String(), "") - } - if addresses[1].Name != "" { - t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ - "got: %s", tt.header, addresses[1].Name) - } + checkAddrHeader(t, message, tt.header, "SetAddrHeader", 0, 2, "toni.tester@example.com", "") + checkAddrHeader(t, message, tt.header, "SetAddrHeader", 1, 2, "tina.tester@example.com", "") }) } }) @@ -777,26 +691,7 @@ func TestMsg_SetAddrHeader(t *testing.T) { "tina.tester@example.com"); err != nil { t.Fatalf("failed to set address header, err: %s", err) } - addresses, ok := message.addrHeader[HeaderFrom] - if !ok { - t.Fatalf("failed to set address header, addrHeader field for %s is not set", HeaderFrom) - } - if len(addresses) != 1 { - t.Fatalf("failed to set address header, addrHeader value count for From is: %d, want: 1", - len(addresses)) - } - if addresses[0].Address != "toni.tester@example.com" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", HeaderFrom, - addresses[0].Address, "toni.tester@example.com") - } - if addresses[0].String() != "" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", HeaderFrom, - addresses[0].String(), "") - } - if addresses[0].Name != "" { - t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ - "got: %s", HeaderFrom, addresses[0].Name) - } + checkAddrHeader(t, message, HeaderFrom, "SetAddrHeader", 0, 1, "toni.tester@example.com", "") }) t.Run("SetAddrHeader with addrHeader map is nil", func(t *testing.T) { message := NewMsg() @@ -808,26 +703,7 @@ func TestMsg_SetAddrHeader(t *testing.T) { "tina.tester@example.com"); err != nil { t.Fatalf("failed to set address header, err: %s", err) } - addresses, ok := message.addrHeader[HeaderFrom] - if !ok { - t.Fatalf("failed to set address header, addrHeader field for %s is not set", HeaderFrom) - } - if len(addresses) != 1 { - t.Fatalf("failed to set address header, addrHeader value count for From is: %d, want: 1", - len(addresses)) - } - if addresses[0].Address != "toni.tester@example.com" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", HeaderFrom, - addresses[0].Address, "toni.tester@example.com") - } - if addresses[0].String() != "" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", HeaderFrom, - addresses[0].String(), "") - } - if addresses[0].Name != "" { - t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ - "got: %s", HeaderFrom, addresses[0].Name) - } + checkAddrHeader(t, message, HeaderFrom, "SetAddrHeader", 0, 1, "toni.tester@example.com", "") }) t.Run("SetAddrHeader with invalid address", func(t *testing.T) { for _, tt := range addrHeaderTests { @@ -853,26 +729,8 @@ func TestMsg_SetAddrHeaderIgnoreInvalid(t *testing.T) { t.Fatal("message is nil") } message.SetAddrHeaderIgnoreInvalid(tt.header, "toni.tester@example.com") - addresses, ok := message.addrHeader[tt.header] - if !ok { - t.Fatalf("failed to set address header, addrHeader field for %s is not set", tt.header) - } - if len(addresses) != 1 { - t.Fatalf("failed to set address header, addrHeader value count for %s is %d, want: 1", - tt.header, len(addresses)) - } - if addresses[0].Address != "toni.tester@example.com" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, - addresses[0].Address, "toni.tester@example.com") - } - if addresses[0].String() != "" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, - addresses[0].String(), "") - } - if addresses[0].Name != "" { - t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ - "got: %s", tt.header, addresses[0].Name) - } + checkAddrHeader(t, message, tt.header, "SetAddrHeaderIgnoreInvalid", 0, 1, + "toni.tester@example.com", "") }) } }) @@ -884,26 +742,8 @@ func TestMsg_SetAddrHeaderIgnoreInvalid(t *testing.T) { t.Fatal("message is nil") } message.SetAddrHeaderIgnoreInvalid(tt.header, "") - addresses, ok := message.addrHeader[tt.header] - if !ok { - t.Fatalf("failed to set address header, addrHeader field for %s is not set", tt.header) - } - if len(addresses) != 1 { - t.Fatalf("failed to set address header, addrHeader value count for %s is %d, want: 1", - tt.header, len(addresses)) - } - if addresses[0].Address != "toni.tester@example.com" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, - addresses[0].Address, "toni.tester@example.com") - } - if addresses[0].String() != "" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, - addresses[0].String(), "") - } - if addresses[0].Name != "" { - t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ - "got: %s", tt.header, addresses[0].Name) - } + checkAddrHeader(t, message, tt.header, "SetAddrHeaderIgnoreInvalid", 0, 1, + "toni.tester@example.com", "") }) } }) @@ -921,38 +761,10 @@ func TestMsg_SetAddrHeaderIgnoreInvalid(t *testing.T) { } message.SetAddrHeaderIgnoreInvalid(tt.header, "toni.tester@example.com", "tina.tester@example.com") - addresses, ok := message.addrHeader[tt.header] - if !ok { - t.Fatalf("failed to set address header, addrHeader field for %s is not set", tt.header) - } - if len(addresses) != 2 { - t.Fatalf("failed to set address header, addrHeader value count for %s is %d, want: 2", - tt.header, len(addresses)) - } - if addresses[0].Address != "toni.tester@example.com" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, - addresses[0].Address, "toni.tester@example.com") - } - if addresses[0].String() != "" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, - addresses[0].String(), "") - } - if addresses[0].Name != "" { - t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ - "got: %s", tt.header, addresses[0].Name) - } - if addresses[1].Address != "tina.tester@example.com" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, - addresses[1].Address, "tina.tester@example.com") - } - if addresses[1].String() != "" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, - addresses[1].String(), "") - } - if addresses[1].Name != "" { - t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ - "got: %s", tt.header, addresses[1].Name) - } + checkAddrHeader(t, message, tt.header, "SetAddrHeaderIgnoreInvalid", 0, 2, + "toni.tester@example.com", "") + checkAddrHeader(t, message, tt.header, "SetAddrHeaderIgnoreInvalid", 1, 2, + "tina.tester@example.com", "") }) } }) @@ -969,39 +781,11 @@ func TestMsg_SetAddrHeaderIgnoreInvalid(t *testing.T) { t.Fatal("message is nil") } message.SetAddrHeaderIgnoreInvalid(tt.header, "toni.tester@example.com", - "invalid", "valid@address.tld") - addresses, ok := message.addrHeader[tt.header] - if !ok { - t.Fatalf("failed to set address header, addrHeader field for %s is not set", tt.header) - } - if len(addresses) != 2 { - t.Fatalf("failed to set address header, addrHeader value count for %s is %d, want: 2", - tt.header, len(addresses)) - } - if addresses[0].Address != "toni.tester@example.com" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, - addresses[0].Address, "toni.tester@example.com") - } - if addresses[0].String() != "" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, - addresses[0].String(), "") - } - if addresses[0].Name != "" { - t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ - "got: %s", tt.header, addresses[0].Name) - } - if addresses[1].Address != "valid@address.tld" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, - addresses[1].Address, "valid@address.tld") - } - if addresses[1].String() != "" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", tt.header, - addresses[1].String(), "") - } - if addresses[1].Name != "" { - t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ - "got: %s", tt.header, addresses[1].Name) - } + "invalid", "tina.tester@example.com") + checkAddrHeader(t, message, tt.header, "SetAddrHeaderIgnoreInvalid", 0, 2, + "toni.tester@example.com", "") + checkAddrHeader(t, message, tt.header, "SetAddrHeaderIgnoreInvalid", 1, 2, + "tina.tester@example.com", "") }) } }) @@ -1012,26 +796,7 @@ func TestMsg_SetAddrHeaderIgnoreInvalid(t *testing.T) { } message.addrHeader = nil message.SetAddrHeaderIgnoreInvalid(HeaderFrom, "toni.tester@example.com", "tina.tester@example.com") - addresses, ok := message.addrHeader[HeaderFrom] - if !ok { - t.Fatalf("failed to set address header, addrHeader field for %s is not set", HeaderFrom) - } - if len(addresses) != 1 { - t.Fatalf("failed to set address header, addrHeader value count for From is: %d, want: 1", - len(addresses)) - } - if addresses[0].Address != "toni.tester@example.com" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", HeaderFrom, - addresses[0].Address, "toni.tester@example.com") - } - if addresses[0].String() != "" { - t.Errorf("failed to set address header, addrHeader value for %s is %s, want: %s", HeaderFrom, - addresses[0].String(), "") - } - if addresses[0].Name != "" { - t.Errorf("failed to set address header, addrHeader name for %s expected to be emtpy, "+ - "got: %s", HeaderFrom, addresses[0].Name) - } + checkAddrHeader(t, message, HeaderFrom, "SetAddrHeaderIgnoreInvalid", 0, 1, "toni.tester@example.com", "") }) t.Run("SetAddrHeaderIgnoreInvalid with invalid addresses only", func(t *testing.T) { for _, tt := range addrHeaderTests { @@ -1063,24 +828,7 @@ func TestMsg_EnvelopeFrom(t *testing.T) { if err := message.EnvelopeFrom("toni.tester@example.com"); err != nil { t.Fatalf("failed to set envelope from: %s", err) } - addresses, ok := message.addrHeader[HeaderEnvelopeFrom] - if !ok { - t.Fatalf("failed to set envelope from, addrHeader field is not set") - } - if len(addresses) != 1 { - t.Fatalf("failed to set envelope from, addrHeader value count is: %d, want: 1", len(addresses)) - } - if addresses[0].Address != "toni.tester@example.com" { - t.Errorf("failed to set envelope from, addrHeader value is %s, want: %s", addresses[0].Address, - "toni.tester@example.com") - } - if addresses[0].String() != "" { - t.Errorf("failed to set envelope from, addrHeader value is %s, want: %s", addresses[0].String(), - "") - } - if addresses[0].Name != "" { - t.Errorf("failed to set envelope from, addrHeader name is %s, want: empty", addresses[0].Name) - } + checkAddrHeader(t, message, HeaderEnvelopeFrom, "EnvelopeFrom", 0, 1, "toni.tester@example.com", "") }) t.Run("EnvelopeFrom with invalid address", func(t *testing.T) { message := NewMsg() @@ -1111,25 +859,7 @@ func TestMsg_EnvelopeFromFormat(t *testing.T) { if err := message.EnvelopeFromFormat("Toni Tester", "toni.tester@example.com"); err != nil { t.Fatalf("failed to set envelope From: %s", err) } - addresses, ok := message.addrHeader[HeaderEnvelopeFrom] - if !ok { - t.Fatalf("failed to set envelope From, addrHeader field is not set") - } - if len(addresses) != 1 { - t.Fatalf("failed to set envelope From, addrHeader value count is: %d, want: 1", len(addresses)) - } - if addresses[0].Address != "toni.tester@example.com" { - t.Errorf("failed to set envelope From, addrHeader value is %s, want: %s", addresses[0].Address, - "toni.tester@example.com") - } - if addresses[0].String() != `"Toni Tester" ` { - t.Errorf("failed to set envelope From, addrHeader value is %s, want: %s", addresses[0].String(), - "") - } - if addresses[0].Name != "Toni Tester" { - t.Errorf("failed to set envelope From, addrHeader name is %s, want: %s", addresses[0].Name, - "Toni Tester") - } + checkAddrHeader(t, message, HeaderEnvelopeFrom, "FromFormat", 0, 1, "toni.tester@example.com", "Toni Tester") }) t.Run("EnvelopeFromFormat with invalid address", func(t *testing.T) { message := NewMsg() @@ -1160,24 +890,7 @@ func TestMsg_From(t *testing.T) { if err := message.From("toni.tester@example.com"); err != nil { t.Fatalf("failed to set From: %s", err) } - addresses, ok := message.addrHeader[HeaderFrom] - if !ok { - t.Fatalf("failed to set From, addrHeader field is not set") - } - if len(addresses) != 1 { - t.Fatalf("failed to set From, addrHeader value count is: %d, want: 1", len(addresses)) - } - if addresses[0].Address != "toni.tester@example.com" { - t.Errorf("failed to set From, addrHeader value is %s, want: %s", addresses[0].Address, - "toni.tester@example.com") - } - if addresses[0].String() != "" { - t.Errorf("failed to set From, addrHeader value is %s, want: %s", addresses[0].String(), - "") - } - if addresses[0].Name != "" { - t.Errorf("failed to set From, addrHeader name is %s, want: empty", addresses[0].Name) - } + checkAddrHeader(t, message, HeaderFrom, "From", 0, 1, "toni.tester@example.com", "") }) t.Run("From with invalid address", func(t *testing.T) { message := NewMsg() @@ -1225,25 +938,7 @@ func TestMsg_FromFormat(t *testing.T) { if err := message.FromFormat("Toni Tester", "toni.tester@example.com"); err != nil { t.Fatalf("failed to set From: %s", err) } - addresses, ok := message.addrHeader[HeaderFrom] - if !ok { - t.Fatalf("failed to set From, addrHeader field is not set") - } - if len(addresses) != 1 { - t.Fatalf("failed to set From, addrHeader value count is: %d, want: 1", len(addresses)) - } - if addresses[0].Address != "toni.tester@example.com" { - t.Errorf("failed to set From, addrHeader value is %s, want: %s", addresses[0].Address, - "toni.tester@example.com") - } - if addresses[0].String() != `"Toni Tester" ` { - t.Errorf("failed to set From, addrHeader value is %s, want: %s", addresses[0].String(), - "") - } - if addresses[0].Name != "Toni Tester" { - t.Errorf("failed to set From, addrHeader name is %s, want: %s", addresses[0].Name, - "Toni Tester") - } + checkAddrHeader(t, message, HeaderFrom, "FromFormat", 0, 1, "toni.tester@example.com", "Toni Tester") }) t.Run("FromFormat with invalid address", func(t *testing.T) { message := NewMsg() @@ -1274,24 +969,7 @@ func TestMsg_To(t *testing.T) { if err := message.To("toni.tester@example.com"); err != nil { t.Fatalf("failed to set To: %s", err) } - addresses, ok := message.addrHeader[HeaderTo] - if !ok { - t.Fatalf("failed to set To, addrHeader field is not set") - } - if len(addresses) != 1 { - t.Fatalf("failed to set To, addrHeader value count is: %d, want: 1", len(addresses)) - } - if addresses[0].Address != "toni.tester@example.com" { - t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[0].Address, - "toni.tester@example.com") - } - if addresses[0].String() != "" { - t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[0].String(), - "") - } - if addresses[0].Name != "" { - t.Errorf("failed to set To, addrHeader name is %s, want: empty", addresses[0].Name) - } + checkAddrHeader(t, message, HeaderTo, "To", 0, 1, "toni.tester@example.com", "") }) t.Run("To with multiple valid addresses", func(t *testing.T) { message := NewMsg() @@ -1301,35 +979,8 @@ func TestMsg_To(t *testing.T) { if err := message.To("toni.tester@example.com", "tina.tester@example.com"); err != nil { t.Fatalf("failed to set To: %s", err) } - addresses, ok := message.addrHeader[HeaderTo] - if !ok { - t.Fatalf("failed to set To, addrHeader field is not set") - } - if len(addresses) != 2 { - t.Fatalf("failed to set To, addrHeader value count is: %d, want: 2", len(addresses)) - } - if addresses[0].Address != "toni.tester@example.com" { - t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[0].Address, - "toni.tester@example.com") - } - if addresses[0].String() != "" { - t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[0].String(), - "") - } - if addresses[0].Name != "" { - t.Errorf("failed to set To, addrHeader name is %s, want: empty", addresses[0].Name) - } - if addresses[1].Address != "tina.tester@example.com" { - t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[1].Address, - "tina.tester@example.com") - } - if addresses[1].String() != "" { - t.Errorf("failed to set To, addrHeader value is %s, want: %s", addresses[1].String(), - "") - } - if addresses[1].Name != "" { - t.Errorf("failed to set To, addrHeader name is %s, want: empty", addresses[1].Name) - } + checkAddrHeader(t, message, HeaderTo, "To", 0, 2, "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderTo, "To", 1, 2, "tina.tester@example.com", "") }) t.Run("To with invalid address", func(t *testing.T) { message := NewMsg() @@ -1380,10 +1031,8 @@ func TestMsg_AddTo(t *testing.T) { if err := message.AddTo("tina.tester@example.com"); err != nil { t.Fatalf("failed to set additional To: %s", err) } - checkAddrHeader(t, message, HeaderTo, "AddTo", 0, 2, - "toni.tester@example.com", "") - checkAddrHeader(t, message, HeaderTo, "AddTo", 1, 2, - "tina.tester@example.com", "") + checkAddrHeader(t, message, HeaderTo, "AddTo", 0, 2, "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderTo, "AddTo", 1, 2, "tina.tester@example.com", "") }) t.Run("AddTo with invalid address", func(t *testing.T) { message := NewMsg() @@ -1396,8 +1045,7 @@ func TestMsg_AddTo(t *testing.T) { if err := message.AddTo("invalid"); err == nil { t.Errorf("AddTo should fail with invalid address") } - checkAddrHeader(t, message, HeaderTo, "AddTo", 0, 1, - "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderTo, "AddTo", 0, 1, "toni.tester@example.com", "") }) } @@ -1413,10 +1061,8 @@ func TestMsg_AddToFormat(t *testing.T) { if err := message.AddToFormat("Tina Tester", "tina.tester@example.com"); err != nil { t.Fatalf("failed to set additional To: %s", err) } - checkAddrHeader(t, message, HeaderTo, "AddToFormat", 0, 2, - "toni.tester@example.com", "") - checkAddrHeader(t, message, HeaderTo, "AddToFormat", 1, 2, - "tina.tester@example.com", "Tina Tester") + checkAddrHeader(t, message, HeaderTo, "AddToFormat", 0, 2, "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderTo, "AddToFormat", 1, 2, "tina.tester@example.com", "Tina Tester") }) t.Run("AddToFormat with invalid address", func(t *testing.T) { message := NewMsg() @@ -1429,8 +1075,7 @@ func TestMsg_AddToFormat(t *testing.T) { if err := message.AddToFormat("Invalid", "invalid"); err == nil { t.Errorf("AddToFormat should fail with invalid address") } - checkAddrHeader(t, message, HeaderTo, "AddToFormat", 0, 1, - "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderTo, "AddToFormat", 0, 1, "toni.tester@example.com", "") }) } @@ -1441,8 +1086,7 @@ func TestMsg_ToIgnoreInvalid(t *testing.T) { t.Fatal("message is nil") } message.ToIgnoreInvalid("toni.tester@example.com") - checkAddrHeader(t, message, HeaderTo, "ToIgnoreInvalid", 0, 1, - "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderTo, "ToIgnoreInvalid", 0, 1, "toni.tester@example.com", "") }) t.Run("ToIgnoreInvalid with invalid address", func(t *testing.T) { message := NewMsg() @@ -1464,10 +1108,8 @@ func TestMsg_ToIgnoreInvalid(t *testing.T) { t.Fatal("message is nil") } message.ToIgnoreInvalid("toni.tester@example.com", "invalid", "tina.tester@example.com") - checkAddrHeader(t, message, HeaderTo, "ToIgnoreInvalid", 0, 2, - "toni.tester@example.com", "") - checkAddrHeader(t, message, HeaderTo, "ToIgnoreInvalid", 1, 2, - "tina.tester@example.com", "") + checkAddrHeader(t, message, HeaderTo, "ToIgnoreInvalid", 0, 2, "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderTo, "ToIgnoreInvalid", 1, 2, "tina.tester@example.com", "") }) } @@ -1480,10 +1122,8 @@ func TestMsg_ToFromString(t *testing.T) { if err := message.ToFromString(`toni.tester@example.com,`); err != nil { t.Fatalf("failed to set ToFromString: %s", err) } - checkAddrHeader(t, message, HeaderTo, "ToFromString", 0, 2, - "toni.tester@example.com", "") - checkAddrHeader(t, message, HeaderTo, "ToFromString", 1, 2, - "tina.tester@example.com", "") + checkAddrHeader(t, message, HeaderTo, "ToFromString", 0, 2, "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderTo, "ToFromString", 1, 2, "tina.tester@example.com", "") }) t.Run("ToFromString with valid addresses and empty fields", func(t *testing.T) { message := NewMsg() @@ -1493,10 +1133,8 @@ func TestMsg_ToFromString(t *testing.T) { if err := message.ToFromString(`toni.tester@example.com ,,`); err != nil { t.Fatalf("failed to set ToFromString: %s", err) } - checkAddrHeader(t, message, HeaderTo, "ToFromString", 0, 2, - "toni.tester@example.com", "") - checkAddrHeader(t, message, HeaderTo, "ToFromString", 1, 2, - "tina.tester@example.com", "") + checkAddrHeader(t, message, HeaderTo, "ToFromString", 0, 2, "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderTo, "ToFromString", 1, 2, "tina.tester@example.com", "") }) } From 9e6c1f041701434981801ea8de1ca0694b7875e0 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 15:33:05 +0200 Subject: [PATCH 079/188] 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. --- .github/workflows/ci.yml | 197 ++++++++++++++++++++++++ .github/workflows/codecov.yml | 67 -------- .github/workflows/dependency-review.yml | 31 ---- .github/workflows/golangci-lint.yml | 54 ------- .github/workflows/govulncheck.yml | 21 --- .github/workflows/offline-tests.yml | 45 ------ .github/workflows/reuse.yml | 23 --- .github/workflows/sonarqube.yml | 56 ------- 8 files changed, 197 insertions(+), 297 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/codecov.yml delete mode 100644 .github/workflows/dependency-review.yml delete mode 100644 .github/workflows/golangci-lint.yml delete mode 100644 .github/workflows/govulncheck.yml delete mode 100644 .github/workflows/offline-tests.yml delete mode 100644 .github/workflows/reuse.yml delete mode 100644 .github/workflows/sonarqube.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..80b612d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,197 @@ +name: CI + +permissions: + contents: read + +on: + push: + branches: + - main + paths: + - '**.go' + - 'go.*' + - '.github/workflows/ci.yml' + - 'codecov.yml' + pull_request: + branches: + - main + paths: + - '**.go' + - 'go.*' + - '.github/workflows/codecov.yml' + - 'codecov.yml' + +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 + 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: Install sendmail + run: | + sudo apt-get -y install sendmail; which sendmail + - name: Run go test + 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 }}) + concurrency: + group: ci-lint + 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: golangci-lint + uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1 + with: + version: latest + dependency-review: + name: Dependency review + 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 + govulncheck: + name: Go vulnerabilities check + 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 + 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 ./... + 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 + 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: Install sendmail + run: | + sudo apt-get -y install sendmail; which sendmail + - 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 }} \ No newline at end of file diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml deleted file mode 100644 index b9661f8..0000000 --- a/.github/workflows/codecov.yml +++ /dev/null @@ -1,67 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Winni Neessen -# -# 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 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml deleted file mode 100644 index 56b5c10..0000000 --- a/.github/workflows/dependency-review.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml deleted file mode 100644 index 7313e04..0000000 --- a/.github/workflows/golangci-lint.yml +++ /dev/null @@ -1,54 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Winni Neessen -# -# 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 diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml deleted file mode 100644 index 9d5cdfb..0000000 --- a/.github/workflows/govulncheck.yml +++ /dev/null @@ -1,21 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Winni Neessen -# -# 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 \ No newline at end of file diff --git a/.github/workflows/offline-tests.yml b/.github/workflows/offline-tests.yml deleted file mode 100644 index 22cddd7..0000000 --- a/.github/workflows/offline-tests.yml +++ /dev/null @@ -1,45 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Winni Neessen -# -# 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 ./... \ No newline at end of file diff --git a/.github/workflows/reuse.yml b/.github/workflows/reuse.yml deleted file mode 100644 index 04fd414..0000000 --- a/.github/workflows/reuse.yml +++ /dev/null @@ -1,23 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Winni Neessen -# -# 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 diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml deleted file mode 100644 index d260ef7..0000000 --- a/.github/workflows/sonarqube.yml +++ /dev/null @@ -1,56 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Winni Neessen -# -# 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 }} From 12e9a0cb5dd3648e6d10c86f912a45f597537409 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 15:37:07 +0200 Subject: [PATCH 080/188] 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. --- .github/workflows/ci.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80b612d..c52ed81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,19 +7,9 @@ on: push: branches: - main - paths: - - '**.go' - - 'go.*' - - '.github/workflows/ci.yml' - - 'codecov.yml' pull_request: branches: - main - paths: - - '**.go' - - 'go.*' - - '.github/workflows/codecov.yml' - - 'codecov.yml' concurrency: group: ${{ github.workflow }}-${{ github.ref_name }} From 960c015a93cf917b3ead5a13f32fd1869beee41c Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 15:45:01 +0200 Subject: [PATCH 081/188] 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. --- msg_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/msg_test.go b/msg_test.go index 57abebb..1ff67b1 100644 --- a/msg_test.go +++ b/msg_test.go @@ -113,8 +113,6 @@ var ( {`hi\ there@domain.tld`, false}, // Spaces must be quoted {"hello@tld", true}, // TLD is enough {`你好@域名.顶级域名`, true}, // We speak RFC6532 - {`cow@[dead::beef]`, true}, // IPv6 is fine - {"1@[1.101.236.21]", true}, // IPv4 is fine {"1@23456789", true}, // Hypothetically valid, if somebody registers that TLD {"1@[23456789]", false}, // While 23456789 is decimal for 1.101.236.21 it is not RFC5322 compliant } @@ -1140,7 +1138,8 @@ func TestMsg_ToFromString(t *testing.T) { // checkAddrHeader validates a single email address in the AddrHeader of a message. func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, - wantMail, wantName string) { + wantMail, wantName string, +) { t.Helper() addresses, ok := message.addrHeader[header] if !ok { From 4d4aa1e1df909ed1315f879405ce47e2e20d638e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 15:47:04 +0200 Subject: [PATCH 082/188] 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. --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c52ed81..db3c512 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,6 +77,7 @@ jobs: version: latest dependency-review: name: Dependency review + runs-on: ubuntu-latest concurrency: group: ci-dependency-review cancel-in-progress: true From 9c57ba56cff99fa543ac603c9ee32e760fd7df53 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 15:48:10 +0200 Subject: [PATCH 083/188] 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. --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db3c512..04f7350 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,6 +55,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos lint: name: golangci-lint (${{ matrix.go }}) + runs-on: ubuntu-latest concurrency: group: ci-lint cancel-in-progress: true @@ -92,6 +93,7 @@ jobs: uses: actions/dependency-review-action@a6993e2c61fd5dc440b409aa1d6904921c5e1894 # v4.3.5 govulncheck: name: Go vulnerabilities check + runs-on: ubuntu-latest concurrency: group: ci-govulncheck cancel-in-progress: true From eeccee0d94cc22a76ffce96abce22ad53da48a60 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 15:50:14 +0200 Subject: [PATCH 084/188] 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. --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04f7350..c0f2fd6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,6 +72,8 @@ jobs: 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: From f82ac0c5ae325faf1bbe8ef8db96d2048517123f Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 15:52:27 +0200 Subject: [PATCH 085/188] 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. --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0f2fd6..4d08c79 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: name: Test with Codecov coverage (${{ matrix.os }} / ${{ matrix.go }}) runs-on: ${{ matrix.os }} concurrency: - group: ci-codecov + group: ci-codecov-${{ matrix.os }}-${{ matrix.go }} cancel-in-progress: true strategy: matrix: @@ -57,7 +57,7 @@ jobs: name: golangci-lint (${{ matrix.go }}) runs-on: ubuntu-latest concurrency: - group: ci-lint + group: ci-lint-${{ matrix.go }} cancel-in-progress: true strategy: matrix: @@ -110,7 +110,7 @@ jobs: name: Test (${{ matrix.os }} / ${{ matrix.go }}) runs-on: ${{ matrix.os }} concurrency: - group: ci-test + group: ci-test-${{ matrix.os }}-${{ matrix.go }} cancel-in-progress: true strategy: matrix: @@ -149,7 +149,7 @@ jobs: name: Test with SonarQube review (${{ matrix.os }} / ${{ matrix.go }}) runs-on: ${{ matrix.os }} concurrency: - group: ci-codecov + group: ci-codecov-${{ matrix.go }} cancel-in-progress: true strategy: matrix: From 3aef85e3247dc4aebc9b967a90f71c2b01bf9b04 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 15:57:27 +0200 Subject: [PATCH 086/188] 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. --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d08c79..c6238b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2024 The go-mail Authors +# +# SPDX-License-Identifier: MIT + name: CI permissions: From 9072aef3551448ca01518547006291835a921309 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 15:58:13 +0200 Subject: [PATCH 087/188] 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. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6238b8..c196ae7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -119,7 +119,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - go: ['1.19', '1.20', '1.21', '1.22', '1.23'] + go: ['1.21', '1.22', '1.23'] steps: - name: Harden Runner uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 From 4b8bf0507ddbb2b6aedaf84b30ea4a2b2c415d9c Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 16:25:42 +0200 Subject: [PATCH 088/188] 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. --- .github/workflows/ci.yml | 7 +++---- msg_nowin_test.go | 8 ++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c196ae7..fbd0342 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,7 @@ jobs: go: ['1.23'] env: PERFORM_ONLINE_TEST: ${{ vars.PERFORM_ONLINE_TEST }} + TEST_SENDMAIL: ${{ vars.TEST_SENDMAIL }} TEST_HOST: ${{ secrets.TEST_HOST }} TEST_USER: ${{ secrets.TEST_USER }} TEST_PASS: ${{ secrets.TEST_PASS }} @@ -48,8 +49,9 @@ jobs: check-latest: true - name: Install sendmail run: | - sudo apt-get -y install sendmail; which sendmail + apt-get -y update >/dev/null && apt-get -y upgrade >/dev/null && apt-get -y install ssmtp >/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 @@ -176,9 +178,6 @@ jobs: with: go-version: ${{ matrix.go }} check-latest: true - - name: Install sendmail - run: | - sudo apt-get -y install sendmail; which sendmail - name: Run go test run: | go test -shuffle=on -race --coverprofile=./cov.out ./... diff --git a/msg_nowin_test.go b/msg_nowin_test.go index 6cde71a..b0ef87b 100644 --- a/msg_nowin_test.go +++ b/msg_nowin_test.go @@ -16,8 +16,8 @@ import ( // 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") + if os.Getenv("TEST_SENDMAIL") != "true" { + t.Skipf("TEST_SENDMAIL variable is not set. Skipping sendmail test") } tests := []struct { name string @@ -45,8 +45,8 @@ func TestMsg_WriteToSendmailWithContext(t *testing.T) { // 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") + if os.Getenv("TEST_SENDMAIL") != "true" { + t.Skipf("TEST_SENDMAIL variable is not set. Skipping sendmail test") } _, err := os.Stat(SendmailPath) if err != nil { From c33900ca29f7f264671511ff7d8958f9e8701687 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 16:26:49 +0200 Subject: [PATCH 089/188] 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. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fbd0342..c0b2d1b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: check-latest: true - name: Install sendmail run: | - apt-get -y update >/dev/null && apt-get -y upgrade >/dev/null && apt-get -y install ssmtp >/dev/null && which sendmail + sudo apt-get -y update >/dev/null && sudo apt-get -y upgrade >/dev/null && sudo apt-get -y install ssmtp >/dev/null && which sendmail - name: Run go test if: success() run: | From a815c585711838cb825a67ec7614c755087b3964 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 16:45:07 +0200 Subject: [PATCH 090/188] 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. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0b2d1b..79e9629 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: check-latest: true - name: Install sendmail run: | - sudo apt-get -y update >/dev/null && sudo apt-get -y upgrade >/dev/null && sudo apt-get -y install ssmtp >/dev/null && which sendmail + 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: | From 273a26ca53d56079fc767ea995af7739608c0b08 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 17:10:50 +0200 Subject: [PATCH 091/188] 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. --- client_121_test.go | 121 +++++++++++++++++++++++++++++++++++++++++++++ client_test.go | 51 ------------------- 2 files changed, 121 insertions(+), 51 deletions(-) create mode 100644 client_121_test.go diff --git a/client_121_test.go b/client_121_test.go new file mode 100644 index 0000000..84f091a --- /dev/null +++ b/client_121_test.go @@ -0,0 +1,121 @@ +// 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" + "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 { + 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") + } + }) +} diff --git a/client_test.go b/client_test.go index 98a46b6..9d24293 100644 --- a/client_test.go +++ b/client_test.go @@ -268,21 +268,6 @@ func TestNewClient(t *testing.T) { }, false, nil, }, - { - "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, - }, { "WithHELO", WithHELO(hostname), func(c *Client) error { @@ -1759,42 +1744,6 @@ func TestClient_DialWithContext(t *testing.T) { t.Fatalf("client has connection") } }) - 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 { - 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") - } - }) t.Run("connect should fail on HELO", func(t *testing.T) { ctxFail, cancelFail := context.WithCancel(ctx) defer cancelFail() From 90e3162a228a24f45b5346bfef0ddb46bf4186c5 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 17:11:21 +0200 Subject: [PATCH 092/188] 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. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79e9629..39a0452 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,7 +121,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - go: ['1.21', '1.22', '1.23'] + go: ['1.19', '1.20', '1.21', '1.22', '1.23'] steps: - name: Harden Runner uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 From 23399ed84c14bcbce2cc6162f769f286076726b7 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 17:17:58 +0200 Subject: [PATCH 093/188] 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". --- client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client_test.go b/client_test.go index 9d24293..0382c5e 100644 --- a/client_test.go +++ b/client_test.go @@ -29,7 +29,7 @@ import ( const ( // DefaultHost is used as default hostname for the Client - DefaultHost = "localhost" + DefaultHost = "127.0.0.1" // TestRcpt is a trash mail address to send test mails to TestRcpt = "couttifaddebro-1473@yopmail.com" // TestServerProto is the protocol used for the simple SMTP test server From d7b32480fdeb0d2607c076c1e4e7399c178813c7 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 17:33:52 +0200 Subject: [PATCH 094/188] 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. --- client_121_test.go | 5 ++ client_test.go | 150 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 152 insertions(+), 3 deletions(-) diff --git a/client_121_test.go b/client_121_test.go index 84f091a..0c96545 100644 --- a/client_121_test.go +++ b/client_121_test.go @@ -12,6 +12,7 @@ import ( "context" "errors" "fmt" + "net" "os" "reflect" "strings" @@ -95,6 +96,10 @@ func TestClient_DialWithContextNewVersionsOnly(t *testing.T) { } 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() { diff --git a/client_test.go b/client_test.go index 0382c5e..e3d03c7 100644 --- a/client_test.go +++ b/client_test.go @@ -1166,6 +1166,10 @@ func TestClient_SetDebugLog(t *testing.T) { client.SetDebugLog(true) 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 test server: %s", err) } t.Cleanup(func() { @@ -1195,6 +1199,10 @@ func TestClient_SetDebugLog(t *testing.T) { client.SetDebugLog(false) 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 test server: %s", err) } t.Cleanup(func() { @@ -1220,6 +1228,10 @@ func TestClient_SetDebugLog(t *testing.T) { } 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 test server: %s", err) } t.Cleanup(func() { @@ -1544,6 +1556,10 @@ func TestClient_Close(t *testing.T) { 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) } if !client.smtpClient.HasConnection() { @@ -1578,6 +1594,10 @@ func TestClient_Close(t *testing.T) { 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) } if !client.smtpClient.HasConnection() { @@ -1616,6 +1636,10 @@ func TestClient_Close(t *testing.T) { 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) } if !client.smtpClient.HasConnection() { @@ -1650,6 +1674,10 @@ func TestClient_DialWithContext(t *testing.T) { 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() { @@ -1673,6 +1701,10 @@ func TestClient_DialWithContext(t *testing.T) { client.fallbackPort = serverPort 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() { @@ -1836,6 +1868,10 @@ func TestClient_DialWithContext(t *testing.T) { 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) } }) @@ -1866,6 +1902,10 @@ func TestClient_DialWithContext(t *testing.T) { 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) } }) @@ -1958,6 +1998,10 @@ func TestClient_DialWithContext(t *testing.T) { 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) } if err := client.Close(); err != nil { @@ -1989,6 +2033,10 @@ func TestClient_Reset(t *testing.T) { 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() { @@ -2022,6 +2070,10 @@ func TestClient_Reset(t *testing.T) { 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) } if err = client.Close(); err != nil { @@ -2057,6 +2109,10 @@ func TestClient_Reset(t *testing.T) { 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() { @@ -2094,6 +2150,10 @@ func TestClient_DialAndSendWithContext(t *testing.T) { t.Fatalf("failed to create new client: %s", err) } if err = client.DialAndSend(message); 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 dial and send: %s", err) } }) @@ -2122,6 +2182,10 @@ func TestClient_DialAndSendWithContext(t *testing.T) { t.Fatalf("failed to create new client: %s", err) } if err = client.DialAndSendWithContext(ctxDial, message); 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 dial and send: %s", err) } }) @@ -2260,6 +2324,10 @@ func TestClient_auth(t *testing.T) { 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 test service: %s", err) } if err := client.Close(); err != nil { @@ -2447,6 +2515,10 @@ func TestClient_Send(t *testing.T) { 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 test server: %s", err) } t.Cleanup(func() { @@ -2499,6 +2571,10 @@ func TestClient_Send(t *testing.T) { 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 test server: %s", err) } t.Cleanup(func() { @@ -2556,6 +2632,10 @@ func TestClient_sendSingleMsg(t *testing.T) { 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 test server: %s", err) } t.Cleanup(func() { @@ -2595,6 +2675,10 @@ func TestClient_sendSingleMsg(t *testing.T) { 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 test server: %s", err) } t.Cleanup(func() { @@ -2636,6 +2720,10 @@ func TestClient_sendSingleMsg(t *testing.T) { 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 test server: %s", err) } t.Cleanup(func() { @@ -2682,6 +2770,10 @@ func TestClient_sendSingleMsg(t *testing.T) { 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 test server: %s", err) } t.Cleanup(func() { @@ -2728,6 +2820,10 @@ func TestClient_sendSingleMsg(t *testing.T) { 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 test server: %s", err) } t.Cleanup(func() { @@ -2774,6 +2870,10 @@ func TestClient_sendSingleMsg(t *testing.T) { 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 test server: %s", err) } t.Cleanup(func() { @@ -2813,6 +2913,10 @@ func TestClient_sendSingleMsg(t *testing.T) { 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 test server: %s", err) } t.Cleanup(func() { @@ -2860,6 +2964,10 @@ func TestClient_sendSingleMsg(t *testing.T) { 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 test server: %s", err) } t.Cleanup(func() { @@ -2907,6 +3015,10 @@ func TestClient_sendSingleMsg(t *testing.T) { 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 test server: %s", err) } t.Cleanup(func() { @@ -2953,6 +3065,10 @@ func TestClient_sendSingleMsg(t *testing.T) { 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 test server: %s", err) } t.Cleanup(func() { @@ -2999,6 +3115,10 @@ func TestClient_sendSingleMsg(t *testing.T) { 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 test server: %s", err) } t.Cleanup(func() { @@ -3045,6 +3165,10 @@ func TestClient_checkConn(t *testing.T) { 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 test server: %s", err) } t.Cleanup(func() { @@ -3082,6 +3206,10 @@ func TestClient_checkConn(t *testing.T) { 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 test server: %s", err) } t.Cleanup(func() { @@ -3148,7 +3276,11 @@ func TestClient_onlinetests(t *testing.T) { t.Cleanup(cancel) if err = client.DialWithContext(ctx); err != nil { - t.Errorf("failed to dial to test server: %s", err) + 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 dial to test server: %s", err) } if err = client.smtpClient.Noop(); err != nil { t.Errorf("failed to send noop: %s", err) @@ -3190,7 +3322,11 @@ func TestClient_onlinetests(t *testing.T) { t.Cleanup(cancel) if err = client.DialWithContext(ctx); err != nil { - t.Errorf("failed to dial to test server: %s", err) + 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 dial to test server: %s", err) } if err = client.smtpClient.Noop(); err != nil { t.Errorf("failed to send noop: %s", err) @@ -3232,7 +3368,11 @@ func TestClient_onlinetests(t *testing.T) { t.Cleanup(cancel) if err = client.DialWithContext(ctx); err != nil { - t.Errorf("failed to dial to test server: %s", err) + 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 dial to test server: %s", err) } if err = client.smtpClient.Noop(); err != nil { t.Errorf("failed to send noop: %s", err) @@ -3274,6 +3414,10 @@ func TestClient_XOAuth2OnFaker(t *testing.T) { t.Fatalf("unable to create new client: %v", err) } if err = c.DialWithContext(context.Background()); 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("unexpected dial error: %v", err) } if err = c.Close(); err != nil { From 855d7f08670af0de375396ae568390fdbc825cfd Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 18:21:50 +0200 Subject: [PATCH 095/188] 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. --- msg.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/msg.go b/msg.go index bd771b0..587894f 100644 --- a/msg.go +++ b/msg.go @@ -847,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. From 03cb09c3bd195e487d6955f069ee59e5d6bc56d0 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 18:22:01 +0200 Subject: [PATCH 096/188] 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. --- msg_test.go | 178 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) diff --git a/msg_test.go b/msg_test.go index 1ff67b1..0cfffd9 100644 --- a/msg_test.go +++ b/msg_test.go @@ -1136,6 +1136,184 @@ func TestMsg_ToFromString(t *testing.T) { }) } +func TestMsg_Cc(t *testing.T) { + t.Run("Cc with valid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Cc("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set Cc: %s", err) + } + checkAddrHeader(t, message, HeaderCc, "Cc", 0, 1, "toni.tester@example.com", "") + }) + t.Run("Cc with multiple valid addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Cc("toni.tester@example.com", "tina.tester@example.com"); err != nil { + t.Fatalf("failed to set Cc: %s", err) + } + checkAddrHeader(t, message, HeaderCc, "Cc", 0, 2, "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderCc, "Cc", 1, 2, "tina.tester@example.com", "") + }) + t.Run("Cc with invalid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Cc("invalid"); err == nil { + t.Fatalf("Cc should fail with invalid address") + } + }) + t.Run("Cc with empty string should fail", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Cc(""); err == nil { + t.Fatalf("Cc should fail with invalid address") + } + }) + t.Run("Cc with different RFC5322 addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + for _, tt := range rfc5322Test { + t.Run(tt.value, func(t *testing.T) { + err := message.Cc(tt.value) + if err != nil && tt.valid { + t.Errorf("Cc on address %s should succeed, but failed with: %s", tt.value, err) + } + if err == nil && !tt.valid { + t.Errorf("Cc on address %s should fail, but succeeded", tt.value) + } + }) + } + }) +} + +func TestMsg_AddCc(t *testing.T) { + t.Run("AddCc with valid addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Cc("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set Cc: %s", err) + } + if err := message.AddCc("tina.tester@example.com"); err != nil { + t.Fatalf("failed to set additional Cc: %s", err) + } + checkAddrHeader(t, message, HeaderCc, "AddCc", 0, 2, "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderCc, "AddCc", 1, 2, "tina.tester@example.com", "") + }) + t.Run("AddCc with invalid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Cc("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set Cc: %s", err) + } + if err := message.AddCc("invalid"); err == nil { + t.Errorf("AddCc should fail with invalid address") + } + checkAddrHeader(t, message, HeaderCc, "AddCc", 0, 1, "toni.tester@example.com", "") + }) +} + +func TestMsg_AddCcFormat(t *testing.T) { + t.Run("AddCcFormat with valid addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Cc("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set Cc: %s", err) + } + if err := message.AddCcFormat("Tina Tester", "tina.tester@example.com"); err != nil { + t.Fatalf("failed to set additional Cc: %s", err) + } + checkAddrHeader(t, message, HeaderCc, "AddCcFormat", 0, 2, "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderCc, "AddCcFormat", 1, 2, "tina.tester@example.com", "Tina Tester") + }) + t.Run("AddCcFormat with invalid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Cc("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set Cc: %s", err) + } + if err := message.AddCcFormat("Invalid", "invalid"); err == nil { + t.Errorf("AddCcFormat should fail with invalid address") + } + checkAddrHeader(t, message, HeaderCc, "AddCcFormat", 0, 1, "toni.tester@example.com", "") + }) +} + +func TestMsg_CcIgnoreInvalid(t *testing.T) { + t.Run("CcIgnoreInvalid with valid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.CcIgnoreInvalid("toni.tester@example.com") + checkAddrHeader(t, message, HeaderCc, "CcIgnoreInvalid", 0, 1, "toni.tester@example.com", "") + }) + t.Run("CcIgnoreInvalid with invalid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.CcIgnoreInvalid("invalid") + addresses, ok := message.addrHeader[HeaderCc] + if !ok { + t.Fatalf("failed to set CcIgnoreInvalid, addrHeader field is not set") + } + if len(addresses) != 0 { + t.Fatalf("failed to set CcIgnoreInvalid, addrHeader value count is: %d, want: 0", len(addresses)) + } + }) + t.Run("CcIgnoreInvalid with valid and invalid addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.CcIgnoreInvalid("toni.tester@example.com", "invalid", "tina.tester@example.com") + checkAddrHeader(t, message, HeaderCc, "CcIgnoreInvalid", 0, 2, "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderCc, "CcIgnoreInvalid", 1, 2, "tina.tester@example.com", "") + }) +} + +func TestMsg_CcFromString(t *testing.T) { + t.Run("CcFromString with valid addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.CcFromString(`toni.tester@example.com,`); err != nil { + t.Fatalf("failed to set CcFromString: %s", err) + } + checkAddrHeader(t, message, HeaderCc, "CcFromString", 0, 2, "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderCc, "CcFromString", 1, 2, "tina.tester@example.com", "") + }) + t.Run("CcFromString with valid addresses and empty fields", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.CcFromString(`toni.tester@example.com ,,`); err != nil { + t.Fatalf("failed to set CcFromString: %s", err) + } + checkAddrHeader(t, message, HeaderCc, "CcFromString", 0, 2, "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderCc, "CcFromString", 1, 2, "tina.tester@example.com", "") + }) +} + // checkAddrHeader validates a single email address in the AddrHeader of a message. func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, wantMail, wantName string, From f079ea09eb4b1856ca1dd93d3ab1d5544dd7ee22 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 18:29:26 +0200 Subject: [PATCH 097/188] 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. --- msg.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/msg.go b/msg.go index 587894f..c96bfac 100644 --- a/msg.go +++ b/msg.go @@ -942,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. From 953a4b4df11f7ecf07876314eff23061d2555778 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 18:29:51 +0200 Subject: [PATCH 098/188] 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. --- msg_test.go | 178 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) diff --git a/msg_test.go b/msg_test.go index 0cfffd9..ce490f0 100644 --- a/msg_test.go +++ b/msg_test.go @@ -1314,6 +1314,184 @@ func TestMsg_CcFromString(t *testing.T) { }) } +func TestMsg_Bcc(t *testing.T) { + t.Run("Bcc with valid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Bcc("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set Bcc: %s", err) + } + checkAddrHeader(t, message, HeaderBcc, "Bcc", 0, 1, "toni.tester@example.com", "") + }) + t.Run("Bcc with multiple valid addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Bcc("toni.tester@example.com", "tina.tester@example.com"); err != nil { + t.Fatalf("failed to set Bcc: %s", err) + } + checkAddrHeader(t, message, HeaderBcc, "Bcc", 0, 2, "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderBcc, "Bcc", 1, 2, "tina.tester@example.com", "") + }) + t.Run("Bcc with invalid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Bcc("invalid"); err == nil { + t.Fatalf("Bcc should fail with invalid address") + } + }) + t.Run("Bcc with empty string should fail", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Bcc(""); err == nil { + t.Fatalf("Bcc should fail with invalid address") + } + }) + t.Run("Bcc with different RFC5322 addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + for _, tt := range rfc5322Test { + t.Run(tt.value, func(t *testing.T) { + err := message.Bcc(tt.value) + if err != nil && tt.valid { + t.Errorf("Bcc on address %s should succeed, but failed with: %s", tt.value, err) + } + if err == nil && !tt.valid { + t.Errorf("Bcc on address %s should fail, but succeeded", tt.value) + } + }) + } + }) +} + +func TestMsg_AddBcc(t *testing.T) { + t.Run("AddBcc with valid addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Bcc("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set Bcc: %s", err) + } + if err := message.AddBcc("tina.tester@example.com"); err != nil { + t.Fatalf("failed to set additional Bcc: %s", err) + } + checkAddrHeader(t, message, HeaderBcc, "AddBcc", 0, 2, "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderBcc, "AddBcc", 1, 2, "tina.tester@example.com", "") + }) + t.Run("AddBcc with invalid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Bcc("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set Bcc: %s", err) + } + if err := message.AddBcc("invalid"); err == nil { + t.Errorf("AddBcc should fail with invalid address") + } + checkAddrHeader(t, message, HeaderBcc, "AddBcc", 0, 1, "toni.tester@example.com", "") + }) +} + +func TestMsg_AddBccFormat(t *testing.T) { + t.Run("AddBccFormat with valid addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Bcc("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set Bcc: %s", err) + } + if err := message.AddBccFormat("Tina Tester", "tina.tester@example.com"); err != nil { + t.Fatalf("failed to set additional Bcc: %s", err) + } + checkAddrHeader(t, message, HeaderBcc, "AddBccFormat", 0, 2, "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderBcc, "AddBccFormat", 1, 2, "tina.tester@example.com", "Tina Tester") + }) + t.Run("AddBccFormat with invalid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Bcc("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set Bcc: %s", err) + } + if err := message.AddBccFormat("Invalid", "invalid"); err == nil { + t.Errorf("AddBccFormat should fail with invalid address") + } + checkAddrHeader(t, message, HeaderBcc, "AddBccFormat", 0, 1, "toni.tester@example.com", "") + }) +} + +func TestMsg_BccIgnoreInvalid(t *testing.T) { + t.Run("BccIgnoreInvalid with valid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.BccIgnoreInvalid("toni.tester@example.com") + checkAddrHeader(t, message, HeaderBcc, "BccIgnoreInvalid", 0, 1, "toni.tester@example.com", "") + }) + t.Run("BccIgnoreInvalid with invalid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.BccIgnoreInvalid("invalid") + addresses, ok := message.addrHeader[HeaderBcc] + if !ok { + t.Fatalf("failed to set BccIgnoreInvalid, addrHeader field is not set") + } + if len(addresses) != 0 { + t.Fatalf("failed to set BccIgnoreInvalid, addrHeader value count is: %d, want: 0", len(addresses)) + } + }) + t.Run("BccIgnoreInvalid with valid and invalid addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.BccIgnoreInvalid("toni.tester@example.com", "invalid", "tina.tester@example.com") + checkAddrHeader(t, message, HeaderBcc, "BccIgnoreInvalid", 0, 2, "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderBcc, "BccIgnoreInvalid", 1, 2, "tina.tester@example.com", "") + }) +} + +func TestMsg_BccFromString(t *testing.T) { + t.Run("BccFromString with valid addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.BccFromString(`toni.tester@example.com,`); err != nil { + t.Fatalf("failed to set BccFromString: %s", err) + } + checkAddrHeader(t, message, HeaderBcc, "BccFromString", 0, 2, "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderBcc, "BccFromString", 1, 2, "tina.tester@example.com", "") + }) + t.Run("BccFromString with valid addresses and empty fields", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.BccFromString(`toni.tester@example.com ,,`); err != nil { + t.Fatalf("failed to set BccFromString: %s", err) + } + checkAddrHeader(t, message, HeaderBcc, "BccFromString", 0, 2, "toni.tester@example.com", "") + checkAddrHeader(t, message, HeaderBcc, "BccFromString", 1, 2, "tina.tester@example.com", "") + }) +} + // checkAddrHeader validates a single email address in the AddrHeader of a message. func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, wantMail, wantName string, From 96d45c26bc27e0e240b7f02c34315c5ea70cc401 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 18:40:53 +0200 Subject: [PATCH 099/188] 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. --- msg_test.go | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/msg_test.go b/msg_test.go index ce490f0..aed62d8 100644 --- a/msg_test.go +++ b/msg_test.go @@ -1492,7 +1492,56 @@ func TestMsg_BccFromString(t *testing.T) { }) } -// checkAddrHeader validates a single email address in the AddrHeader of a message. +func TestMsg_ReplyTo(t *testing.T) { + t.Run("ReplyTo with valid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.ReplyTo("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set ReplyTo: %s", err) + } + checkGenHeader(t, message, HeaderReplyTo, "ReplyTo", 0, 1, "") + }) + t.Run("ReplyTo with invalid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.ReplyTo("invalid"); err == nil { + t.Fatalf("ReplyTo should fail with invalid address") + } + }) + t.Run("ReplyTo with empty string should fail", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.ReplyTo(""); err == nil { + t.Fatalf("ReplyTo should fail with invalid address") + } + }) + t.Run("ReplyTo with different RFC5322 addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + for _, tt := range rfc5322Test { + t.Run(tt.value, func(t *testing.T) { + err := message.ReplyTo(tt.value) + if err != nil && tt.valid { + t.Errorf("ReplyTo on address %s should succeed, but failed with: %s", tt.value, err) + } + if err == nil && !tt.valid { + t.Errorf("ReplyTo on address %s should fail, but succeeded", tt.value) + } + }) + } + }) +} + +// checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. +// It checks whether the AddrHeader contains the correct address, name, and number of fields. func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, wantMail, wantName string, ) { @@ -1519,6 +1568,23 @@ func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, f } } +// checkGenHeader validates the generated header in an email message, verifying its presence and expected values. +func checkGenHeader(t *testing.T, message *Msg, header Header, fn string, field, wantFields int, + wantVal string, +) { + t.Helper() + values, ok := message.genHeader[header] + if !ok { + t.Fatalf("failed to set %s, genHeader field is not set", fn) + } + if len(values) != wantFields { + t.Fatalf("failed to set %s, genHeader value count is: %d, want: %d", fn, len(values), field) + } + if values[field] != wantVal { + t.Errorf("failed to set %s, genHeader value is %s, want: %s", fn, values[field], wantVal) + } +} + /* // TestNewMsgWithMiddleware tests WithMiddleware From 5b602be818d7cabfe11c08944d9706a7d19610e8 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 18:46:46 +0200 Subject: [PATCH 100/188] 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. --- msg_test.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/msg_test.go b/msg_test.go index aed62d8..9bf5f36 100644 --- a/msg_test.go +++ b/msg_test.go @@ -1540,6 +1540,28 @@ func TestMsg_ReplyTo(t *testing.T) { }) } +func TestMsg_ReplyToFormat(t *testing.T) { + t.Run("ReplyToFormat with valid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.ReplyToFormat("Tina Tester", "tina.tester@example.com"); err != nil { + t.Fatalf("failed to set ReplyTo: %s", err) + } + checkGenHeader(t, message, HeaderReplyTo, "ReplyToFormat", 0, 1, `"Tina Tester" `) + }) + t.Run("ReplyToFormat with invalid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.ReplyToFormat("Invalid", "invalid"); err == nil { + t.Errorf("ReplyToFormat should fail with invalid address") + } + }) +} + // checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. // It checks whether the AddrHeader contains the correct address, name, and number of fields. func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, From 007286fc5ec3bb4ef2672354947f027a03831578 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 19:03:31 +0200 Subject: [PATCH 101/188] 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. --- msg_test.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/msg_test.go b/msg_test.go index 9bf5f36..2c0301b 100644 --- a/msg_test.go +++ b/msg_test.go @@ -1562,6 +1562,43 @@ func TestMsg_ReplyToFormat(t *testing.T) { }) } +func TestMsg_Subject(t *testing.T) { + tests := []struct { + name string + subject string + want string + }{ + {"Normal latin characters", "Hello world!", "Hello world!"}, + {"Empty string", "", ""}, + { + "Japanese characters", `これはテスト対象です。`, + `=?UTF-8?q?=E3=81=93=E3=82=8C=E3=81=AF=E3=83=86=E3=82=B9=E3=83=88=E5=AF=BE?= ` + + `=?UTF-8?q?=E8=B1=A1=E3=81=A7=E3=81=99=E3=80=82?=`, + }, + { + "Simplified chinese characters", `这是一个测试主题`, + `=?UTF-8?q?=E8=BF=99=E6=98=AF=E4=B8=80=E4=B8=AA=E6=B5=8B=E8=AF=95=E4=B8=BB?= ` + + `=?UTF-8?q?=E9=A2=98?=`, + }, + { + "Cyrillic characters", `Это испытуемый`, + `=?UTF-8?q?=D0=AD=D1=82=D0=BE_=D0=B8=D1=81=D0=BF=D1=8B=D1=82=D1=83=D0=B5?= ` + + `=?UTF-8?q?=D0=BC=D1=8B=D0=B9?=`, + }, + {"Emoji characters", `New Offer 🚀`, `=?UTF-8?q?New_Offer_=F0=9F=9A=80?=`}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.Subject(tt.subject) + checkGenHeader(t, message, HeaderSubject, "Subject", 0, 1, tt.want) + }) + } +} + // checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. // It checks whether the AddrHeader contains the correct address, name, and number of fields. func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, From 591425bb99b18fcc5f654e341886699ed9712cbd Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 19:10:25 +0200 Subject: [PATCH 102/188] 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. --- msg_test.go | 380 +--------------------------------------------------- 1 file changed, 2 insertions(+), 378 deletions(-) diff --git a/msg_test.go b/msg_test.go index 2c0301b..b928e40 100644 --- a/msg_test.go +++ b/msg_test.go @@ -1570,6 +1570,8 @@ func TestMsg_Subject(t *testing.T) { }{ {"Normal latin characters", "Hello world!", "Hello world!"}, {"Empty string", "", ""}, + {"European umlaut characters", "Héllô wörld! äöüß", + "=?UTF-8?q?H=C3=A9ll=C3=B4_w=C3=B6rld!_=C3=A4=C3=B6=C3=BC=C3=9F?="}, { "Japanese characters", `これはテスト対象です。`, `=?UTF-8?q?=E3=81=93=E3=82=8C=E3=81=AF=E3=83=86=E3=82=B9=E3=83=88=E5=AF=BE?= ` + @@ -1694,225 +1696,6 @@ func checkGenHeader(t *testing.T, message *Msg, header Header, fn string, field, } } -// TestMsg_AddTo tests the Msg.AddTo method - - func TestMsg_AddTo(t *testing.T) { - a := []string{"address1@example.com", "address2@example.com"} - na := "address3@example.com" - m := NewMsg() - if err := m.To(a...); err != nil { - t.Errorf("failed to set TO addresses: %s", err) - return - } - if err := m.AddTo(na); err != nil { - t.Errorf("AddTo failed: %s", err) - return - } - - atf := false - for _, v := range m.addrHeader[HeaderTo] { - if v.Address == na { - atf = true - } - } - if !atf { - t.Errorf("AddTo() failed. Address %q not found in TO address slice.", na) - } - } - -// TestMsg_AddToFormat tests the Msg.AddToFormat method - - func TestMsg_AddToFormat(t *testing.T) { - a := []string{"address1@example.com", "address2@example.com"} - nn := "Toni Tester" - na := "address3@example.com" - w := `"Toni Tester" ` - m := NewMsg() - if err := m.To(a...); err != nil { - t.Errorf("failed to set TO addresses: %s", err) - return - } - if err := m.AddToFormat(nn, na); err != nil { - t.Errorf("AddToFormat failed: %s", err) - return - } - - atf := false - for _, v := range m.addrHeader[HeaderTo] { - if v.String() == w { - atf = true - } - } - if !atf { - t.Errorf("AddToFormat() failed. Address %q not found in TO address slice.", w) - } - } - -// TestMsg_ToIgnoreInvalid tests the Msg.ToIgnoreInvalid method - - func TestMsg_ToIgnoreInvalid(t *testing.T) { - a := []string{"address1@example.com", "address2@example.com"} - fa := []string{"address1@example.com", "address2@example.com", "failedaddress.com"} - m := NewMsg() - - m.ToIgnoreInvalid(a...) - l := len(m.addrHeader[HeaderTo]) - if l != len(a) { - t.Errorf("ToIgnoreInvalid() failed. Expected %d addresses, got: %d", len(a), l) - } - m.ToIgnoreInvalid(fa...) - l = len(m.addrHeader[HeaderTo]) - if l != len(fa)-1 { - t.Errorf("ToIgnoreInvalid() failed. Expected %d addresses, got: %d", len(fa)-1, l) - } - } - -// TestMsg_AddCc tests the Msg.AddCc method - - func TestMsg_AddCc(t *testing.T) { - a := []string{"address1@example.com", "address2@example.com"} - na := "address3@example.com" - m := NewMsg() - if err := m.Cc(a...); err != nil { - t.Errorf("failed to set CC addresses: %s", err) - return - } - if err := m.AddCc(na); err != nil { - t.Errorf("AddCc failed: %s", err) - return - } - - atf := false - for _, v := range m.addrHeader[HeaderCc] { - if v.Address == na { - atf = true - } - } - if !atf { - t.Errorf("AddCc() failed. Address %q not found in CC address slice.", na) - } - } - -// TestMsg_AddCcFormat tests the Msg.AddCcFormat method - - func TestMsg_AddCcFormat(t *testing.T) { - a := []string{"address1@example.com", "address2@example.com"} - nn := "Toni Tester" - na := "address3@example.com" - w := `"Toni Tester" ` - m := NewMsg() - if err := m.Cc(a...); err != nil { - t.Errorf("failed to set CC addresses: %s", err) - return - } - if err := m.AddCcFormat(nn, na); err != nil { - t.Errorf("AddCcFormat failed: %s", err) - return - } - - atf := false - for _, v := range m.addrHeader[HeaderCc] { - if v.String() == w { - atf = true - } - } - if !atf { - t.Errorf("AddCcFormat() failed. Address %q not found in CC address slice.", w) - } - } - -// TestMsg_CcIgnoreInvalid tests the Msg.CcIgnoreInvalid method - - func TestMsg_CcIgnoreInvalid(t *testing.T) { - a := []string{"address1@example.com", "address2@example.com"} - fa := []string{"address1@example.com", "address2@example.com", "failedaddress.com"} - m := NewMsg() - - m.CcIgnoreInvalid(a...) - l := len(m.addrHeader[HeaderCc]) - if l != len(a) { - t.Errorf("CcIgnoreInvalid() failed. Expected %d addresses, got: %d", len(a), l) - } - m.CcIgnoreInvalid(fa...) - l = len(m.addrHeader[HeaderCc]) - if l != len(fa)-1 { - t.Errorf("CcIgnoreInvalid() failed. Expected %d addresses, got: %d", len(fa)-1, l) - } - } - -// TestMsg_AddBcc tests the Msg.AddBcc method - - func TestMsg_AddBcc(t *testing.T) { - a := []string{"address1@example.com", "address2@example.com"} - na := "address3@example.com" - m := NewMsg() - if err := m.Bcc(a...); err != nil { - t.Errorf("failed to set BCC addresses: %s", err) - return - } - if err := m.AddBcc(na); err != nil { - t.Errorf("AddBcc failed: %s", err) - return - } - - atf := false - for _, v := range m.addrHeader[HeaderBcc] { - if v.Address == na { - atf = true - } - } - if !atf { - t.Errorf("AddBcc() failed. Address %q not found in BCC address slice.", na) - } - } - -// TestMsg_AddBccFormat tests the Msg.AddBccFormat method - - func TestMsg_AddBccFormat(t *testing.T) { - a := []string{"address1@example.com", "address2@example.com"} - nn := "Toni Tester" - na := "address3@example.com" - w := `"Toni Tester" ` - m := NewMsg() - if err := m.Bcc(a...); err != nil { - t.Errorf("failed to set BCC addresses: %s", err) - return - } - if err := m.AddBccFormat(nn, na); err != nil { - t.Errorf("AddBccFormat failed: %s", err) - return - } - - atf := false - for _, v := range m.addrHeader[HeaderBcc] { - if v.String() == w { - atf = true - } - } - if !atf { - t.Errorf("AddBccFormat() failed. Address %q not found in BCC address slice.", w) - } - } - -// TestMsg_BccIgnoreInvalid tests the Msg.BccIgnoreInvalid method - - func TestMsg_BccIgnoreInvalid(t *testing.T) { - a := []string{"address1@example.com", "address2@example.com"} - fa := []string{"address1@example.com", "address2@example.com", "failedaddress.com"} - m := NewMsg() - - m.BccIgnoreInvalid(a...) - l := len(m.addrHeader[HeaderBcc]) - if l != len(a) { - t.Errorf("BccIgnoreInvalid() failed. Expected %d addresses, got: %d", len(a), l) - } - m.BccIgnoreInvalid(fa...) - l = len(m.addrHeader[HeaderBcc]) - if l != len(fa)-1 { - t.Errorf("BccIgnoreInvalid() failed. Expected %d addresses, got: %d", len(fa)-1, l) - } - } - // TestMsg_SetBulk tests the Msg.SetBulk method func TestMsg_SetBulk(t *testing.T) { @@ -2043,63 +1826,6 @@ func checkGenHeader(t *testing.T, message *Msg, header Header, fn string, field, } } -// TestMsg_FromFormat tests the FromFormat and EnvelopeFrom methods for the Msg object - - func TestMsg_FromFormat(t *testing.T) { - tests := []struct { - tname string - name string - addr string - want string - fail bool - }{ - { - "valid name and addr", "Toni Tester", "tester@example.com", - `"Toni Tester" `, false, - }, - { - "no name with valid addr", "", "tester@example.com", - ``, false, - }, - { - "valid name with invalid addr", "Toni Tester", "@example.com", - ``, true, - }, - } - - m := NewMsg() - for _, tt := range tests { - t.Run(tt.tname, func(t *testing.T) { - if err := m.FromFormat(tt.name, tt.addr); err != nil && !tt.fail { - t.Errorf("failed to FromFormat(): %s", err) - return - } - if err := m.EnvelopeFromFormat(tt.name, tt.addr); err != nil && !tt.fail { - t.Errorf("failed to EnvelopeFromFormat(): %s", err) - return - } - - var fa *mail.Address - f, ok := m.addrHeader[HeaderFrom] - if ok && len(f) > 0 { - fa = f[0] - } - if (!ok || len(f) == 0) && !tt.fail { - t.Errorf(`valid from address expected, but "From:" field is empty`) - return - } - if tt.fail && len(f) > 0 { - t.Errorf("FromFormat() was supposed to failed but got value: %s", fa.String()) - return - } - - if !tt.fail && fa.String() != tt.want { - t.Errorf("wrong result for FromFormat(). Want: %s, got: %s", tt.want, fa.String()) - } - m.addrHeader[HeaderFrom] = nil - }) - } - } func TestMsg_GetRecipients(t *testing.T) { a := []string{"to@example.com", "cc@example.com", "bcc@example.com"} @@ -2156,108 +1882,6 @@ func checkGenHeader(t *testing.T, message *Msg, header Header, fn string, field, } } -// TestMsg_ReplyTo tests the Msg.ReplyTo and Msg.ReplyToFormat methods - - func TestMsg_ReplyTo(t *testing.T) { - tests := []struct { - tname string - name string - addr string - want string - sf bool - }{ - { - "valid name and addr", "Toni Tester", "tester@example.com", - `"Toni Tester" `, false, - }, - { - "no name with valid addr", "", "tester@example.com", - ``, false, - }, - { - "valid name with invalid addr", "Toni Tester", "@example.com", - ``, true, - }, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.tname, func(t *testing.T) { - if err := m.ReplyTo(tt.want); err != nil && !tt.sf { - t.Errorf("ReplyTo() method failed: %s", err) - } - if !tt.sf { - rt, ok := m.genHeader[HeaderReplyTo] - if !ok { - t.Errorf("ReplyTo() failed: ReplyTo generic header not set") - return - } - if len(rt) <= 0 { - t.Errorf("ReplyTo() failed: length of generic ReplyTo header is zero or less than zero") - return - } - if rt[0] != tt.want { - t.Errorf("ReplyTo() failed: expected value: %s, got: %s", tt.want, rt[0]) - } - } - m.genHeader = nil - m.genHeader = make(map[Header][]string) - if err := m.ReplyToFormat(tt.name, tt.addr); err != nil && !tt.sf { - t.Errorf("ReplyToFormat() method failed: %s", err) - } - if !tt.sf { - rt, ok := m.genHeader[HeaderReplyTo] - if !ok { - t.Errorf("ReplyTo() failed: ReplyTo generic header not set") - return - } - if len(rt) <= 0 { - t.Errorf("ReplyTo() failed: length of generic ReplyTo header is zero or less than zero") - return - } - if rt[0] != tt.want { - t.Errorf("ReplyTo() failed: expected value: %s, got: %s", tt.want, rt[0]) - } - } - m.genHeader = nil - m.genHeader = make(map[Header][]string) - }) - } - } - -// TestMsg_Subject tests the Msg.Subject method - - func TestMsg_Subject(t *testing.T) { - tests := []struct { - name string - sub string - want string - }{ - {"normal subject", "This is a test subject", "This is a test subject"}, - { - "subject with umlauts", "This is a test subject with umlauts: üäöß", - "=?UTF-8?q?This_is_a_test_subject_with_umlauts:_=C3=BC=C3=A4=C3=B6=C3=9F?=", - }, - { - "subject with emoji", "This is a test subject with emoji: 📧", - "=?UTF-8?q?This_is_a_test_subject_with_emoji:_=F0=9F=93=A7?=", - }, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m.Subject(tt.sub) - s, ok := m.genHeader[HeaderSubject] - if !ok || len(s) <= 0 { - t.Errorf("Subject() method failed. Generic header for Subject is empty") - return - } - if s[0] != tt.want { - t.Errorf("Subject() method failed. Expected: %s, got: %s", tt.want, s[0]) - } - }) - } - } - // TestMsg_SetImportance tests the Msg.SetImportance method func TestMsg_SetImportance(t *testing.T) { From ea5b02bfdd78d440110b1e22a9a59ba051b331b6 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 19:30:48 +0200 Subject: [PATCH 103/188] 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. --- msg_test.go | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/msg_test.go b/msg_test.go index b928e40..71826d3 100644 --- a/msg_test.go +++ b/msg_test.go @@ -1601,6 +1601,67 @@ func TestMsg_Subject(t *testing.T) { } } +func TestMsg_SetMessageID(t *testing.T) { + t.Run("SetMessageID randomness", func(t *testing.T) { + var mids []string + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + for i := 0; i < 50_000; i++ { + message.SetMessageID() + mid := message.GetMessageID() + mids = append(mids, mid) + } + c := make(map[string]int) + for i := range mids { + c[mids[i]]++ + } + for k, v := range c { + if v > 1 { + t.Errorf("MessageID randomness not given. MessageID %q was generated %d times", k, v) + } + } + }) +} +func TestMsg_GetMessageID(t *testing.T) { + t.Run("GetMessageID with normal IDs", func(t *testing.T) { + tests := []struct { + msgid string + want string + }{ + {"this.is.a.test", ""}, + {"12345.6789@domain.com", "<12345.6789@domain.com>"}, + {"abcd1234@sub.domain.com", ""}, + {"uniqeID-123@domain.co.tld", ""}, + {"2024_10_26192300@domain.tld", "<2024_10_26192300@domain.tld>"}, + } + for _, tt := range tests { + t.Run(tt.msgid, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetMessageIDWithValue(tt.msgid) + msgid := message.GetMessageID() + if !strings.EqualFold(tt.want, msgid) { + t.Errorf("GetMessageID() failed. Want: %s, got: %s", tt.want, msgid) + } + }) + } + }) + t.Run("GetMessageID no messageID set should return an empty string", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + msgid := message.GetMessageID() + if msgid != "" { + t.Errorf("GetMessageID() failed. Want: empty string, got: %s", msgid) + } + }) +} + // checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. // It checks whether the AddrHeader contains the correct address, name, and number of fields. func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, From ae15a12ce568a990715a2345931503f04ca6c0f2 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 19:57:02 +0200 Subject: [PATCH 104/188] 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. --- msg.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/msg.go b/msg.go index c96bfac..e19f8e5 100644 --- a/msg.go +++ b/msg.go @@ -1087,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. From ef3f103c30c40bce7feed6af84a291c431207fc7 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 20:08:16 +0200 Subject: [PATCH 105/188] 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. --- msg_test.go | 98 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 75 insertions(+), 23 deletions(-) diff --git a/msg_test.go b/msg_test.go index 71826d3..83ba6d9 100644 --- a/msg_test.go +++ b/msg_test.go @@ -9,6 +9,7 @@ import ( "reflect" "strings" "testing" + "time" ) var ( @@ -1662,6 +1663,80 @@ func TestMsg_GetMessageID(t *testing.T) { }) } +func TestMsg_SetMessageIDWithValue(t *testing.T) { + // We have already covered SetMessageIDWithValue in SetMessageID and GetMessageID + t.Skip("SetMessageIDWithValue is fully covered by TestMsg_GetMessageID") +} + +func TestMsg_SetBulk(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetBulk() + checkGenHeader(t, message, HeaderPrecedence, "SetBulk", 0, 1, "bulk") + checkGenHeader(t, message, HeaderXAutoResponseSuppress, "Bulk", 0, 1, "All") +} + +func TestMsg_SetDate(t *testing.T) { + t.Run("SetDate and compare date down to the minute", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + + message.SetDate() + values, ok := message.genHeader[HeaderDate] + if !ok { + t.Fatal("failed to set SetDate, genHeader field is not set") + } + if len(values) != 1 { + t.Fatalf("failed to set SetDate, genHeader value count is: %d, want: %d", len(values), 1) + } + date := values[0] + parsed, err := time.Parse(time.RFC1123Z, date) + if err != nil { + t.Fatalf("SetDate failed, failed to parse retrieved date: %s, error: %s", date, err) + } + now := time.Now() + nowNoSec := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), 0, 0, now.Location()) + parsedNoSec := time.Date(parsed.Year(), parsed.Month(), parsed.Day(), parsed.Hour(), parsed.Minute(), + 0, 0, parsed.Location()) + if !nowNoSec.Equal(parsedNoSec) { + t.Errorf("SetDate failed, retrieved date mismatch, got: %s, want: %s", parsedNoSec.String(), + nowNoSec.String()) + } + }) +} + +func TestMsg_SetDateWithValue(t *testing.T) { + t.Run("SetDateWithValue and compare date down to the second", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + + now := time.Now() + message.SetDateWithValue(now) + values, ok := message.genHeader[HeaderDate] + if !ok { + t.Fatal("failed to set SetDate, genHeader field is not set") + } + if len(values) != 1 { + t.Fatalf("failed to set SetDate, genHeader value count is: %d, want: %d", len(values), 1) + } + date := values[0] + parsed, err := time.Parse(time.RFC1123Z, date) + if err != nil { + t.Fatalf("SetDate failed, failed to parse retrieved date: %s, error: %s", date, err) + } + if !strings.EqualFold(parsed.Format(time.RFC1123Z), now.Format(time.RFC1123Z)) { + t.Errorf("SetDate failed, retrieved date mismatch, got: %s, want: %s", now.Format(time.RFC1123Z), + parsed.Format(time.RFC1123Z)) + } + }) +} + // checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. // It checks whether the AddrHeader contains the correct address, name, and number of fields. func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, @@ -1757,29 +1832,6 @@ func checkGenHeader(t *testing.T, message *Msg, header Header, fn string, field, } } -// TestMsg_SetBulk tests the Msg.SetBulk method - - func TestMsg_SetBulk(t *testing.T) { - m := NewMsg() - m.SetBulk() - if m.genHeader[HeaderPrecedence] == nil { - t.Errorf("SetBulk() failed. Precedence header is nil") - return - } - if m.genHeader[HeaderPrecedence][0] != "bulk" { - t.Errorf("SetBulk() failed. Expected Precedence header: %q, got: %q", "bulk", - m.genHeader[HeaderPrecedence][0]) - } - if m.genHeader[HeaderXAutoResponseSuppress] == nil { - t.Errorf("SetBulk() failed. X-Auto-Response-Suppress header is nil") - return - } - if m.genHeader[HeaderXAutoResponseSuppress][0] != "All" { - t.Errorf("SetBulk() failed. Expected X-Auto-Response-Suppress header: %q, got: %q", "All", - m.genHeader[HeaderXAutoResponseSuppress][0]) - } - } - // TestMsg_SetDate tests the Msg.SetDate and Msg.SetDateWithValue method func TestMsg_SetDate(t *testing.T) { From f5279cd584ef05658a6652a01a25cd6a2f3cf231 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 20:22:04 +0200 Subject: [PATCH 106/188] 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. --- msg_test.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/msg_test.go b/msg_test.go index 83ba6d9..59b4236 100644 --- a/msg_test.go +++ b/msg_test.go @@ -1571,8 +1571,10 @@ func TestMsg_Subject(t *testing.T) { }{ {"Normal latin characters", "Hello world!", "Hello world!"}, {"Empty string", "", ""}, - {"European umlaut characters", "Héllô wörld! äöüß", - "=?UTF-8?q?H=C3=A9ll=C3=B4_w=C3=B6rld!_=C3=A4=C3=B6=C3=BC=C3=9F?="}, + { + "European umlaut characters", "Héllô wörld! äöüß", + "=?UTF-8?q?H=C3=A9ll=C3=B4_w=C3=B6rld!_=C3=A4=C3=B6=C3=BC=C3=9F?=", + }, { "Japanese characters", `これはテスト対象です。`, `=?UTF-8?q?=E3=81=93=E3=82=8C=E3=81=AF=E3=83=86=E3=82=B9=E3=83=88=E5=AF=BE?= ` + @@ -1625,6 +1627,7 @@ func TestMsg_SetMessageID(t *testing.T) { } }) } + func TestMsg_GetMessageID(t *testing.T) { t.Run("GetMessageID with normal IDs", func(t *testing.T) { tests := []struct { From 42c63791ef2ac7f41e24975df9345e5bc458c5ba Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 21:06:21 +0200 Subject: [PATCH 107/188] 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. --- client.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client.go b/client.go index 50d2f28..abb90f4 100644 --- a/client.go +++ b/client.go @@ -1261,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{ From cf117d320b223b2f6e64bef6b8663c0188ff075c Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 21:07:04 +0200 Subject: [PATCH 108/188] 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. --- client_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client_test.go b/client_test.go index e3d03c7..70032ba 100644 --- a/client_test.go +++ b/client_test.go @@ -3527,6 +3527,9 @@ func parseJSONLog(t *testing.T, buf *bytes.Buffer) logData { func testMessage(t *testing.T) *Msg { t.Helper() message := NewMsg() + if message == nil { + t.Fatal("failed to create new message") + } if err := message.From(TestSenderValid); err != nil { t.Errorf("failed to set sender address: %s", err) } From 9e51dba82aca98504da51c12d3c58930227d24d5 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 21:07:48 +0200 Subject: [PATCH 109/188] 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. --- msg_test.go | 281 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 175 insertions(+), 106 deletions(-) diff --git a/msg_test.go b/msg_test.go index 59b4236..d085713 100644 --- a/msg_test.go +++ b/msg_test.go @@ -5,7 +5,10 @@ package mail import ( + "context" + "errors" "fmt" + "net" "reflect" "strings" "testing" @@ -1740,6 +1743,178 @@ func TestMsg_SetDateWithValue(t *testing.T) { }) } +func TestMsg_SetImportance(t *testing.T) { + tests := []struct { + name string + importance Importance + }{ + {"Non-Urgent", ImportanceNonUrgent}, + {"Low", ImportanceLow}, + {"Normal", ImportanceNormal}, + {"High", ImportanceHigh}, + {"Urgent", ImportanceUrgent}, + {"Unknown", 9}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetImportance(tt.importance) + if tt.importance == ImportanceNormal { + t.Skip("ImportanceNormal is does currently not set any values") + } + checkGenHeader(t, message, HeaderImportance, "SetImportance", 0, 1, tt.importance.String()) + checkGenHeader(t, message, HeaderPriority, "SetImportance", 0, 1, tt.importance.NumString()) + checkGenHeader(t, message, HeaderXPriority, "SetImportance", 0, 1, tt.importance.XPrioString()) + checkGenHeader(t, message, HeaderXMSMailPriority, "SetImportance", 0, 1, tt.importance.NumString()) + }) + } +} + +func TestMsg_SetOrganization(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetOrganization("ACME Inc.") + checkGenHeader(t, message, HeaderOrganization, "SetOrganization", 0, 1, "ACME Inc.") +} + +func TestMsg_SetUserAgent(t *testing.T) { + t.Run("SetUserAgent with value", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetUserAgent("go-mail test suite") + checkGenHeader(t, message, HeaderUserAgent, "SetUserAgent", 0, 1, "go-mail test suite") + checkGenHeader(t, message, HeaderXMailer, "SetUserAgent", 0, 1, "go-mail test suite") + }) + t.Run("Message without SetUserAgent should provide default agent", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + want := fmt.Sprintf("go-mail v%s // https://github.com/wneessen/go-mail", VERSION) + message.checkUserAgent() + checkGenHeader(t, message, HeaderUserAgent, "SetUserAgent", 0, 1, want) + checkGenHeader(t, message, HeaderXMailer, "SetUserAgent", 0, 1, want) + }) +} + +func TestMsg_IsDelivered(t *testing.T) { + t.Run("IsDelivered on unsent message", func(t *testing.T) { + message := testMessage(t) + if message.IsDelivered() { + t.Error("IsDelivered on unsent message should return false") + } + }) + t.Run("IsDelivered on sent message", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-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) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + + message := testMessage(t) + if err = client.DialAndSend(message); 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 test server: %s", err) + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + + if !message.IsDelivered() { + t.Error("IsDelivered on sent message should return true") + } + }) + t.Run("IsDelivered on failed message delivery (DATA close)", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnDataClose: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + + message := testMessage(t) + if err = client.DialAndSend(message); err == nil { + t.Error("message delivery was supposed to fail on data close") + } + if message.IsDelivered() { + t.Error("IsDelivered on failed message delivery should return false") + } + }) + t.Run("IsDelivered on failed message delivery (final RESET)", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnReset: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + + message := testMessage(t) + if err = client.DialAndSend(message); err == nil { + t.Error("message delivery was supposed to fail on data close") + } + if !message.IsDelivered() { + t.Error("IsDelivered on sent message should return true") + } + }) +} + // checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. // It checks whether the AddrHeader contains the correct address, name, and number of fields. func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, @@ -1835,112 +2010,6 @@ func checkGenHeader(t *testing.T, message *Msg, header Header, fn string, field, } } -// TestMsg_SetDate tests the Msg.SetDate and Msg.SetDateWithValue method - - func TestMsg_SetDate(t *testing.T) { - m := NewMsg() - m.SetDate() - if m.genHeader[HeaderDate] == nil { - t.Errorf("SetDate() failed. Date header is nil") - return - } - d, ok := m.genHeader[HeaderDate] - if !ok { - t.Errorf("failed to get date header") - return - } - _, err := time.Parse(time.RFC1123Z, d[0]) - if err != nil { - t.Errorf("failed to parse time in date header: %s", err) - } - m.genHeader = nil - m.genHeader = make(map[Header][]string) - - now := time.Now() - m.SetDateWithValue(now) - if m.genHeader[HeaderDate] == nil { - t.Errorf("SetDateWithValue() failed. Date header is nil") - return - } - d, ok = m.genHeader[HeaderDate] - if !ok { - t.Errorf("failed to get date header") - return - } - pt, err := time.Parse(time.RFC1123Z, d[0]) - if err != nil { - t.Errorf("failed to parse time in date header: %s", err) - } - if pt.Unix() != now.Unix() { - t.Errorf("SetDateWithValue() failed. Expected time: %d, got: %d", now.Unix(), - pt.Unix()) - } - } - -// TestMsg_SetMessageIDWIthValue tests the Msg.SetMessageIDWithValue and Msg.SetMessageID methods - - func TestMsg_SetMessageIDWithValue(t *testing.T) { - m := NewMsg() - m.SetMessageID() - if m.genHeader[HeaderMessageID] == nil { - t.Errorf("SetMessageID() failed. MessageID header is nil") - return - } - if m.genHeader[HeaderMessageID][0] == "" { - t.Errorf("SetMessageID() failed. Expected value, got: empty") - return - } - if _, ok := m.genHeader[HeaderMessageID]; ok { - m.genHeader[HeaderMessageID] = nil - } - v := "This.is.a.message.id" - vf := "" - m.SetMessageIDWithValue(v) - if m.genHeader[HeaderMessageID] == nil { - t.Errorf("SetMessageIDWithValue() failed. MessageID header is nil") - return - } - if m.genHeader[HeaderMessageID][0] != vf { - t.Errorf("SetMessageIDWithValue() failed. Expected: %s, got: %s", vf, m.genHeader[HeaderMessageID][0]) - return - } - } - -// TestMsg_SetMessageIDRandomness tests the randomness of Msg.SetMessageID methods - - func TestMsg_SetMessageIDRandomness(t *testing.T) { - var mids []string - m := NewMsg() - for i := 0; i < 50_000; i++ { - m.SetMessageID() - mid := m.GetMessageID() - mids = append(mids, mid) - } - c := make(map[string]int) - for i := range mids { - c[mids[i]]++ - } - for k, v := range c { - if v > 1 { - t.Errorf("MessageID randomness not given. MessageID %q was generated %d times", k, v) - } - } - } - - func TestMsg_GetMessageID(t *testing.T) { - expected := "this.is.a.message.id" - msg := NewMsg() - msg.SetMessageIDWithValue(expected) - val := msg.GetMessageID() - if !strings.EqualFold(val, fmt.Sprintf("<%s>", expected)) { - t.Errorf("GetMessageID() failed. Expected: %s, got: %s", fmt.Sprintf("<%s>", expected), val) - } - msg.genHeader[HeaderMessageID] = nil - val = msg.GetMessageID() - if val != "" { - t.Errorf("GetMessageID() failed. Expected empty string, got: %s", val) - } - } func TestMsg_GetRecipients(t *testing.T) { From 4f97cd8261ab28be5abf07e5db1326e66537ac25 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 21:33:43 +0200 Subject: [PATCH 110/188] 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. --- msg.go | 7 +++--- msg_test.go | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/msg.go b/msg.go index e19f8e5..8ce7a91 100644 --- a/msg.go +++ b/msg.go @@ -1187,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) @@ -1195,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 } diff --git a/msg_test.go b/msg_test.go index d085713..31a7565 100644 --- a/msg_test.go +++ b/msg_test.go @@ -1915,6 +1915,76 @@ func TestMsg_IsDelivered(t *testing.T) { }) } +func TestMsg_RequestMDNTo(t *testing.T) { + t.Run("RequestMDNTo with valid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.RequestMDNTo("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set RequestMDNTo: %s", err) + } + checkGenHeader(t, message, HeaderDispositionNotificationTo, "RequestMDNTo", 0, 1, "") + }) + t.Run("RequestMDNTo with valid address and nil-genHeader", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.genHeader = nil + if err := message.RequestMDNTo("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set RequestMDNTo: %s", err) + } + checkGenHeader(t, message, HeaderDispositionNotificationTo, "RequestMDNTo", 0, 1, "") + }) + t.Run("RequestMDNTo with multiple valid addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.RequestMDNTo("toni.tester@example.com", "tina.tester@example.com"); err != nil { + t.Fatalf("failed to set RequestMDNTo: %s", err) + } + checkGenHeader(t, message, HeaderDispositionNotificationTo, "RequestMDNTo", 0, 2, "") + checkGenHeader(t, message, HeaderDispositionNotificationTo, "RequestMDNTo", 1, 2, "") + }) + t.Run("RequestMDNTo with invalid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.RequestMDNTo("invalid"); err == nil { + t.Fatalf("RequestMDNTo should fail with invalid address") + } + }) + t.Run("RequestMDNTo with empty string should fail", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.RequestMDNTo(""); err == nil { + t.Fatalf("RequestMDNTo should fail with invalid address") + } + }) + t.Run("RequestMDNTo with different RFC5322 addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + for _, tt := range rfc5322Test { + t.Run(tt.value, func(t *testing.T) { + err := message.RequestMDNTo(tt.value) + if err != nil && tt.valid { + t.Errorf("RequestMDNTo on address %s should succeed, but failed with: %s", tt.value, err) + } + if err == nil && !tt.valid { + t.Errorf("RequestMDNTo on address %s should fail, but succeeded", tt.value) + } + }) + } + }) +} + // checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. // It checks whether the AddrHeader contains the correct address, name, and number of fields. func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, From 4fe90228158268fe32b21057dec61bf3b935379c Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 22:13:37 +0200 Subject: [PATCH 111/188] 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. --- msg.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/msg.go b/msg.go index 8ce7a91..c770973 100644 --- a/msg.go +++ b/msg.go @@ -1237,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 } From 5c2831c33103ea6804e816c1eae18d8c77e9b3e4 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 22:13:51 +0200 Subject: [PATCH 112/188] 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. --- msg_test.go | 206 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) diff --git a/msg_test.go b/msg_test.go index 31a7565..dffc33b 100644 --- a/msg_test.go +++ b/msg_test.go @@ -1984,6 +1984,212 @@ func TestMsg_RequestMDNTo(t *testing.T) { } }) } +func TestMsg_RequestMDNToFormat(t *testing.T) { + t.Run("RequestMDNToFormat with valid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.RequestMDNToFormat("Toni Tester", "toni.tester@example.com"); err != nil { + t.Fatalf("failed to set RequestMDNToFormat: %s", err) + } + checkGenHeader(t, message, HeaderDispositionNotificationTo, "RequestMDNToFormat", 0, 1, + `"Toni Tester" `) + }) + t.Run("RequestMDNToFormat with invalid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.RequestMDNToFormat("invalid", "invalid"); err == nil { + t.Fatalf("RequestMDNToFormat should fail with invalid address") + } + }) +} +func TestMsg_RequestMDNAddTo(t *testing.T) { + t.Run("RequestMDNAddTo with valid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.RequestMDNTo("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set RequestMDNTo: %s", err) + } + if err := message.RequestMDNAddTo("tina.tester@example.com"); err != nil { + t.Fatalf("failed to set RequestMDNAddTo: %s", err) + } + checkGenHeader(t, message, HeaderDispositionNotificationTo, "RequestMDNAddTo", 0, 2, + ``) + checkGenHeader(t, message, HeaderDispositionNotificationTo, "RequestMDNAddTo", 1, 2, + ``) + }) + t.Run("RequestMDNAddTo with invalid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.RequestMDNTo("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set RequestMDNTo: %s", err) + } + if err := message.RequestMDNAddTo("invalid"); err == nil { + t.Errorf("RequestMDNAddTo should fail with invalid address") + } + checkGenHeader(t, message, HeaderDispositionNotificationTo, "RequestMDNAddTo", 0, 1, + ``) + }) +} + +func TestMsg_RequestMDNAddToFormat(t *testing.T) { + t.Run("RequestMDNAddToFormat with valid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.RequestMDNTo("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set RequestMDNTo: %s", err) + } + if err := message.RequestMDNAddToFormat("Tina Tester", "tina.tester@example.com"); err != nil { + t.Fatalf("failed to set RequestMDNAddToFormat: %s", err) + } + checkGenHeader(t, message, HeaderDispositionNotificationTo, "RequestMDNAddToFormat", 0, 2, + ``) + checkGenHeader(t, message, HeaderDispositionNotificationTo, "RequestMDNAddToFormat", 1, 2, + `"Tina Tester" `) + }) + t.Run("RequestMDNAddToFormat with invalid address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.RequestMDNTo("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set RequestMDNTo: %s", err) + } + if err := message.RequestMDNAddToFormat("invalid", "invalid"); err == nil { + t.Errorf("RequestMDNAddToFormat should fail with invalid address") + } + checkGenHeader(t, message, HeaderDispositionNotificationTo, "RequestMDNAddToFormat", 0, 1, + ``) + }) +} + +func TestMsg_GetSender(t *testing.T) { + t.Run("GetSender with envelope from only (no full address)", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.EnvelopeFrom("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set envelope from address: %s", err) + } + sender, err := message.GetSender(false) + if err != nil { + t.Errorf("failed to get sender: %s", err) + } + if !strings.EqualFold(sender, "toni.tester@example.com") { + t.Errorf("GetSender: expected sender not returned. Want: %s, got: %s", "toni.tester@example.com", sender) + } + }) + t.Run("GetSender with envelope from only (full address)", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.EnvelopeFrom("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set envelope from address: %s", err) + } + sender, err := message.GetSender(true) + if err != nil { + t.Errorf("failed to get sender: %s", err) + } + if !strings.EqualFold(sender, "") { + t.Errorf("GetSender: expected sender not returned. Want: %s, got: %s", "", sender) + } + }) + t.Run("GetSender with from only (no full address)", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.From("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set from address: %s", err) + } + sender, err := message.GetSender(false) + if err != nil { + t.Errorf("failed to get sender: %s", err) + } + if !strings.EqualFold(sender, "toni.tester@example.com") { + t.Errorf("GetSender: expected sender not returned. Want: %s, got: %s", "toni.tester@example.com", sender) + } + }) + t.Run("GetSender with from only (full address)", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.From("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set from address: %s", err) + } + sender, err := message.GetSender(true) + if err != nil { + t.Errorf("failed to get sender: %s", err) + } + if !strings.EqualFold(sender, "") { + t.Errorf("GetSender: expected sender not returned. Want: %s, got: %s", "", sender) + } + }) + t.Run("GetSender with envelope from and from (no full address)", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.EnvelopeFrom("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set envelope from address: %s", err) + } + if err := message.From("tina.tester@example.com"); err != nil { + t.Fatalf("failed to set from address: %s", err) + } + sender, err := message.GetSender(false) + if err != nil { + t.Errorf("failed to get sender: %s", err) + } + if !strings.EqualFold(sender, "toni.tester@example.com") { + t.Errorf("GetSender: expected sender not returned. Want: %s, got: %s", "toni.tester@example.com", sender) + } + }) + t.Run("GetSender with envelope from and from (full address)", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.EnvelopeFrom("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set envelope from address: %s", err) + } + if err := message.From("tina.tester@example.com"); err != nil { + t.Fatalf("failed to set from address: %s", err) + } + sender, err := message.GetSender(true) + if err != nil { + t.Errorf("failed to get sender: %s", err) + } + if !strings.EqualFold(sender, "") { + t.Errorf("GetSender: expected sender not returned. Want: %s, got: %s", "", sender) + } + }) + t.Run("GetSender with no envelope from or from", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + _, err := message.GetSender(false) + if err == nil { + t.Errorf("GetSender with no envelope from or from should return error") + } + if !errors.Is(err, ErrNoFromAddress) { + t.Errorf("GetSender with no envelope from or from should return error. Want: %s, got: %s", + ErrNoFromAddress, err) + } + }) +} // checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. // It checks whether the AddrHeader contains the correct address, name, and number of fields. From 842d4373f207edc0e040f19f2172ec771a887ca8 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 22:14:56 +0200 Subject: [PATCH 113/188] 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. --- msg_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/msg_test.go b/msg_test.go index dffc33b..377f659 100644 --- a/msg_test.go +++ b/msg_test.go @@ -1984,6 +1984,7 @@ func TestMsg_RequestMDNTo(t *testing.T) { } }) } + func TestMsg_RequestMDNToFormat(t *testing.T) { t.Run("RequestMDNToFormat with valid address", func(t *testing.T) { message := NewMsg() @@ -2006,6 +2007,7 @@ func TestMsg_RequestMDNToFormat(t *testing.T) { } }) } + func TestMsg_RequestMDNAddTo(t *testing.T) { t.Run("RequestMDNAddTo with valid address", func(t *testing.T) { message := NewMsg() From 7b600534ea8a629af23f4cfd57fc2f5d915ae559 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 22:31:13 +0200 Subject: [PATCH 114/188] 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. --- msg_test.go | 191 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) diff --git a/msg_test.go b/msg_test.go index 377f659..982dd2f 100644 --- a/msg_test.go +++ b/msg_test.go @@ -2193,6 +2193,197 @@ func TestMsg_GetSender(t *testing.T) { }) } +func TestMsg_GetRecipients(t *testing.T) { + t.Run("GetRecipients with only to", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.To("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set to address: %s", err) + } + rcpts, err := message.GetRecipients() + if err != nil { + t.Errorf("failed to get recipients: %s", err) + } + if len(rcpts) != 1 { + t.Fatalf("GetRecipients: expected 1 recipient, got: %d", len(rcpts)) + } + if !strings.EqualFold(rcpts[0], "toni.tester@example.com") { + t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + "toni.tester@example.com", rcpts[0]) + } + }) + t.Run("GetRecipients with only cc", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Cc("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set cc address: %s", err) + } + rcpts, err := message.GetRecipients() + if err != nil { + t.Errorf("failed to get recipients: %s", err) + } + if len(rcpts) != 1 { + t.Fatalf("GetRecipients: expected 1 recipient, got: %d", len(rcpts)) + } + if !strings.EqualFold(rcpts[0], "toni.tester@example.com") { + t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + "toni.tester@example.com", rcpts[0]) + } + }) + t.Run("GetRecipients with only bcc", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Bcc("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set bcc address: %s", err) + } + rcpts, err := message.GetRecipients() + if err != nil { + t.Errorf("failed to get recipients: %s", err) + } + if len(rcpts) != 1 { + t.Fatalf("GetRecipients: expected 1 recipient, got: %d", len(rcpts)) + } + if !strings.EqualFold(rcpts[0], "toni.tester@example.com") { + t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + "toni.tester@example.com", rcpts[0]) + } + }) + t.Run("GetRecipients with to and cc", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.To("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set to address: %s", err) + } + if err := message.Cc("tina.tester@example.com"); err != nil { + t.Fatalf("failed to set cc address: %s", err) + } + rcpts, err := message.GetRecipients() + if err != nil { + t.Errorf("failed to get recipients: %s", err) + } + if len(rcpts) != 2 { + t.Fatalf("GetRecipients: expected 2 recipient, got: %d", len(rcpts)) + } + if !strings.EqualFold(rcpts[0], "toni.tester@example.com") { + t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + "toni.tester@example.com", rcpts[0]) + } + if !strings.EqualFold(rcpts[1], "tina.tester@example.com") { + t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + "tina.tester@example.com", rcpts[1]) + } + }) + t.Run("GetRecipients with to and bcc", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.To("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set to address: %s", err) + } + if err := message.Bcc("tina.tester@example.com"); err != nil { + t.Fatalf("failed to set bcc address: %s", err) + } + rcpts, err := message.GetRecipients() + if err != nil { + t.Errorf("failed to get recipients: %s", err) + } + if len(rcpts) != 2 { + t.Fatalf("GetRecipients: expected 2 recipient, got: %d", len(rcpts)) + } + if !strings.EqualFold(rcpts[0], "toni.tester@example.com") { + t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + "toni.tester@example.com", rcpts[0]) + } + if !strings.EqualFold(rcpts[1], "tina.tester@example.com") { + t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + "tina.tester@example.com", rcpts[1]) + } + }) + t.Run("GetRecipients with cc and bcc", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Cc("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set cc address: %s", err) + } + if err := message.Bcc("tina.tester@example.com"); err != nil { + t.Fatalf("failed to set bcc address: %s", err) + } + rcpts, err := message.GetRecipients() + if err != nil { + t.Errorf("failed to get recipients: %s", err) + } + if len(rcpts) != 2 { + t.Fatalf("GetRecipients: expected 2 recipient, got: %d", len(rcpts)) + } + if !strings.EqualFold(rcpts[0], "toni.tester@example.com") { + t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + "toni.tester@example.com", rcpts[0]) + } + if !strings.EqualFold(rcpts[1], "tina.tester@example.com") { + t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + "tina.tester@example.com", rcpts[1]) + } + }) + t.Run("GetRecipients with to, cc and bcc", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.To("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set to address: %s", err) + } + if err := message.Cc("tina.tester@example.com"); err != nil { + t.Fatalf("failed to set cc address: %s", err) + } + if err := message.Bcc("tom.tester@example.com"); err != nil { + t.Fatalf("failed to set bcc address: %s", err) + } + rcpts, err := message.GetRecipients() + if err != nil { + t.Errorf("failed to get recipients: %s", err) + } + if len(rcpts) != 3 { + t.Fatalf("GetRecipients: expected 3 recipient, got: %d", len(rcpts)) + } + if !strings.EqualFold(rcpts[0], "toni.tester@example.com") { + t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + "toni.tester@example.com", rcpts[0]) + } + if !strings.EqualFold(rcpts[1], "tina.tester@example.com") { + t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + "tina.tester@example.com", rcpts[1]) + } + if !strings.EqualFold(rcpts[2], "tom.tester@example.com") { + t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + "tina.tester@example.com", rcpts[2]) + } + }) + t.Run("GetRecipients with no recipients", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + _, err := message.GetRecipients() + if err == nil { + t.Errorf("GetRecipients: expected error, got nil") + } + if !errors.Is(err, ErrNoRcptAddresses) { + t.Errorf("GetRecipients: expected ErrNoRcptAddresses, got: %s", err) + } + }) +} + // checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. // It checks whether the AddrHeader contains the correct address, name, and number of fields. func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, From 5e5bcef696a47bcd9512c6d38ecf1661e8c25635 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 22:50:31 +0200 Subject: [PATCH 115/188] 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." --- msg_test.go | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/msg_test.go b/msg_test.go index 982dd2f..0c3c441 100644 --- a/msg_test.go +++ b/msg_test.go @@ -2384,6 +2384,82 @@ func TestMsg_GetRecipients(t *testing.T) { }) } +func TestMsg_GetAddrHeader(t *testing.T) { + t.Run("GetAddrHeader with valid address (from)", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.From("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set header: %s", err) + } + addrheader := message.GetAddrHeader(HeaderFrom) + if len(addrheader) != 1 { + t.Errorf("GetAddrHeader: expected 1 address, got: %d", len(addrheader)) + } + if addrheader[0] == nil { + t.Fatalf("GetAddrHeader: expected address, got nil") + } + if addrheader[0].String() != "" { + t.Errorf("GetAddrHeader: expected address not returned. Want: %s, got: %s", + "", addrheader[0].String()) + } + }) + t.Run("GetAddrHeader with valid address (to, cc, bcc)", func(t *testing.T) { + var fn func(...string) error + for _, tt := range addrHeaderTests { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + + switch tt.header { + case HeaderFrom: + continue + case HeaderTo: + fn = message.To + case HeaderCc: + fn = message.Cc + case HeaderBcc: + fn = message.Bcc + default: + t.Logf("header %s not supported", tt.header) + continue + } + t.Run(tt.name, func(t *testing.T) { + if err := fn("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set header: %s", err) + } + addrheader := message.GetAddrHeader(tt.header) + if len(addrheader) != 1 { + t.Errorf("GetAddrHeader: expected 1 address, got: %d", len(addrheader)) + } + if addrheader[0] == nil { + t.Fatalf("GetAddrHeader: expected address, got nil") + } + if addrheader[0].String() != "" { + t.Errorf("GetAddrHeader: expected address not returned. Want: %s, got: %s", + "", addrheader[0].String()) + } + }) + } + }) + t.Run("GetAddrHeader with no addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + for _, tt := range addrHeaderTests { + t.Run(tt.name, func(t *testing.T) { + addrheader := message.GetAddrHeader(HeaderFrom) + if len(addrheader) != 0 { + t.Errorf("GetAddrHeader: expected 0 address, got: %d", len(addrheader)) + } + }) + } + }) +} + // checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. // It checks whether the AddrHeader contains the correct address, name, and number of fields. func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, From d16ae61f64547b62c74ea5e976799d01525d7a5a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 22:53:30 +0200 Subject: [PATCH 116/188] 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. --- msg_test.go | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/msg_test.go b/msg_test.go index 0c3c441..f622359 100644 --- a/msg_test.go +++ b/msg_test.go @@ -2444,6 +2444,59 @@ func TestMsg_GetAddrHeader(t *testing.T) { }) } }) + t.Run("GetAddrHeader with multiple valid address (to, cc, bcc)", func(t *testing.T) { + var fn func(...string) error + var addfn func(string) error + for _, tt := range addrHeaderTests { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + + switch tt.header { + case HeaderFrom: + continue + case HeaderTo: + fn = message.To + addfn = message.AddTo + case HeaderCc: + fn = message.Cc + addfn = message.AddCc + case HeaderBcc: + fn = message.Bcc + addfn = message.AddBcc + default: + t.Logf("header %s not supported", tt.header) + continue + } + t.Run(tt.name, func(t *testing.T) { + if err := fn("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set header: %s", err) + } + if err := addfn("tina.tester@example.com"); err != nil { + t.Fatalf("failed to set additional header value: %s", err) + } + addrheader := message.GetAddrHeader(tt.header) + if len(addrheader) != 2 { + t.Errorf("GetAddrHeader: expected 1 address, got: %d", len(addrheader)) + } + if addrheader[0] == nil { + t.Fatalf("GetAddrHeader: expected address, got nil") + } + if addrheader[0].String() != "" { + t.Errorf("GetAddrHeader: expected address not returned. Want: %s, got: %s", + "", addrheader[0].String()) + } + if addrheader[1] == nil { + t.Fatalf("GetAddrHeader: expected address, got nil") + } + if addrheader[1].String() != "" { + t.Errorf("GetAddrHeader: expected address not returned. Want: %s, got: %s", + "", addrheader[1].String()) + } + }) + } + }) t.Run("GetAddrHeader with no addresses", func(t *testing.T) { message := NewMsg() if message == nil { From 22f56a014342e491ae62ebc891b686970f1d6cc0 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 22:57:47 +0200 Subject: [PATCH 117/188] 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. --- msg_test.go | 129 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/msg_test.go b/msg_test.go index f622359..bd39146 100644 --- a/msg_test.go +++ b/msg_test.go @@ -2513,6 +2513,135 @@ func TestMsg_GetAddrHeader(t *testing.T) { }) } +func TestMsg_GetAddrHeaderString(t *testing.T) { + t.Run("GetAddrHeaderString with valid address (from)", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.From("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set header: %s", err) + } + addrheader := message.GetAddrHeaderString(HeaderFrom) + if len(addrheader) != 1 { + t.Errorf("GetAddrHeaderString: expected 1 address, got: %d", len(addrheader)) + } + if addrheader[0] == "" { + t.Fatalf("GetAddrHeaderString: expected address, got empty string") + } + if addrheader[0] != "" { + t.Errorf("GetAddrHeaderString: expected address not returned. Want: %s, got: %s", + "", addrheader[0]) + } + }) + t.Run("GetAddrHeaderString with valid address (to, cc, bcc)", func(t *testing.T) { + var fn func(...string) error + for _, tt := range addrHeaderTests { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + + switch tt.header { + case HeaderFrom: + continue + case HeaderTo: + fn = message.To + case HeaderCc: + fn = message.Cc + case HeaderBcc: + fn = message.Bcc + default: + t.Logf("header %s not supported", tt.header) + continue + } + t.Run(tt.name, func(t *testing.T) { + if err := fn("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set header: %s", err) + } + addrheader := message.GetAddrHeaderString(tt.header) + if len(addrheader) != 1 { + t.Errorf("GetAddrHeaderString: expected 1 address, got: %d", len(addrheader)) + } + if addrheader[0] == "" { + t.Fatalf("GetAddrHeaderString: expected address, got empty string") + } + if addrheader[0] != "" { + t.Errorf("GetAddrHeaderString: expected address not returned. Want: %s, got: %s", + "", addrheader[0]) + } + }) + } + }) + t.Run("GetAddrHeaderString with multiple valid address (to, cc, bcc)", func(t *testing.T) { + var fn func(...string) error + var addfn func(string) error + for _, tt := range addrHeaderTests { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + + switch tt.header { + case HeaderFrom: + continue + case HeaderTo: + fn = message.To + addfn = message.AddTo + case HeaderCc: + fn = message.Cc + addfn = message.AddCc + case HeaderBcc: + fn = message.Bcc + addfn = message.AddBcc + default: + t.Logf("header %s not supported", tt.header) + continue + } + t.Run(tt.name, func(t *testing.T) { + if err := fn("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set header: %s", err) + } + if err := addfn("tina.tester@example.com"); err != nil { + t.Fatalf("failed to set additional header value: %s", err) + } + addrheader := message.GetAddrHeaderString(tt.header) + if len(addrheader) != 2 { + t.Errorf("GetAddrHeaderString: expected 1 address, got: %d", len(addrheader)) + } + if addrheader[0] == "" { + t.Fatalf("GetAddrHeaderString: expected address, got empty string") + } + if addrheader[0] != "" { + t.Errorf("GetAddrHeaderString: expected address not returned. Want: %s, got: %s", + "", addrheader[0]) + } + if addrheader[1] == "" { + t.Fatalf("GetAddrHeaderString: expected address, got nil") + } + if addrheader[1] != "" { + t.Errorf("GetAddrHeaderString: expected address not returned. Want: %s, got: %s", + "", addrheader[1]) + } + }) + } + }) + t.Run("GetAddrHeaderString with no addresses", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + for _, tt := range addrHeaderTests { + t.Run(tt.name, func(t *testing.T) { + addrheader := message.GetAddrHeaderString(HeaderFrom) + if len(addrheader) != 0 { + t.Errorf("GetAddrHeaderString: expected 0 address, got: %d", len(addrheader)) + } + }) + } + }) +} + // checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. // It checks whether the AddrHeader contains the correct address, name, and number of fields. func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, From e808e0b972ef61d243282e70cc10e16dfe4399fd Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 26 Oct 2024 23:20:23 +0200 Subject: [PATCH 118/188] 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. --- msg_test.go | 696 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 426 insertions(+), 270 deletions(-) diff --git a/msg_test.go b/msg_test.go index bd39146..99cab22 100644 --- a/msg_test.go +++ b/msg_test.go @@ -2642,6 +2642,432 @@ func TestMsg_GetAddrHeaderString(t *testing.T) { }) } +func TestMsg_GetFrom(t *testing.T) { + t.Run("GetFrom with address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.From("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set from address: %s", err) + } + addresses := message.GetFrom() + if len(addresses) != 1 { + t.Fatalf("GetFrom: expected 1 address, got: %d", len(addresses)) + } + if addresses[0] == nil { + t.Fatalf("GetFrom: expected address, got nil") + } + if addresses[0].String() != "" { + t.Errorf("GetFrom: expected address not returned. Want: %s, got: %s", + "", addresses[0].String()) + } + }) + t.Run("GetFrom with no address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + addresses := message.GetFrom() + if len(addresses) != 0 { + t.Errorf("GetFrom: expected 0 address, got: %d", len(addresses)) + } + }) +} + +func TestMsg_GetFromString(t *testing.T) { + t.Run("GetFromString with address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.From("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set from address: %s", err) + } + addresses := message.GetFromString() + if len(addresses) != 1 { + t.Fatalf("GetFromString: expected 1 address, got: %d", len(addresses)) + } + if addresses[0] == "" { + t.Fatalf("GetFromString: expected address, got nil") + } + if addresses[0] != "" { + t.Errorf("GetFromString: expected address not returned. Want: %s, got: %s", + "", addresses[0]) + } + }) + t.Run("GetFromString with no address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + addresses := message.GetFromString() + if len(addresses) != 0 { + t.Errorf("GetFrom: expected 0 address, got: %d", len(addresses)) + } + }) +} + +func TestMsg_GetTo(t *testing.T) { + t.Run("GetTo with address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.To("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set from address: %s", err) + } + addresses := message.GetTo() + if len(addresses) != 1 { + t.Fatalf("GetTo: expected 1 address, got: %d", len(addresses)) + } + if addresses[0] == nil { + t.Fatalf("GetTo: expected address, got nil") + } + if addresses[0].String() != "" { + t.Errorf("GetTo: expected address not returned. Want: %s, got: %s", + "", addresses[0].String()) + } + }) + t.Run("GetTo with multiple address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.To("toni.tester@example.com", "tina.tester@example.com"); err != nil { + t.Fatalf("failed to set from address: %s", err) + } + addresses := message.GetTo() + if len(addresses) != 2 { + t.Fatalf("GetTo: expected 2 address, got: %d", len(addresses)) + } + if addresses[0] == nil { + t.Fatalf("GetTo: expected address, got nil") + } + if addresses[0].String() != "" { + t.Errorf("GetTo: expected address not returned. Want: %s, got: %s", + "", addresses[0].String()) + } + if addresses[1] == nil { + t.Fatalf("GetTo: expected address, got nil") + } + if addresses[1].String() != "" { + t.Errorf("GetTo: expected address not returned. Want: %s, got: %s", + "", addresses[1].String()) + } + }) + t.Run("GetTo with no address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + addresses := message.GetTo() + if len(addresses) != 0 { + t.Errorf("GetTo: expected 0 address, got: %d", len(addresses)) + } + }) +} + +func TestMsg_GetToString(t *testing.T) { + t.Run("GetToString with address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.To("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set from address: %s", err) + } + addresses := message.GetToString() + if len(addresses) != 1 { + t.Fatalf("GetToString: expected 1 address, got: %d", len(addresses)) + } + if addresses[0] == "" { + t.Fatalf("GetToString: expected address, got nil") + } + if addresses[0] != "" { + t.Errorf("GetToString: expected address not returned. Want: %s, got: %s", + "", addresses[0]) + } + }) + t.Run("GetToString with multiple address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.To("toni.tester@example.com", "tina.tester@example.com"); err != nil { + t.Fatalf("failed to set from address: %s", err) + } + addresses := message.GetToString() + if len(addresses) != 2 { + t.Fatalf("GetToString: expected 2 address, got: %d", len(addresses)) + } + if addresses[0] == "" { + t.Fatalf("GetToString: expected address, got nil") + } + if addresses[0] != "" { + t.Errorf("GetToString: expected address not returned. Want: %s, got: %s", + "", addresses[0]) + } + if addresses[1] == "" { + t.Fatalf("GetToString: expected address, got nil") + } + if addresses[1] != "" { + t.Errorf("GetToString: expected address not returned. Want: %s, got: %s", + "", addresses[1]) + } + }) + t.Run("GetToString with no address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + addresses := message.GetToString() + if len(addresses) != 0 { + t.Errorf("GetTo: expected 0 address, got: %d", len(addresses)) + } + }) +} + +func TestMsg_GetCc(t *testing.T) { + t.Run("GetCc with address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Cc("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set from address: %s", err) + } + addresses := message.GetCc() + if len(addresses) != 1 { + t.Fatalf("GetCc: expected 1 address, got: %d", len(addresses)) + } + if addresses[0] == nil { + t.Fatalf("GetCc: expected address, got nil") + } + if addresses[0].String() != "" { + t.Errorf("GetCc: expected address not returned. Want: %s, got: %s", + "", addresses[0].String()) + } + }) + t.Run("GetCc with multiple address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Cc("toni.tester@example.com", "tina.tester@example.com"); err != nil { + t.Fatalf("failed to set from address: %s", err) + } + addresses := message.GetCc() + if len(addresses) != 2 { + t.Fatalf("GetCc: expected 2 address, got: %d", len(addresses)) + } + if addresses[0] == nil { + t.Fatalf("GetCc: expected address, got nil") + } + if addresses[0].String() != "" { + t.Errorf("GetCc: expected address not returned. Want: %s, got: %s", + "", addresses[0].String()) + } + if addresses[1] == nil { + t.Fatalf("GetCc: expected address, got nil") + } + if addresses[1].String() != "" { + t.Errorf("GetCc: expected address not returned. Want: %s, got: %s", + "", addresses[1].String()) + } + }) + t.Run("GetCc with no address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + addresses := message.GetCc() + if len(addresses) != 0 { + t.Errorf("GetCc: expected 0 address, got: %d", len(addresses)) + } + }) +} + +func TestMsg_GetCcString(t *testing.T) { + t.Run("GetCcString with address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Cc("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set from address: %s", err) + } + addresses := message.GetCcString() + if len(addresses) != 1 { + t.Fatalf("GetCcString: expected 1 address, got: %d", len(addresses)) + } + if addresses[0] == "" { + t.Fatalf("GetCcString: expected address, got nil") + } + if addresses[0] != "" { + t.Errorf("GetCcString: expected address not returned. Want: %s, got: %s", + "", addresses[0]) + } + }) + t.Run("GetCcString with multiple address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Cc("toni.tester@example.com", "tina.tester@example.com"); err != nil { + t.Fatalf("failed to set from address: %s", err) + } + addresses := message.GetCcString() + if len(addresses) != 2 { + t.Fatalf("GetCcString: expected 2 address, got: %d", len(addresses)) + } + if addresses[0] == "" { + t.Fatalf("GetCcString: expected address, got nil") + } + if addresses[0] != "" { + t.Errorf("GetCcString: expected address not returned. Want: %s, got: %s", + "", addresses[0]) + } + if addresses[1] == "" { + t.Fatalf("GetCcString: expected address, got nil") + } + if addresses[1] != "" { + t.Errorf("GetCcString: expected address not returned. Want: %s, got: %s", + "", addresses[1]) + } + }) + t.Run("GetCcString with no address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + addresses := message.GetCcString() + if len(addresses) != 0 { + t.Errorf("GetTo: expected 0 address, got: %d", len(addresses)) + } + }) +} + +func TestMsg_GetBcc(t *testing.T) { + t.Run("GetBcc with address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Bcc("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set from address: %s", err) + } + addresses := message.GetBcc() + if len(addresses) != 1 { + t.Fatalf("GetBcc: expected 1 address, got: %d", len(addresses)) + } + if addresses[0] == nil { + t.Fatalf("GetBcc: expected address, got nil") + } + if addresses[0].String() != "" { + t.Errorf("GetBcc: expected address not returned. Want: %s, got: %s", + "", addresses[0].String()) + } + }) + t.Run("GetBcc with multiple address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Bcc("toni.tester@example.com", "tina.tester@example.com"); err != nil { + t.Fatalf("failed to set from address: %s", err) + } + addresses := message.GetBcc() + if len(addresses) != 2 { + t.Fatalf("GetBcc: expected 2 address, got: %d", len(addresses)) + } + if addresses[0] == nil { + t.Fatalf("GetBcc: expected address, got nil") + } + if addresses[0].String() != "" { + t.Errorf("GetBcc: expected address not returned. Want: %s, got: %s", + "", addresses[0].String()) + } + if addresses[1] == nil { + t.Fatalf("GetBcc: expected address, got nil") + } + if addresses[1].String() != "" { + t.Errorf("GetBcc: expected address not returned. Want: %s, got: %s", + "", addresses[1].String()) + } + }) + t.Run("GetBcc with no address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + addresses := message.GetBcc() + if len(addresses) != 0 { + t.Errorf("GetBcc: expected 0 address, got: %d", len(addresses)) + } + }) +} + +func TestMsg_GetBccString(t *testing.T) { + t.Run("GetBccString with address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Bcc("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set from address: %s", err) + } + addresses := message.GetBccString() + if len(addresses) != 1 { + t.Fatalf("GetBccString: expected 1 address, got: %d", len(addresses)) + } + if addresses[0] == "" { + t.Fatalf("GetBccString: expected address, got nil") + } + if addresses[0] != "" { + t.Errorf("GetBccString: expected address not returned. Want: %s, got: %s", + "", addresses[0]) + } + }) + t.Run("GetBccString with multiple address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.Bcc("toni.tester@example.com", "tina.tester@example.com"); err != nil { + t.Fatalf("failed to set from address: %s", err) + } + addresses := message.GetBccString() + if len(addresses) != 2 { + t.Fatalf("GetBccString: expected 2 address, got: %d", len(addresses)) + } + if addresses[0] == "" { + t.Fatalf("GetBccString: expected address, got nil") + } + if addresses[0] != "" { + t.Errorf("GetBccString: expected address not returned. Want: %s, got: %s", + "", addresses[0]) + } + if addresses[1] == "" { + t.Fatalf("GetBccString: expected address, got nil") + } + if addresses[1] != "" { + t.Errorf("GetBccString: expected address not returned. Want: %s, got: %s", + "", addresses[1]) + } + }) + t.Run("GetBccString with no address", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + addresses := message.GetBccString() + if len(addresses) != 0 { + t.Errorf("GetTo: expected 0 address, got: %d", len(addresses)) + } + }) +} + // checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. // It checks whether the AddrHeader contains the correct address, name, and number of fields. func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, @@ -2737,276 +3163,6 @@ func checkGenHeader(t *testing.T, message *Msg, header Header, fn string, field, } } - - - func TestMsg_GetRecipients(t *testing.T) { - a := []string{"to@example.com", "cc@example.com", "bcc@example.com"} - m := NewMsg() - - _, err := m.GetRecipients() - if err == nil { - t.Errorf("GetRecipients() succeeded but was expected to fail") - return - } - - if err := m.AddTo(a[0]); err != nil { - t.Errorf("AddTo() failed: %s", err) - return - } - if err := m.AddCc(a[1]); err != nil { - t.Errorf("AddCc() failed: %s", err) - return - } - if err := m.AddBcc(a[2]); err != nil { - t.Errorf("AddBcc() failed: %s", err) - return - } - - al, err := m.GetRecipients() - if err != nil { - t.Errorf("GetRecipients() failed: %s", err) - return - } - - tf, cf, bf := false, false, false - for _, r := range al { - if r == a[0] { - tf = true - } - if r == a[1] { - cf = true - } - if r == a[2] { - bf = true - } - } - if !tf { - t.Errorf("GetRecipients() failed. Expected to address %s but was not found", a[0]) - return - } - if !cf { - t.Errorf("GetRecipients() failed. Expected cc address %s but was not found", a[1]) - return - } - if !bf { - t.Errorf("GetRecipients() failed. Expected bcc address %s but was not found", a[2]) - return - } - } - -// TestMsg_SetImportance tests the Msg.SetImportance method - - func TestMsg_SetImportance(t *testing.T) { - tests := []struct { - name string - imp Importance - wantns string - xprio string - want string - sf bool - }{ - {"Importance: Non-Urgent", ImportanceNonUrgent, "0", "5", "non-urgent", false}, - {"Importance: Low", ImportanceLow, "0", "5", "low", false}, - {"Importance: Normal", ImportanceNormal, "", "", "", true}, - {"Importance: High", ImportanceHigh, "1", "1", "high", false}, - {"Importance: Urgent", ImportanceUrgent, "1", "1", "urgent", false}, - {"Importance: Unknown", 9, "", "", "", true}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m.SetImportance(tt.imp) - hi, ok := m.genHeader[HeaderImportance] - if (!ok || len(hi) <= 0) && !tt.sf { - t.Errorf("SetImportance() method failed. Generic header for Importance is empty") - } - hp, ok := m.genHeader[HeaderPriority] - if (!ok || len(hp) <= 0) && !tt.sf { - t.Errorf("SetImportance() method failed. Generic header for Priority is empty") - } - hx, ok := m.genHeader[HeaderXPriority] - if (!ok || len(hx) <= 0) && !tt.sf { - t.Errorf("SetImportance() method failed. Generic header for X-Priority is empty") - } - hm, ok := m.genHeader[HeaderXMSMailPriority] - if (!ok || len(hm) <= 0) && !tt.sf { - t.Errorf("SetImportance() method failed. Generic header for X-MS-XPriority is empty") - } - if !tt.sf { - if hi[0] != tt.want { - t.Errorf("SetImportance() method failed. Expected Imporance: %s, got: %s", tt.want, hi[0]) - } - if hp[0] != tt.wantns { - t.Errorf("SetImportance() method failed. Expected Priority: %s, got: %s", tt.want, hp[0]) - } - if hx[0] != tt.xprio { - t.Errorf("SetImportance() method failed. Expected X-Priority: %s, got: %s", tt.want, hx[0]) - } - if hm[0] != tt.wantns { - t.Errorf("SetImportance() method failed. Expected X-MS-Priority: %s, got: %s", tt.wantns, hm[0]) - } - } - m.genHeader = nil - m.genHeader = make(map[Header][]string) - }) - } - } - -// TestMsg_SetOrganization tests the Msg.SetOrganization method - - func TestMsg_SetOrganization(t *testing.T) { - tests := []struct { - name string - org string - }{ - {"Org: testcorp", "testcorp"}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m.SetOrganization(tt.org) - o, ok := m.genHeader[HeaderOrganization] - if !ok || len(o) <= 0 { - t.Errorf("SetOrganization() method failed. Generic header for Organization is empty") - return - } - if o[0] != tt.org { - t.Errorf("SetOrganization() method failed. Expected: %s, got: %s", tt.org, o[0]) - } - }) - } - } - -// TestMsg_SetUserAgent tests the Msg.SetUserAgent method - - func TestMsg_SetUserAgent(t *testing.T) { - tests := []struct { - name string - ua string - }{ - {"UA: Testmail 1.0", "Testmailer 1.0"}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m.SetUserAgent(tt.ua) - xm, ok := m.genHeader[HeaderXMailer] - if !ok || len(xm) <= 0 { - t.Errorf("SetUserAgent() method failed. Generic header for X-Mailer is empty") - return - } - ua, ok := m.genHeader[HeaderUserAgent] - if !ok || len(ua) <= 0 { - t.Errorf("SetUserAgent() method failed. Generic header for UserAgent is empty") - return - } - if xm[0] != tt.ua { - t.Errorf("SetUserAgent() method failed. Expected X-Mailer: %s, got: %s", tt.ua, xm[0]) - } - if ua[0] != tt.ua { - t.Errorf("SetUserAgent() method failed. Expected User-Agent: %s, got: %s", tt.ua, ua[0]) - } - }) - } - } - -// TestMsg_RequestMDN tests the different RequestMDN* related methods of Msg - - func TestMsg_RequestMDN(t *testing.T) { - n := "Toni Tester" - n2 := "Melanie Tester" - v := "toni.tester@example.com" - v2 := "melanie.tester@example.com" - iv := "testertest.tld" - vl := []string{v, v2} - m := NewMsg() - - // Single valid address - if err := m.RequestMDNTo(v); err != nil { - t.Errorf("RequestMDNTo with a single valid address failed: %s", err) - } - if val := m.genHeader[HeaderDispositionNotificationTo]; len(val) > 1 { - if val[0] != fmt.Sprintf("<%s>", v) { - t.Errorf("RequestMDNTo with a single valid address failed. Expected: %s, got: %s", v, - val[0]) - } - } - m.Reset() - - // Multiples valid addresses - if err := m.RequestMDNTo(vl...); err != nil { - t.Errorf("RequestMDNTo with a multiple valid address failed: %s", err) - } - if val := m.genHeader[HeaderDispositionNotificationTo]; len(val) > 0 { - if val[0] != fmt.Sprintf("<%s>", v) { - t.Errorf("RequestMDNTo with a multiple valid addresses failed. Expected 0: %s, got 0: %s", v, - val[0]) - } - } - if val := m.genHeader[HeaderDispositionNotificationTo]; len(val) > 1 { - if val[1] != fmt.Sprintf("<%s>", v2) { - t.Errorf("RequestMDNTo with a multiple valid addresses failed. Expected 1: %s, got 1: %s", v2, - val[1]) - } - } - m.Reset() - - // Invalid address - if err := m.RequestMDNTo(iv); err == nil { - t.Errorf("RequestMDNTo with an invalid address was supposed to failed, but didn't") - } - m.Reset() - - // Single valid addresses + AddTo - if err := m.RequestMDNTo(v); err != nil { - t.Errorf("RequestMDNTo with a single valid address failed: %s", err) - } - if err := m.RequestMDNAddTo(v2); err != nil { - t.Errorf("RequestMDNAddTo with a valid address failed: %s", err) - } - if val := m.genHeader[HeaderDispositionNotificationTo]; len(val) > 1 { - if val[1] != fmt.Sprintf("<%s>", v2) { - t.Errorf("RequestMDNTo with a multiple valid addresses failed. Expected 1: %s, got 1: %s", v2, - val[1]) - } - } - m.Reset() - - // Single valid address formated + AddToFromat - if err := m.RequestMDNToFormat(n, v); err != nil { - t.Errorf("RequestMDNToFormat with a single valid address failed: %s", err) - } - if val := m.genHeader[HeaderDispositionNotificationTo]; len(val) > 0 { - if val[0] != fmt.Sprintf(`"%s" <%s>`, n, v) { - t.Errorf(`RequestMDNToFormat with a single valid address failed. Expected: "%s" <%s>, got: %s`, n, v, - val[0]) - } - } - if err := m.RequestMDNAddToFormat(n2, v2); err != nil { - t.Errorf("RequestMDNAddToFormat with a valid address failed: %s", err) - } - if val := m.genHeader[HeaderDispositionNotificationTo]; len(val) > 1 { - if val[1] != fmt.Sprintf(`"%s" <%s>`, n2, v2) { - t.Errorf(`RequestMDNAddToFormat with a single valid address failed. Expected: "%s" <%s>, got: %s`, n2, v2, - val[1]) - } - } - m.Reset() - - // Invalid formated address - if err := m.RequestMDNToFormat(n, iv); err == nil { - t.Errorf("RequestMDNToFormat with an invalid address was supposed to failed, but didn't") - } - - // Invalid address AddTo + AddToFormat - if err := m.RequestMDNAddTo(iv); err == nil { - t.Errorf("RequestMDNAddTo with an invalid address was supposed to failed, but didn't") - } - if err := m.RequestMDNAddToFormat(n, iv); err == nil { - t.Errorf("RequestMDNAddToFormat with an invalid address was supposed to failed, but didn't") - } - } - // TestMsg_SetBodyString tests the Msg.SetBodyString method func TestMsg_SetBodyString(t *testing.T) { From 43f9ffa3af3446010e4095d24cfc0c45f9f23aef Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 09:58:22 +0100 Subject: [PATCH 119/188] 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. --- msg_test.go | 139 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/msg_test.go b/msg_test.go index 99cab22..061614e 100644 --- a/msg_test.go +++ b/msg_test.go @@ -5,6 +5,7 @@ package mail import ( + "bytes" "context" "errors" "fmt" @@ -3068,6 +3069,144 @@ func TestMsg_GetBccString(t *testing.T) { }) } +func TestMsg_GetGenHeader(t *testing.T) { + t.Run("GetGenHeader with single value", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + for _, tt := range genHeaderTests { + t.Run(tt.name, func(t *testing.T) { + message.SetGenHeader(tt.header, "test") + values := message.GetGenHeader(tt.header) + if len(values) != 1 { + t.Errorf("GetGenHeader: expected 1 value, got: %d", len(values)) + } + if values[0] != "test" { + t.Errorf("GetGenHeader: expected value not returned. Want: %s, got: %s", + "test", values[0]) + } + }) + } + }) + t.Run("GetGenHeader with multiple values", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + for _, tt := range genHeaderTests { + t.Run(tt.name, func(t *testing.T) { + message.SetGenHeader(tt.header, "test", "foobar") + values := message.GetGenHeader(tt.header) + if len(values) != 2 { + t.Errorf("GetGenHeader: expected 1 value, got: %d", len(values)) + } + if values[0] != "test" { + t.Errorf("GetGenHeader: expected value not returned. Want: %s, got: %s", + "test", values[0]) + } + if values[1] != "foobar" { + t.Errorf("GetGenHeader: expected value not returned. Want: %s, got: %s", + "foobar", values[1]) + } + }) + } + }) + t.Run("GetGenHeader with nil", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + for _, tt := range genHeaderTests { + t.Run(tt.name, func(t *testing.T) { + message.SetGenHeader(tt.header) + values := message.GetGenHeader(tt.header) + if len(values) != 0 { + t.Errorf("GetGenHeader: expected 1 value, got: %d", len(values)) + } + }) + } + }) +} + +func TestMsg_GetParts(t *testing.T) { + t.Run("GetParts with single part", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetBodyString(TypeTextPlain, "this is a test body") + parts := message.GetParts() + if len(parts) != 1 { + t.Fatalf("GetParts: expected 1 part, got: %d", len(parts)) + } + if parts[0] == nil { + t.Fatalf("GetParts: expected part, got nil") + } + if parts[0].contentType != TypeTextPlain { + t.Errorf("GetParts: expected contentType to be TypeTextPlain, got: %s", parts[0].contentType) + } + messageBuf := bytes.NewBuffer(nil) + _, err := parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("GetParts: writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "this is a test body") { + t.Errorf("GetParts: expected message body to be %s, got: %s", "this is a test body", + messageBuf.String()) + } + }) + t.Run("GetParts with multiple parts", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetBodyString(TypeTextPlain, "this is a test body") + message.AddAlternativeString(TypeTextHTML, "

This is HTML

") + parts := message.GetParts() + if len(parts) != 2 { + t.Fatalf("GetParts: expected 2 parts, got: %d", len(parts)) + } + if parts[0] == nil || parts[1] == nil { + t.Fatalf("GetParts: expected parts, got nil") + } + if parts[0].contentType != TypeTextPlain { + t.Errorf("GetParts: expected contentType to be TypeTextPlain, got: %s", parts[0].contentType) + } + if parts[1].contentType != TypeTextHTML { + t.Errorf("GetParts: expected contentType to be TypeTextHTML, got: %s", parts[1].contentType) + } + messageBuf := bytes.NewBuffer(nil) + _, err := parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("GetParts: writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "this is a test body") { + t.Errorf("GetParts: expected message body to be %s, got: %s", "this is a test body", + messageBuf.String()) + } + messageBuf.Reset() + _, err = parts[1].writeFunc(messageBuf) + if err != nil { + t.Errorf("GetParts: writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "

This is HTML

") { + t.Errorf("GetParts: expected message body to be %s, got: %s", "

This is HTML

", + messageBuf.String()) + } + }) + t.Run("GetParts with no parts", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + parts := message.GetParts() + if len(parts) != 0 { + t.Fatalf("GetParts: expected no parts, got: %d", len(parts)) + } + }) +} + // checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. // It checks whether the AddrHeader contains the correct address, name, and number of fields. func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, From babf7b9780448500a036cf52fc42a416b5480277 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 10:07:05 +0100 Subject: [PATCH 120/188] 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. --- msg_test.go | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/msg_test.go b/msg_test.go index 061614e..3f0d065 100644 --- a/msg_test.go +++ b/msg_test.go @@ -3207,6 +3207,77 @@ func TestMsg_GetParts(t *testing.T) { }) } +func TestMsg_GetAttachments(t *testing.T) { + t.Run("GetAttachments with single attachment", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.AttachFile("testdata/attachment.txt") + attachments := message.GetAttachments() + if len(attachments) != 1 { + t.Fatalf("GetAttachments: expected 1 attachment, got: %d", len(attachments)) + } + if attachments[0] == nil { + t.Fatalf("GetAttachments: expected attachment, got nil") + } + if attachments[0].Name != "attachment.txt" { + t.Errorf("GetAttachments: expected attachment name to be %s, got: %s", "attachment.txt", + attachments[0].Name) + } + messageBuf := bytes.NewBuffer(nil) + _, err := attachments[0].Writer(messageBuf) + if err != nil { + t.Errorf("GetAttachments: Writer func failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "This is a test attachment\n") { + t.Errorf("GetParts: expected message body to be %s, got: %s", "This is a test attachment\n", + messageBuf.String()) + } + }) + t.Run("GetAttachments with multiple attachments", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.AttachFile("testdata/attachment.txt") + message.AttachFile("testdata/attachment.txt", WithFileName("attachment2.txt")) + attachments := message.GetAttachments() + if len(attachments) != 2 { + t.Fatalf("GetAttachments: expected 2 attachment, got: %d", len(attachments)) + } + if attachments[0] == nil || attachments[1] == nil { + t.Fatalf("GetAttachments: expected attachment, got nil") + } + if attachments[0].Name != "attachment.txt" { + t.Errorf("GetAttachments: expected attachment name to be %s, got: %s", "attachment.txt", + attachments[0].Name) + } + if attachments[1].Name != "attachment2.txt" { + t.Errorf("GetAttachments: expected attachment name to be %s, got: %s", "attachment2.txt", + attachments[1].Name) + } + messageBuf := bytes.NewBuffer(nil) + _, err := attachments[0].Writer(messageBuf) + if err != nil { + t.Errorf("GetAttachments: Writer func failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "This is a test attachment\n") { + t.Errorf("GetParts: expected message body to be %s, got: %s", "This is a test attachment\n", + messageBuf.String()) + } + messageBuf.Reset() + _, err = attachments[1].Writer(messageBuf) + if err != nil { + t.Errorf("GetAttachments: Writer func failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "This is a test attachment\n") { + t.Errorf("GetParts: expected message body to be %s, got: %s", "This is a test attachment\n", + messageBuf.String()) + } + }) +} + // checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. // It checks whether the AddrHeader contains the correct address, name, and number of fields. func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, From 66e25d82d310b7bc176da3d5192e10bade112956 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 10:09:25 +0100 Subject: [PATCH 121/188] 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. --- msg_test.go | 57 ++++++++++------------------------------------------- 1 file changed, 10 insertions(+), 47 deletions(-) diff --git a/msg_test.go b/msg_test.go index 3f0d065..934c36e 100644 --- a/msg_test.go +++ b/msg_test.go @@ -3276,6 +3276,16 @@ func TestMsg_GetAttachments(t *testing.T) { messageBuf.String()) } }) + t.Run("GetAttachments with no attachment", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + attachments := message.GetAttachments() + if len(attachments) != 0 { + t.Fatalf("GetAttachments: expected 1 attachment, got: %d", len(attachments)) + } + }) } // checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. @@ -3488,53 +3498,6 @@ func checkGenHeader(t *testing.T, message *Msg, header Header, fn string, field, } } -// TestMsg_GetAttachments tests the Msg.GetAttachments method - - func TestMsg_GetAttachments(t *testing.T) { - tests := []struct { - name string - files []string - }{ - {"File: README.md", []string{"README.md"}}, - {"File: doc.go", []string{"doc.go"}}, - {"File: README.md and doc.go", []string{"README.md", "doc.go"}}, - {"File: nonexisting", nil}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - for _, f := range tt.files { - m.AttachFile(f, WithFileName(f), nil) - } - if len(m.attachments) != len(tt.files) { - t.Errorf("AttachFile() failed. Number of attachments expected: %d, got: %d", len(tt.files), - len(m.attachments)) - return - } - ff := m.GetAttachments() - if len(m.attachments) != len(ff) { - t.Errorf("GetAttachments() failed. Number of attachments expected: %d, got: %d", len(m.attachments), - len(ff)) - return - } - var fn []string - for _, f := range ff { - fn = append(fn, f.Name) - } - sort.Strings(fn) - sort.Strings(tt.files) - for i, f := range tt.files { - if f != fn[i] { - t.Errorf("GetAttachments() failed. Attachment name expected: %s, got: %s", f, - fn[i]) - return - } - } - m.Reset() - }) - } - } - // TestMsg_SetAttachments tests the Msg.GetAttachments method func TestMsg_SetAttachments(t *testing.T) { From b7ca41af815325c6b697c5a608385d078656a9b6 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 10:09:56 +0100 Subject: [PATCH 122/188] 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. --- msg_test.go | 48 ------------------------------------------------ 1 file changed, 48 deletions(-) diff --git a/msg_test.go b/msg_test.go index 934c36e..b09949d 100644 --- a/msg_test.go +++ b/msg_test.go @@ -3498,54 +3498,6 @@ func checkGenHeader(t *testing.T, message *Msg, header Header, fn string, field, } } -// TestMsg_SetAttachments tests the Msg.GetAttachments method - - func TestMsg_SetAttachments(t *testing.T) { - tests := []struct { - name string - attachments []string - files []string - }{ - {"File: replace README.md with doc.go", []string{"README.md"}, []string{"doc.go"}}, - {"File: add README.md with doc.go ", []string{"doc.go"}, []string{"README.md", "doc.go"}}, - {"File: remove README.md and doc.go", []string{"README.md", "doc.go"}, nil}, - {"File: add README.md and doc.go", nil, []string{"README.md", "doc.go"}}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - sort.Strings(tt.attachments) - sort.Strings(tt.files) - for _, a := range tt.attachments { - m.AttachFile(a, WithFileName(a), nil) - } - if len(m.attachments) != len(tt.attachments) { - t.Errorf("AttachFile() failed. Number of attachments expected: %d, got: %d", len(tt.files), - len(m.attachments)) - return - } - var files []*File - for _, f := range tt.files { - files = append(files, &File{Name: f}) - } - m.SetAttachments(files) - if len(m.attachments) != len(files) { - t.Errorf("SetAttachements() failed. Number of attachments expected: %d, got: %d", len(files), - len(m.attachments)) - return - } - for i, f := range tt.files { - if f != m.attachments[i].Name { - t.Errorf("SetAttachments() failed. Attachment name expected: %s, got: %s", f, - m.attachments[i].Name) - return - } - } - m.Reset() - }) - } - } - // TestMsg_UnsetAllAttachments tests the Msg.UnsetAllAttachments method func TestMsg_UnsetAllAttachments(t *testing.T) { From f2619737e876752269c13a3d68d10b89d017349b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 10:13:14 +0100 Subject: [PATCH 123/188] 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. --- msg_test.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/msg_test.go b/msg_test.go index b09949d..1d3089c 100644 --- a/msg_test.go +++ b/msg_test.go @@ -3288,6 +3288,27 @@ func TestMsg_GetAttachments(t *testing.T) { }) } +func TestMsg_GetBoundary(t *testing.T) { + t.Run("GetBoundary", func(t *testing.T) { + message := NewMsg(WithBoundary("test")) + if message == nil { + t.Fatal("message is nil") + } + if message.GetBoundary() != "test" { + t.Errorf("GetBoundary: expected %s, got: %s", "test", message.GetBoundary()) + } + }) + t.Run("GetBoundary with no boundary", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if message.GetBoundary() != "" { + t.Errorf("GetBoundary: expected empty, got: %s", message.GetBoundary()) + } + }) +} + // checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. // It checks whether the AddrHeader contains the correct address, name, and number of fields. func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, From 472a5a64540951b192e842368a06fe727d610335 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 10:25:53 +0100 Subject: [PATCH 124/188] 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. --- msg_test.go | 118 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/msg_test.go b/msg_test.go index 1d3089c..2cc1824 100644 --- a/msg_test.go +++ b/msg_test.go @@ -9,6 +9,7 @@ import ( "context" "errors" "fmt" + "io" "net" "reflect" "strings" @@ -3309,6 +3310,123 @@ func TestMsg_GetBoundary(t *testing.T) { }) } +func TestMsg_SetAttachments(t *testing.T) { + t.Run("SetAttachments with single file", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + file := &File{ + ContentType: TypeTextPlain, + Desc: "Test file", + Name: "attachment.txt", + Writer: func(w io.Writer) (int64, error) { + buf := bytes.NewBuffer([]byte("This is a test attachment\n")) + n, err := w.Write(buf.Bytes()) + return int64(n), err + }, + } + message.SetAttachments([]*File{file}) + attachments := message.GetAttachments() + if len(attachments) != 1 { + t.Fatalf("GetAttachments: expected 1 attachment, got: %d", len(attachments)) + } + if attachments[0] == nil { + t.Fatalf("GetAttachments: expected attachment, got nil") + } + if attachments[0].Name != "attachment.txt" { + t.Errorf("GetAttachments: expected attachment name to be %s, got: %s", "attachment.txt", + attachments[0].Name) + } + messageBuf := bytes.NewBuffer(nil) + _, err := attachments[0].Writer(messageBuf) + if err != nil { + t.Errorf("GetAttachments: Writer func failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "This is a test attachment\n") { + t.Errorf("GetParts: expected message body to be %s, got: %s", "This is a test attachment\n", + messageBuf.String()) + } + }) + t.Run("SetAttachments with multiple files", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + file1 := &File{ + ContentType: TypeTextPlain, + Desc: "Test file", + Name: "attachment.txt", + Writer: func(w io.Writer) (int64, error) { + buf := bytes.NewBuffer([]byte("This is a test attachment\n")) + n, err := w.Write(buf.Bytes()) + return int64(n), err + }, + } + file2 := &File{ + ContentType: TypeTextPlain, + Desc: "Test file no. 2", + Name: "attachment2.txt", + Writer: func(w io.Writer) (int64, error) { + buf := bytes.NewBuffer([]byte("This is also a test attachment\n")) + n, err := w.Write(buf.Bytes()) + return int64(n), err + }, + } + message.SetAttachments([]*File{file1, file2}) + attachments := message.GetAttachments() + if len(attachments) != 2 { + t.Fatalf("GetAttachments: expected 2 attachment, got: %d", len(attachments)) + } + if attachments[0] == nil || attachments[1] == nil { + t.Fatalf("GetAttachments: expected attachment, got nil") + } + if attachments[0].Name != "attachment.txt" { + t.Errorf("GetAttachments: expected attachment name to be %s, got: %s", "attachment.txt", + attachments[0].Name) + } + if attachments[1].Name != "attachment2.txt" { + t.Errorf("GetAttachments: expected attachment name to be %s, got: %s", "attachment2.txt", + attachments[1].Name) + } + messageBuf := bytes.NewBuffer(nil) + _, err := attachments[0].Writer(messageBuf) + if err != nil { + t.Errorf("GetAttachments: Writer func failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "This is a test attachment\n") { + t.Errorf("GetParts: expected message body to be %s, got: %s", "This is a test attachment\n", + messageBuf.String()) + } + messageBuf.Reset() + _, err = attachments[1].Writer(messageBuf) + if err != nil { + t.Errorf("GetAttachments: Writer func failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "This is also a test attachment\n") { + t.Errorf("GetParts: expected message body to be %s, got: %s", "This is also a test attachment\n", + messageBuf.String()) + } + }) + t.Run("SetAttachments with no file", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetAttachments(nil) + attachments := message.GetAttachments() + if len(attachments) != 0 { + t.Fatalf("GetAttachments: expected 0 attachment, got: %d", len(attachments)) + } + }) +} + +func TestMsg_SetAttachements(t *testing.T) { + message := NewMsg() + message.SetAttachements(nil) + t.Skip("SetAttachements is deprecated and fully tested by SetAttachments already") +} + // checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. // It checks whether the AddrHeader contains the correct address, name, and number of fields. func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, From c8c7d18ba913fc088e5bb8f687f6aba3f768ef8d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 10:30:20 +0100 Subject: [PATCH 125/188] 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. --- testdata/attachment.txt | 1 + testdata/attachment.txt.license | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 testdata/attachment.txt create mode 100644 testdata/attachment.txt.license diff --git a/testdata/attachment.txt b/testdata/attachment.txt new file mode 100644 index 0000000..fc21731 --- /dev/null +++ b/testdata/attachment.txt @@ -0,0 +1 @@ +This is a test attachment diff --git a/testdata/attachment.txt.license b/testdata/attachment.txt.license new file mode 100644 index 0000000..615ea2b --- /dev/null +++ b/testdata/attachment.txt.license @@ -0,0 +1,3 @@ +// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors +// +// SPDX-License-Identifier: MIT From 2dad9d36b2390a690f217af4296ac3462f1d814b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 10:42:32 +0100 Subject: [PATCH 126/188] 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. --- msg_test.go | 117 +++++++++++++++++++++++++++++++++++++ testdata/embed.txt | 1 + testdata/embed.txt.license | 3 + 3 files changed, 121 insertions(+) create mode 100644 testdata/embed.txt create mode 100644 testdata/embed.txt.license diff --git a/msg_test.go b/msg_test.go index 2cc1824..559c477 100644 --- a/msg_test.go +++ b/msg_test.go @@ -3427,6 +3427,123 @@ func TestMsg_SetAttachements(t *testing.T) { t.Skip("SetAttachements is deprecated and fully tested by SetAttachments already") } +func TestMsg_UnsetAllAttachments(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + file1 := &File{ + ContentType: TypeTextPlain, + Desc: "Test file", + Name: "attachment.txt", + Writer: func(w io.Writer) (int64, error) { + buf := bytes.NewBuffer([]byte("This is a test attachment\n")) + n, err := w.Write(buf.Bytes()) + return int64(n), err + }, + } + file2 := &File{ + ContentType: TypeTextPlain, + Desc: "Test file no. 2", + Name: "attachment2.txt", + Writer: func(w io.Writer) (int64, error) { + buf := bytes.NewBuffer([]byte("This is also a test attachment\n")) + n, err := w.Write(buf.Bytes()) + return int64(n), err + }, + } + message.SetAttachments([]*File{file1, file2}) + message.UnsetAllAttachments() + if message.attachments != nil { + t.Errorf("UnsetAllAttachments: expected attachments to be nil, got: %v", message.attachments) + } + attachments := message.GetAttachments() + if len(attachments) != 0 { + t.Fatalf("GetAttachments: expected 0 attachment, got: %d", len(attachments)) + } +} + +func TestMsg_GetEmbeds(t *testing.T) { + t.Run("GetEmbeds with single embed", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.EmbedFile("testdata/embed.txt") + embeds := message.GetEmbeds() + if len(embeds) != 1 { + t.Fatalf("GetEmbeds: expected 1 embed, got: %d", len(embeds)) + } + if embeds[0] == nil { + t.Fatalf("GetEmbeds: expected embed, got nil") + } + if embeds[0].Name != "embed.txt" { + t.Errorf("GetEmbeds: expected embed name to be %s, got: %s", "embed.txt", + embeds[0].Name) + } + messageBuf := bytes.NewBuffer(nil) + _, err := embeds[0].Writer(messageBuf) + if err != nil { + t.Errorf("GetEmbeds: Writer func failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "This is a test embed\n") { + t.Errorf("GetParts: expected message body to be %s, got: %s", "This is a test embed\n", + messageBuf.String()) + } + }) + t.Run("GetEmbeds with multiple embeds", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.EmbedFile("testdata/embed.txt") + message.EmbedFile("testdata/embed.txt", WithFileName("embed2.txt")) + embeds := message.GetEmbeds() + if len(embeds) != 2 { + t.Fatalf("GetEmbeds: expected 2 embed, got: %d", len(embeds)) + } + if embeds[0] == nil || embeds[1] == nil { + t.Fatalf("GetEmbeds: expected embed, got nil") + } + if embeds[0].Name != "embed.txt" { + t.Errorf("GetEmbeds: expected embed name to be %s, got: %s", "embed.txt", + embeds[0].Name) + } + if embeds[1].Name != "embed2.txt" { + t.Errorf("GetEmbeds: expected embed name to be %s, got: %s", "embed2.txt", + embeds[1].Name) + } + messageBuf := bytes.NewBuffer(nil) + _, err := embeds[0].Writer(messageBuf) + if err != nil { + t.Errorf("GetEmbeds: Writer func failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "This is a test embed\n") { + t.Errorf("GetParts: expected message body to be %s, got: %s", "This is a test embed\n", + messageBuf.String()) + } + messageBuf.Reset() + _, err = embeds[1].Writer(messageBuf) + if err != nil { + t.Errorf("GetEmbeds: Writer func failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "This is a test embed\n") { + t.Errorf("GetParts: expected message body to be %s, got: %s", "This is a test embed\n", + messageBuf.String()) + } + }) + t.Run("GetEmbeds with no embeds", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + embeds := message.GetEmbeds() + if len(embeds) != 0 { + t.Fatalf("GetEmbeds: expected 1 embeds, got: %d", len(embeds)) + } + }) +} + // checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. // It checks whether the AddrHeader contains the correct address, name, and number of fields. func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, diff --git a/testdata/embed.txt b/testdata/embed.txt new file mode 100644 index 0000000..9a41257 --- /dev/null +++ b/testdata/embed.txt @@ -0,0 +1 @@ +This is a test embed diff --git a/testdata/embed.txt.license b/testdata/embed.txt.license new file mode 100644 index 0000000..615ea2b --- /dev/null +++ b/testdata/embed.txt.license @@ -0,0 +1,3 @@ +// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors +// +// SPDX-License-Identifier: MIT From 9a01629c4711ab3be347821757b70ce8fd3420f2 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 10:49:02 +0100 Subject: [PATCH 127/188] 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. --- msg_test.go | 188 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) diff --git a/msg_test.go b/msg_test.go index 559c477..9e53e5d 100644 --- a/msg_test.go +++ b/msg_test.go @@ -3544,6 +3544,194 @@ func TestMsg_GetEmbeds(t *testing.T) { }) } +func TestMsg_SetEmbeds(t *testing.T) { + t.Run("SetEmbeds with single file", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + file := &File{ + ContentType: TypeTextPlain, + Desc: "Test file", + Name: "embed.txt", + Writer: func(w io.Writer) (int64, error) { + buf := bytes.NewBuffer([]byte("This is a test embed\n")) + n, err := w.Write(buf.Bytes()) + return int64(n), err + }, + } + message.SetEmbeds([]*File{file}) + embeds := message.GetEmbeds() + if len(embeds) != 1 { + t.Fatalf("GetEmbeds: expected 1 embed, got: %d", len(embeds)) + } + if embeds[0] == nil { + t.Fatalf("GetEmbeds: expected embed, got nil") + } + if embeds[0].Name != "embed.txt" { + t.Errorf("GetEmbeds: expected embed name to be %s, got: %s", "embed.txt", + embeds[0].Name) + } + messageBuf := bytes.NewBuffer(nil) + _, err := embeds[0].Writer(messageBuf) + if err != nil { + t.Errorf("GetEmbeds: Writer func failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "This is a test embed\n") { + t.Errorf("GetParts: expected message body to be %s, got: %s", "This is a test embed\n", + messageBuf.String()) + } + }) + t.Run("SetEmbeds with multiple files", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + file1 := &File{ + ContentType: TypeTextPlain, + Desc: "Test file", + Name: "embed.txt", + Writer: func(w io.Writer) (int64, error) { + buf := bytes.NewBuffer([]byte("This is a test embed\n")) + n, err := w.Write(buf.Bytes()) + return int64(n), err + }, + } + file2 := &File{ + ContentType: TypeTextPlain, + Desc: "Test file no. 2", + Name: "embed2.txt", + Writer: func(w io.Writer) (int64, error) { + buf := bytes.NewBuffer([]byte("This is also a test embed\n")) + n, err := w.Write(buf.Bytes()) + return int64(n), err + }, + } + message.SetEmbeds([]*File{file1, file2}) + embeds := message.GetEmbeds() + if len(embeds) != 2 { + t.Fatalf("GetEmbeds: expected 2 embed, got: %d", len(embeds)) + } + if embeds[0] == nil || embeds[1] == nil { + t.Fatalf("GetEmbeds: expected embed, got nil") + } + if embeds[0].Name != "embed.txt" { + t.Errorf("GetEmbeds: expected embed name to be %s, got: %s", "embed.txt", + embeds[0].Name) + } + if embeds[1].Name != "embed2.txt" { + t.Errorf("GetEmbeds: expected embed name to be %s, got: %s", "embed2.txt", + embeds[1].Name) + } + messageBuf := bytes.NewBuffer(nil) + _, err := embeds[0].Writer(messageBuf) + if err != nil { + t.Errorf("GetEmbeds: Writer func failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "This is a test embed\n") { + t.Errorf("GetParts: expected message body to be %s, got: %s", "This is a test embed\n", + messageBuf.String()) + } + messageBuf.Reset() + _, err = embeds[1].Writer(messageBuf) + if err != nil { + t.Errorf("GetEmbeds: Writer func failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "This is also a test embed\n") { + t.Errorf("GetParts: expected message body to be %s, got: %s", "This is also a test embed\n", + messageBuf.String()) + } + }) + t.Run("SetEmbeds with no file", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetEmbeds(nil) + embeds := message.GetEmbeds() + if len(embeds) != 0 { + t.Fatalf("GetEmbeds: expected 0 embed, got: %d", len(embeds)) + } + }) +} + +func TestMsg_UnsetAllEmbeds(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + file1 := &File{ + ContentType: TypeTextPlain, + Desc: "Test file", + Name: "embed.txt", + Writer: func(w io.Writer) (int64, error) { + buf := bytes.NewBuffer([]byte("This is a test embed\n")) + n, err := w.Write(buf.Bytes()) + return int64(n), err + }, + } + file2 := &File{ + ContentType: TypeTextPlain, + Desc: "Test file no. 2", + Name: "embed2.txt", + Writer: func(w io.Writer) (int64, error) { + buf := bytes.NewBuffer([]byte("This is also a test embed\n")) + n, err := w.Write(buf.Bytes()) + return int64(n), err + }, + } + message.SetEmbeds([]*File{file1, file2}) + message.UnsetAllEmbeds() + if message.embeds != nil { + t.Errorf("UnsetAllEmbeds: expected embeds to be nil, got: %v", message.embeds) + } + embeds := message.GetEmbeds() + if len(embeds) != 0 { + t.Fatalf("GetEmbeds: expected 0 embed, got: %d", len(embeds)) + } +} + +func TestMsg_UnsetAllParts(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + file1 := &File{ + ContentType: TypeTextPlain, + Desc: "Test file", + Name: "embed.txt", + Writer: func(w io.Writer) (int64, error) { + buf := bytes.NewBuffer([]byte("This is a test embed\n")) + n, err := w.Write(buf.Bytes()) + return int64(n), err + }, + } + file2 := &File{ + ContentType: TypeTextPlain, + Desc: "Test file no. 2", + Name: "embed2.txt", + Writer: func(w io.Writer) (int64, error) { + buf := bytes.NewBuffer([]byte("This is also a test embed\n")) + n, err := w.Write(buf.Bytes()) + return int64(n), err + }, + } + message.SetAttachments([]*File{file1}) + message.SetEmbeds([]*File{file2}) + message.UnsetAllParts() + if message.embeds != nil || message.attachments != nil { + t.Error("UnsetAllParts: expected attachments/embeds to be nil, got: value") + } + embeds := message.GetEmbeds() + if len(embeds) != 0 { + t.Fatalf("GetEmbeds: expected 0 embed, got: %d", len(embeds)) + } + attachments := message.GetAttachments() + if len(attachments) != 0 { + t.Fatalf("GetAttachments: expected 0 attachments, got: %d", len(attachments)) + } +} + // checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. // It checks whether the AddrHeader contains the correct address, name, and number of fields. func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, From 6376f2919074a3834f49c606f1ed47fbf6561ce9 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 11:27:38 +0100 Subject: [PATCH 128/188] 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. --- msg_test.go | 503 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 308 insertions(+), 195 deletions(-) diff --git a/msg_test.go b/msg_test.go index 9e53e5d..af05cf8 100644 --- a/msg_test.go +++ b/msg_test.go @@ -89,6 +89,14 @@ var ( {"1.0", MIME10, "1.0"}, {"1.1 (not a valid version at this time)", MIMEVersion("1.1"), "1.1"}, } + contentTypeTests = []struct { + name string + ctype ContentType + }{ + {"text/plain", TypeTextPlain}, + {"text/html", TypeTextHTML}, + {"application/octet-stream", TypeAppOctetStream}, + } // Inspired by https://www.youtube.com/watch?v=xxX81WmXjPg&t=623s, yet, some assumptions in that video are // incorrect for RFC5321/RFC5322 but rely on deprecated information from RFC822. The tests have been // adjusted accordingly. @@ -2090,7 +2098,7 @@ func TestMsg_GetSender(t *testing.T) { t.Errorf("failed to get sender: %s", err) } if !strings.EqualFold(sender, "toni.tester@example.com") { - t.Errorf("GetSender: expected sender not returned. Want: %s, got: %s", "toni.tester@example.com", sender) + t.Errorf("expected sender not returned. Want: %s, got: %s", "toni.tester@example.com", sender) } }) t.Run("GetSender with envelope from only (full address)", func(t *testing.T) { @@ -2106,7 +2114,7 @@ func TestMsg_GetSender(t *testing.T) { t.Errorf("failed to get sender: %s", err) } if !strings.EqualFold(sender, "") { - t.Errorf("GetSender: expected sender not returned. Want: %s, got: %s", "", sender) + t.Errorf("expected sender not returned. Want: %s, got: %s", "", sender) } }) t.Run("GetSender with from only (no full address)", func(t *testing.T) { @@ -2122,7 +2130,7 @@ func TestMsg_GetSender(t *testing.T) { t.Errorf("failed to get sender: %s", err) } if !strings.EqualFold(sender, "toni.tester@example.com") { - t.Errorf("GetSender: expected sender not returned. Want: %s, got: %s", "toni.tester@example.com", sender) + t.Errorf("expected sender not returned. Want: %s, got: %s", "toni.tester@example.com", sender) } }) t.Run("GetSender with from only (full address)", func(t *testing.T) { @@ -2138,7 +2146,7 @@ func TestMsg_GetSender(t *testing.T) { t.Errorf("failed to get sender: %s", err) } if !strings.EqualFold(sender, "") { - t.Errorf("GetSender: expected sender not returned. Want: %s, got: %s", "", sender) + t.Errorf("expected sender not returned. Want: %s, got: %s", "", sender) } }) t.Run("GetSender with envelope from and from (no full address)", func(t *testing.T) { @@ -2157,7 +2165,7 @@ func TestMsg_GetSender(t *testing.T) { t.Errorf("failed to get sender: %s", err) } if !strings.EqualFold(sender, "toni.tester@example.com") { - t.Errorf("GetSender: expected sender not returned. Want: %s, got: %s", "toni.tester@example.com", sender) + t.Errorf("expected sender not returned. Want: %s, got: %s", "toni.tester@example.com", sender) } }) t.Run("GetSender with envelope from and from (full address)", func(t *testing.T) { @@ -2176,7 +2184,7 @@ func TestMsg_GetSender(t *testing.T) { t.Errorf("failed to get sender: %s", err) } if !strings.EqualFold(sender, "") { - t.Errorf("GetSender: expected sender not returned. Want: %s, got: %s", "", sender) + t.Errorf("expected sender not returned. Want: %s, got: %s", "", sender) } }) t.Run("GetSender with no envelope from or from", func(t *testing.T) { @@ -2209,10 +2217,10 @@ func TestMsg_GetRecipients(t *testing.T) { t.Errorf("failed to get recipients: %s", err) } if len(rcpts) != 1 { - t.Fatalf("GetRecipients: expected 1 recipient, got: %d", len(rcpts)) + t.Fatalf("expected 1 recipient, got: %d", len(rcpts)) } if !strings.EqualFold(rcpts[0], "toni.tester@example.com") { - t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + t.Errorf("expected recipient not returned. Want: %s, got: %s", "toni.tester@example.com", rcpts[0]) } }) @@ -2229,10 +2237,10 @@ func TestMsg_GetRecipients(t *testing.T) { t.Errorf("failed to get recipients: %s", err) } if len(rcpts) != 1 { - t.Fatalf("GetRecipients: expected 1 recipient, got: %d", len(rcpts)) + t.Fatalf("expected 1 recipient, got: %d", len(rcpts)) } if !strings.EqualFold(rcpts[0], "toni.tester@example.com") { - t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + t.Errorf("expected recipient not returned. Want: %s, got: %s", "toni.tester@example.com", rcpts[0]) } }) @@ -2249,10 +2257,10 @@ func TestMsg_GetRecipients(t *testing.T) { t.Errorf("failed to get recipients: %s", err) } if len(rcpts) != 1 { - t.Fatalf("GetRecipients: expected 1 recipient, got: %d", len(rcpts)) + t.Fatalf("expected 1 recipient, got: %d", len(rcpts)) } if !strings.EqualFold(rcpts[0], "toni.tester@example.com") { - t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + t.Errorf("expected recipient not returned. Want: %s, got: %s", "toni.tester@example.com", rcpts[0]) } }) @@ -2272,14 +2280,14 @@ func TestMsg_GetRecipients(t *testing.T) { t.Errorf("failed to get recipients: %s", err) } if len(rcpts) != 2 { - t.Fatalf("GetRecipients: expected 2 recipient, got: %d", len(rcpts)) + t.Fatalf("expected 2 recipient, got: %d", len(rcpts)) } if !strings.EqualFold(rcpts[0], "toni.tester@example.com") { - t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + t.Errorf("expected recipient not returned. Want: %s, got: %s", "toni.tester@example.com", rcpts[0]) } if !strings.EqualFold(rcpts[1], "tina.tester@example.com") { - t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + t.Errorf("expected recipient not returned. Want: %s, got: %s", "tina.tester@example.com", rcpts[1]) } }) @@ -2299,14 +2307,14 @@ func TestMsg_GetRecipients(t *testing.T) { t.Errorf("failed to get recipients: %s", err) } if len(rcpts) != 2 { - t.Fatalf("GetRecipients: expected 2 recipient, got: %d", len(rcpts)) + t.Fatalf("expected 2 recipient, got: %d", len(rcpts)) } if !strings.EqualFold(rcpts[0], "toni.tester@example.com") { - t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + t.Errorf("expected recipient not returned. Want: %s, got: %s", "toni.tester@example.com", rcpts[0]) } if !strings.EqualFold(rcpts[1], "tina.tester@example.com") { - t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + t.Errorf("expected recipient not returned. Want: %s, got: %s", "tina.tester@example.com", rcpts[1]) } }) @@ -2326,14 +2334,14 @@ func TestMsg_GetRecipients(t *testing.T) { t.Errorf("failed to get recipients: %s", err) } if len(rcpts) != 2 { - t.Fatalf("GetRecipients: expected 2 recipient, got: %d", len(rcpts)) + t.Fatalf("expected 2 recipient, got: %d", len(rcpts)) } if !strings.EqualFold(rcpts[0], "toni.tester@example.com") { - t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + t.Errorf("expected recipient not returned. Want: %s, got: %s", "toni.tester@example.com", rcpts[0]) } if !strings.EqualFold(rcpts[1], "tina.tester@example.com") { - t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + t.Errorf("expected recipient not returned. Want: %s, got: %s", "tina.tester@example.com", rcpts[1]) } }) @@ -2356,18 +2364,18 @@ func TestMsg_GetRecipients(t *testing.T) { t.Errorf("failed to get recipients: %s", err) } if len(rcpts) != 3 { - t.Fatalf("GetRecipients: expected 3 recipient, got: %d", len(rcpts)) + t.Fatalf("expected 3 recipient, got: %d", len(rcpts)) } if !strings.EqualFold(rcpts[0], "toni.tester@example.com") { - t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + t.Errorf("expected recipient not returned. Want: %s, got: %s", "toni.tester@example.com", rcpts[0]) } if !strings.EqualFold(rcpts[1], "tina.tester@example.com") { - t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + t.Errorf("expected recipient not returned. Want: %s, got: %s", "tina.tester@example.com", rcpts[1]) } if !strings.EqualFold(rcpts[2], "tom.tester@example.com") { - t.Errorf("GetRecipients: expected recipient not returned. Want: %s, got: %s", + t.Errorf("expected recipient not returned. Want: %s, got: %s", "tina.tester@example.com", rcpts[2]) } }) @@ -2378,10 +2386,10 @@ func TestMsg_GetRecipients(t *testing.T) { } _, err := message.GetRecipients() if err == nil { - t.Errorf("GetRecipients: expected error, got nil") + t.Errorf("expected error, got nil") } if !errors.Is(err, ErrNoRcptAddresses) { - t.Errorf("GetRecipients: expected ErrNoRcptAddresses, got: %s", err) + t.Errorf("expected ErrNoRcptAddresses, got: %s", err) } }) } @@ -2397,13 +2405,13 @@ func TestMsg_GetAddrHeader(t *testing.T) { } addrheader := message.GetAddrHeader(HeaderFrom) if len(addrheader) != 1 { - t.Errorf("GetAddrHeader: expected 1 address, got: %d", len(addrheader)) + t.Errorf("expected 1 address, got: %d", len(addrheader)) } if addrheader[0] == nil { - t.Fatalf("GetAddrHeader: expected address, got nil") + t.Fatalf("expected address, got nil") } if addrheader[0].String() != "" { - t.Errorf("GetAddrHeader: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addrheader[0].String()) } }) @@ -2434,13 +2442,13 @@ func TestMsg_GetAddrHeader(t *testing.T) { } addrheader := message.GetAddrHeader(tt.header) if len(addrheader) != 1 { - t.Errorf("GetAddrHeader: expected 1 address, got: %d", len(addrheader)) + t.Errorf("expected 1 address, got: %d", len(addrheader)) } if addrheader[0] == nil { - t.Fatalf("GetAddrHeader: expected address, got nil") + t.Fatalf("expected address, got nil") } if addrheader[0].String() != "" { - t.Errorf("GetAddrHeader: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addrheader[0].String()) } }) @@ -2480,20 +2488,20 @@ func TestMsg_GetAddrHeader(t *testing.T) { } addrheader := message.GetAddrHeader(tt.header) if len(addrheader) != 2 { - t.Errorf("GetAddrHeader: expected 1 address, got: %d", len(addrheader)) + t.Errorf("expected 1 address, got: %d", len(addrheader)) } if addrheader[0] == nil { - t.Fatalf("GetAddrHeader: expected address, got nil") + t.Fatalf("expected address, got nil") } if addrheader[0].String() != "" { - t.Errorf("GetAddrHeader: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addrheader[0].String()) } if addrheader[1] == nil { - t.Fatalf("GetAddrHeader: expected address, got nil") + t.Fatalf("expected address, got nil") } if addrheader[1].String() != "" { - t.Errorf("GetAddrHeader: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addrheader[1].String()) } }) @@ -2508,7 +2516,7 @@ func TestMsg_GetAddrHeader(t *testing.T) { t.Run(tt.name, func(t *testing.T) { addrheader := message.GetAddrHeader(HeaderFrom) if len(addrheader) != 0 { - t.Errorf("GetAddrHeader: expected 0 address, got: %d", len(addrheader)) + t.Errorf("expected 0 address, got: %d", len(addrheader)) } }) } @@ -2526,13 +2534,13 @@ func TestMsg_GetAddrHeaderString(t *testing.T) { } addrheader := message.GetAddrHeaderString(HeaderFrom) if len(addrheader) != 1 { - t.Errorf("GetAddrHeaderString: expected 1 address, got: %d", len(addrheader)) + t.Errorf("expected 1 address, got: %d", len(addrheader)) } if addrheader[0] == "" { - t.Fatalf("GetAddrHeaderString: expected address, got empty string") + t.Fatalf("expected address, got empty string") } if addrheader[0] != "" { - t.Errorf("GetAddrHeaderString: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addrheader[0]) } }) @@ -2563,13 +2571,13 @@ func TestMsg_GetAddrHeaderString(t *testing.T) { } addrheader := message.GetAddrHeaderString(tt.header) if len(addrheader) != 1 { - t.Errorf("GetAddrHeaderString: expected 1 address, got: %d", len(addrheader)) + t.Errorf("expected 1 address, got: %d", len(addrheader)) } if addrheader[0] == "" { - t.Fatalf("GetAddrHeaderString: expected address, got empty string") + t.Fatalf("expected address, got empty string") } if addrheader[0] != "" { - t.Errorf("GetAddrHeaderString: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addrheader[0]) } }) @@ -2609,20 +2617,20 @@ func TestMsg_GetAddrHeaderString(t *testing.T) { } addrheader := message.GetAddrHeaderString(tt.header) if len(addrheader) != 2 { - t.Errorf("GetAddrHeaderString: expected 1 address, got: %d", len(addrheader)) + t.Errorf("expected 1 address, got: %d", len(addrheader)) } if addrheader[0] == "" { - t.Fatalf("GetAddrHeaderString: expected address, got empty string") + t.Fatalf("expected address, got empty string") } if addrheader[0] != "" { - t.Errorf("GetAddrHeaderString: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addrheader[0]) } if addrheader[1] == "" { - t.Fatalf("GetAddrHeaderString: expected address, got nil") + t.Fatalf("expected address, got nil") } if addrheader[1] != "" { - t.Errorf("GetAddrHeaderString: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addrheader[1]) } }) @@ -2637,7 +2645,7 @@ func TestMsg_GetAddrHeaderString(t *testing.T) { t.Run(tt.name, func(t *testing.T) { addrheader := message.GetAddrHeaderString(HeaderFrom) if len(addrheader) != 0 { - t.Errorf("GetAddrHeaderString: expected 0 address, got: %d", len(addrheader)) + t.Errorf("expected 0 address, got: %d", len(addrheader)) } }) } @@ -2655,13 +2663,13 @@ func TestMsg_GetFrom(t *testing.T) { } addresses := message.GetFrom() if len(addresses) != 1 { - t.Fatalf("GetFrom: expected 1 address, got: %d", len(addresses)) + t.Fatalf("expected 1 address, got: %d", len(addresses)) } if addresses[0] == nil { - t.Fatalf("GetFrom: expected address, got nil") + t.Fatalf("expected address, got nil") } if addresses[0].String() != "" { - t.Errorf("GetFrom: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addresses[0].String()) } }) @@ -2672,7 +2680,7 @@ func TestMsg_GetFrom(t *testing.T) { } addresses := message.GetFrom() if len(addresses) != 0 { - t.Errorf("GetFrom: expected 0 address, got: %d", len(addresses)) + t.Errorf("expected 0 address, got: %d", len(addresses)) } }) } @@ -2688,13 +2696,13 @@ func TestMsg_GetFromString(t *testing.T) { } addresses := message.GetFromString() if len(addresses) != 1 { - t.Fatalf("GetFromString: expected 1 address, got: %d", len(addresses)) + t.Fatalf("expected 1 address, got: %d", len(addresses)) } if addresses[0] == "" { - t.Fatalf("GetFromString: expected address, got nil") + t.Fatalf("expected address, got nil") } if addresses[0] != "" { - t.Errorf("GetFromString: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addresses[0]) } }) @@ -2705,7 +2713,7 @@ func TestMsg_GetFromString(t *testing.T) { } addresses := message.GetFromString() if len(addresses) != 0 { - t.Errorf("GetFrom: expected 0 address, got: %d", len(addresses)) + t.Errorf("expected 0 address, got: %d", len(addresses)) } }) } @@ -2721,13 +2729,13 @@ func TestMsg_GetTo(t *testing.T) { } addresses := message.GetTo() if len(addresses) != 1 { - t.Fatalf("GetTo: expected 1 address, got: %d", len(addresses)) + t.Fatalf("expected 1 address, got: %d", len(addresses)) } if addresses[0] == nil { - t.Fatalf("GetTo: expected address, got nil") + t.Fatalf("expected address, got nil") } if addresses[0].String() != "" { - t.Errorf("GetTo: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addresses[0].String()) } }) @@ -2741,20 +2749,20 @@ func TestMsg_GetTo(t *testing.T) { } addresses := message.GetTo() if len(addresses) != 2 { - t.Fatalf("GetTo: expected 2 address, got: %d", len(addresses)) + t.Fatalf("expected 2 address, got: %d", len(addresses)) } if addresses[0] == nil { - t.Fatalf("GetTo: expected address, got nil") + t.Fatalf("expected address, got nil") } if addresses[0].String() != "" { - t.Errorf("GetTo: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addresses[0].String()) } if addresses[1] == nil { - t.Fatalf("GetTo: expected address, got nil") + t.Fatalf("expected address, got nil") } if addresses[1].String() != "" { - t.Errorf("GetTo: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addresses[1].String()) } }) @@ -2765,7 +2773,7 @@ func TestMsg_GetTo(t *testing.T) { } addresses := message.GetTo() if len(addresses) != 0 { - t.Errorf("GetTo: expected 0 address, got: %d", len(addresses)) + t.Errorf("expected 0 address, got: %d", len(addresses)) } }) } @@ -2781,10 +2789,10 @@ func TestMsg_GetToString(t *testing.T) { } addresses := message.GetToString() if len(addresses) != 1 { - t.Fatalf("GetToString: expected 1 address, got: %d", len(addresses)) + t.Fatalf("expected 1 address, got: %d", len(addresses)) } if addresses[0] == "" { - t.Fatalf("GetToString: expected address, got nil") + t.Fatalf("expected address, got nil") } if addresses[0] != "" { t.Errorf("GetToString: expected address not returned. Want: %s, got: %s", @@ -2801,20 +2809,20 @@ func TestMsg_GetToString(t *testing.T) { } addresses := message.GetToString() if len(addresses) != 2 { - t.Fatalf("GetToString: expected 2 address, got: %d", len(addresses)) + t.Fatalf("expected 2 address, got: %d", len(addresses)) } if addresses[0] == "" { - t.Fatalf("GetToString: expected address, got nil") + t.Fatalf("expected address, got nil") } if addresses[0] != "" { - t.Errorf("GetToString: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addresses[0]) } if addresses[1] == "" { - t.Fatalf("GetToString: expected address, got nil") + t.Fatalf("expected address, got nil") } if addresses[1] != "" { - t.Errorf("GetToString: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addresses[1]) } }) @@ -2825,7 +2833,7 @@ func TestMsg_GetToString(t *testing.T) { } addresses := message.GetToString() if len(addresses) != 0 { - t.Errorf("GetTo: expected 0 address, got: %d", len(addresses)) + t.Errorf("expected 0 address, got: %d", len(addresses)) } }) } @@ -2841,13 +2849,13 @@ func TestMsg_GetCc(t *testing.T) { } addresses := message.GetCc() if len(addresses) != 1 { - t.Fatalf("GetCc: expected 1 address, got: %d", len(addresses)) + t.Fatalf("expected 1 address, got: %d", len(addresses)) } if addresses[0] == nil { - t.Fatalf("GetCc: expected address, got nil") + t.Fatalf("expected address, got nil") } if addresses[0].String() != "" { - t.Errorf("GetCc: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addresses[0].String()) } }) @@ -2861,20 +2869,20 @@ func TestMsg_GetCc(t *testing.T) { } addresses := message.GetCc() if len(addresses) != 2 { - t.Fatalf("GetCc: expected 2 address, got: %d", len(addresses)) + t.Fatalf("expected 2 address, got: %d", len(addresses)) } if addresses[0] == nil { - t.Fatalf("GetCc: expected address, got nil") + t.Fatalf("expected address, got nil") } if addresses[0].String() != "" { - t.Errorf("GetCc: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addresses[0].String()) } if addresses[1] == nil { - t.Fatalf("GetCc: expected address, got nil") + t.Fatalf("expected address, got nil") } if addresses[1].String() != "" { - t.Errorf("GetCc: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addresses[1].String()) } }) @@ -2885,7 +2893,7 @@ func TestMsg_GetCc(t *testing.T) { } addresses := message.GetCc() if len(addresses) != 0 { - t.Errorf("GetCc: expected 0 address, got: %d", len(addresses)) + t.Errorf("expected 0 address, got: %d", len(addresses)) } }) } @@ -2901,13 +2909,13 @@ func TestMsg_GetCcString(t *testing.T) { } addresses := message.GetCcString() if len(addresses) != 1 { - t.Fatalf("GetCcString: expected 1 address, got: %d", len(addresses)) + t.Fatalf("expected 1 address, got: %d", len(addresses)) } if addresses[0] == "" { - t.Fatalf("GetCcString: expected address, got nil") + t.Fatalf("expected address, got nil") } if addresses[0] != "" { - t.Errorf("GetCcString: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addresses[0]) } }) @@ -2921,20 +2929,20 @@ func TestMsg_GetCcString(t *testing.T) { } addresses := message.GetCcString() if len(addresses) != 2 { - t.Fatalf("GetCcString: expected 2 address, got: %d", len(addresses)) + t.Fatalf("expected 2 address, got: %d", len(addresses)) } if addresses[0] == "" { - t.Fatalf("GetCcString: expected address, got nil") + t.Fatalf("expected address, got nil") } if addresses[0] != "" { - t.Errorf("GetCcString: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addresses[0]) } if addresses[1] == "" { t.Fatalf("GetCcString: expected address, got nil") } if addresses[1] != "" { - t.Errorf("GetCcString: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addresses[1]) } }) @@ -2945,7 +2953,7 @@ func TestMsg_GetCcString(t *testing.T) { } addresses := message.GetCcString() if len(addresses) != 0 { - t.Errorf("GetTo: expected 0 address, got: %d", len(addresses)) + t.Errorf("expected 0 address, got: %d", len(addresses)) } }) } @@ -2961,13 +2969,13 @@ func TestMsg_GetBcc(t *testing.T) { } addresses := message.GetBcc() if len(addresses) != 1 { - t.Fatalf("GetBcc: expected 1 address, got: %d", len(addresses)) + t.Fatalf("expected 1 address, got: %d", len(addresses)) } if addresses[0] == nil { - t.Fatalf("GetBcc: expected address, got nil") + t.Fatalf("expected address, got nil") } if addresses[0].String() != "" { - t.Errorf("GetBcc: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addresses[0].String()) } }) @@ -2981,20 +2989,20 @@ func TestMsg_GetBcc(t *testing.T) { } addresses := message.GetBcc() if len(addresses) != 2 { - t.Fatalf("GetBcc: expected 2 address, got: %d", len(addresses)) + t.Fatalf("expected 2 address, got: %d", len(addresses)) } if addresses[0] == nil { - t.Fatalf("GetBcc: expected address, got nil") + t.Fatalf("expected address, got nil") } if addresses[0].String() != "" { - t.Errorf("GetBcc: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addresses[0].String()) } if addresses[1] == nil { - t.Fatalf("GetBcc: expected address, got nil") + t.Fatalf("expected address, got nil") } if addresses[1].String() != "" { - t.Errorf("GetBcc: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addresses[1].String()) } }) @@ -3005,7 +3013,7 @@ func TestMsg_GetBcc(t *testing.T) { } addresses := message.GetBcc() if len(addresses) != 0 { - t.Errorf("GetBcc: expected 0 address, got: %d", len(addresses)) + t.Errorf("expected 0 address, got: %d", len(addresses)) } }) } @@ -3021,13 +3029,13 @@ func TestMsg_GetBccString(t *testing.T) { } addresses := message.GetBccString() if len(addresses) != 1 { - t.Fatalf("GetBccString: expected 1 address, got: %d", len(addresses)) + t.Fatalf("expected 1 address, got: %d", len(addresses)) } if addresses[0] == "" { - t.Fatalf("GetBccString: expected address, got nil") + t.Fatalf("expected address, got nil") } if addresses[0] != "" { - t.Errorf("GetBccString: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addresses[0]) } }) @@ -3041,20 +3049,20 @@ func TestMsg_GetBccString(t *testing.T) { } addresses := message.GetBccString() if len(addresses) != 2 { - t.Fatalf("GetBccString: expected 2 address, got: %d", len(addresses)) + t.Fatalf("expected 2 address, got: %d", len(addresses)) } if addresses[0] == "" { - t.Fatalf("GetBccString: expected address, got nil") + t.Fatalf("expected address, got nil") } if addresses[0] != "" { - t.Errorf("GetBccString: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addresses[0]) } if addresses[1] == "" { - t.Fatalf("GetBccString: expected address, got nil") + t.Fatalf("expected address, got nil") } if addresses[1] != "" { - t.Errorf("GetBccString: expected address not returned. Want: %s, got: %s", + t.Errorf("expected address not returned. Want: %s, got: %s", "", addresses[1]) } }) @@ -3065,7 +3073,7 @@ func TestMsg_GetBccString(t *testing.T) { } addresses := message.GetBccString() if len(addresses) != 0 { - t.Errorf("GetTo: expected 0 address, got: %d", len(addresses)) + t.Errorf("expected 0 address, got: %d", len(addresses)) } }) } @@ -3081,10 +3089,10 @@ func TestMsg_GetGenHeader(t *testing.T) { message.SetGenHeader(tt.header, "test") values := message.GetGenHeader(tt.header) if len(values) != 1 { - t.Errorf("GetGenHeader: expected 1 value, got: %d", len(values)) + t.Errorf("expected 1 value, got: %d", len(values)) } if values[0] != "test" { - t.Errorf("GetGenHeader: expected value not returned. Want: %s, got: %s", + t.Errorf("expected value not returned. Want: %s, got: %s", "test", values[0]) } }) @@ -3100,14 +3108,14 @@ func TestMsg_GetGenHeader(t *testing.T) { message.SetGenHeader(tt.header, "test", "foobar") values := message.GetGenHeader(tt.header) if len(values) != 2 { - t.Errorf("GetGenHeader: expected 1 value, got: %d", len(values)) + t.Errorf("expected 1 value, got: %d", len(values)) } if values[0] != "test" { - t.Errorf("GetGenHeader: expected value not returned. Want: %s, got: %s", + t.Errorf("expected value not returned. Want: %s, got: %s", "test", values[0]) } if values[1] != "foobar" { - t.Errorf("GetGenHeader: expected value not returned. Want: %s, got: %s", + t.Errorf("expected value not returned. Want: %s, got: %s", "foobar", values[1]) } }) @@ -3123,7 +3131,7 @@ func TestMsg_GetGenHeader(t *testing.T) { message.SetGenHeader(tt.header) values := message.GetGenHeader(tt.header) if len(values) != 0 { - t.Errorf("GetGenHeader: expected 1 value, got: %d", len(values)) + t.Errorf("expected 1 value, got: %d", len(values)) } }) } @@ -3139,21 +3147,21 @@ func TestMsg_GetParts(t *testing.T) { message.SetBodyString(TypeTextPlain, "this is a test body") parts := message.GetParts() if len(parts) != 1 { - t.Fatalf("GetParts: expected 1 part, got: %d", len(parts)) + t.Fatalf("expected 1 part, got: %d", len(parts)) } if parts[0] == nil { - t.Fatalf("GetParts: expected part, got nil") + t.Fatalf("expected part, got nil") } if parts[0].contentType != TypeTextPlain { - t.Errorf("GetParts: expected contentType to be TypeTextPlain, got: %s", parts[0].contentType) + t.Errorf("expected contentType to be TypeTextPlain, got: %s", parts[0].contentType) } messageBuf := bytes.NewBuffer(nil) _, err := parts[0].writeFunc(messageBuf) if err != nil { - t.Errorf("GetParts: writeFunc failed: %s", err) + t.Errorf("writeFunc failed: %s", err) } if !strings.EqualFold(messageBuf.String(), "this is a test body") { - t.Errorf("GetParts: expected message body to be %s, got: %s", "this is a test body", + t.Errorf("expected message body to be %s, got: %s", "this is a test body", messageBuf.String()) } }) @@ -3166,24 +3174,24 @@ func TestMsg_GetParts(t *testing.T) { message.AddAlternativeString(TypeTextHTML, "

This is HTML

") parts := message.GetParts() if len(parts) != 2 { - t.Fatalf("GetParts: expected 2 parts, got: %d", len(parts)) + t.Fatalf("expected 2 parts, got: %d", len(parts)) } if parts[0] == nil || parts[1] == nil { - t.Fatalf("GetParts: expected parts, got nil") + t.Fatalf("expected parts, got nil") } if parts[0].contentType != TypeTextPlain { - t.Errorf("GetParts: expected contentType to be TypeTextPlain, got: %s", parts[0].contentType) + t.Errorf("expected contentType to be TypeTextPlain, got: %s", parts[0].contentType) } if parts[1].contentType != TypeTextHTML { - t.Errorf("GetParts: expected contentType to be TypeTextHTML, got: %s", parts[1].contentType) + t.Errorf("expected contentType to be TypeTextHTML, got: %s", parts[1].contentType) } messageBuf := bytes.NewBuffer(nil) _, err := parts[0].writeFunc(messageBuf) if err != nil { - t.Errorf("GetParts: writeFunc failed: %s", err) + t.Errorf("writeFunc failed: %s", err) } if !strings.EqualFold(messageBuf.String(), "this is a test body") { - t.Errorf("GetParts: expected message body to be %s, got: %s", "this is a test body", + t.Errorf("expected message body to be %s, got: %s", "this is a test body", messageBuf.String()) } messageBuf.Reset() @@ -3192,7 +3200,7 @@ func TestMsg_GetParts(t *testing.T) { t.Errorf("GetParts: writeFunc failed: %s", err) } if !strings.EqualFold(messageBuf.String(), "

This is HTML

") { - t.Errorf("GetParts: expected message body to be %s, got: %s", "

This is HTML

", + t.Errorf("expected message body to be %s, got: %s", "

This is HTML

", messageBuf.String()) } }) @@ -3203,7 +3211,7 @@ func TestMsg_GetParts(t *testing.T) { } parts := message.GetParts() if len(parts) != 0 { - t.Fatalf("GetParts: expected no parts, got: %d", len(parts)) + t.Fatalf("expected no parts, got: %d", len(parts)) } }) } @@ -3217,22 +3225,22 @@ func TestMsg_GetAttachments(t *testing.T) { message.AttachFile("testdata/attachment.txt") attachments := message.GetAttachments() if len(attachments) != 1 { - t.Fatalf("GetAttachments: expected 1 attachment, got: %d", len(attachments)) + t.Fatalf("expected 1 attachment, got: %d", len(attachments)) } if attachments[0] == nil { - t.Fatalf("GetAttachments: expected attachment, got nil") + t.Fatalf("expected attachment, got nil") } if attachments[0].Name != "attachment.txt" { - t.Errorf("GetAttachments: expected attachment name to be %s, got: %s", "attachment.txt", + t.Errorf("expected attachment name to be %s, got: %s", "attachment.txt", attachments[0].Name) } messageBuf := bytes.NewBuffer(nil) _, err := attachments[0].Writer(messageBuf) if err != nil { - t.Errorf("GetAttachments: Writer func failed: %s", err) + t.Errorf("writer func failed: %s", err) } if !strings.EqualFold(messageBuf.String(), "This is a test attachment\n") { - t.Errorf("GetParts: expected message body to be %s, got: %s", "This is a test attachment\n", + t.Errorf("expected message body to be %s, got: %s", "This is a test attachment\n", messageBuf.String()) } }) @@ -3245,35 +3253,35 @@ func TestMsg_GetAttachments(t *testing.T) { message.AttachFile("testdata/attachment.txt", WithFileName("attachment2.txt")) attachments := message.GetAttachments() if len(attachments) != 2 { - t.Fatalf("GetAttachments: expected 2 attachment, got: %d", len(attachments)) + t.Fatalf("expected 2 attachment, got: %d", len(attachments)) } if attachments[0] == nil || attachments[1] == nil { - t.Fatalf("GetAttachments: expected attachment, got nil") + t.Fatalf("expected attachment, got nil") } if attachments[0].Name != "attachment.txt" { - t.Errorf("GetAttachments: expected attachment name to be %s, got: %s", "attachment.txt", + t.Errorf("expected attachment name to be %s, got: %s", "attachment.txt", attachments[0].Name) } if attachments[1].Name != "attachment2.txt" { - t.Errorf("GetAttachments: expected attachment name to be %s, got: %s", "attachment2.txt", + t.Errorf("expected attachment name to be %s, got: %s", "attachment2.txt", attachments[1].Name) } messageBuf := bytes.NewBuffer(nil) _, err := attachments[0].Writer(messageBuf) if err != nil { - t.Errorf("GetAttachments: Writer func failed: %s", err) + t.Errorf("writer func failed: %s", err) } if !strings.EqualFold(messageBuf.String(), "This is a test attachment\n") { - t.Errorf("GetParts: expected message body to be %s, got: %s", "This is a test attachment\n", + t.Errorf("expected message body to be %s, got: %s", "This is a test attachment\n", messageBuf.String()) } messageBuf.Reset() _, err = attachments[1].Writer(messageBuf) if err != nil { - t.Errorf("GetAttachments: Writer func failed: %s", err) + t.Errorf("writer func failed: %s", err) } if !strings.EqualFold(messageBuf.String(), "This is a test attachment\n") { - t.Errorf("GetParts: expected message body to be %s, got: %s", "This is a test attachment\n", + t.Errorf("expected message body to be %s, got: %s", "This is a test attachment\n", messageBuf.String()) } }) @@ -3284,7 +3292,7 @@ func TestMsg_GetAttachments(t *testing.T) { } attachments := message.GetAttachments() if len(attachments) != 0 { - t.Fatalf("GetAttachments: expected 1 attachment, got: %d", len(attachments)) + t.Fatalf("expected 1 attachment, got: %d", len(attachments)) } }) } @@ -3296,7 +3304,7 @@ func TestMsg_GetBoundary(t *testing.T) { t.Fatal("message is nil") } if message.GetBoundary() != "test" { - t.Errorf("GetBoundary: expected %s, got: %s", "test", message.GetBoundary()) + t.Errorf("expected %s, got: %s", "test", message.GetBoundary()) } }) t.Run("GetBoundary with no boundary", func(t *testing.T) { @@ -3305,7 +3313,7 @@ func TestMsg_GetBoundary(t *testing.T) { t.Fatal("message is nil") } if message.GetBoundary() != "" { - t.Errorf("GetBoundary: expected empty, got: %s", message.GetBoundary()) + t.Errorf("expected empty, got: %s", message.GetBoundary()) } }) } @@ -3329,22 +3337,22 @@ func TestMsg_SetAttachments(t *testing.T) { message.SetAttachments([]*File{file}) attachments := message.GetAttachments() if len(attachments) != 1 { - t.Fatalf("GetAttachments: expected 1 attachment, got: %d", len(attachments)) + t.Fatalf("expected 1 attachment, got: %d", len(attachments)) } if attachments[0] == nil { - t.Fatalf("GetAttachments: expected attachment, got nil") + t.Fatalf("expected attachment, got nil") } if attachments[0].Name != "attachment.txt" { - t.Errorf("GetAttachments: expected attachment name to be %s, got: %s", "attachment.txt", + t.Errorf("expected attachment name to be %s, got: %s", "attachment.txt", attachments[0].Name) } messageBuf := bytes.NewBuffer(nil) _, err := attachments[0].Writer(messageBuf) if err != nil { - t.Errorf("GetAttachments: Writer func failed: %s", err) + t.Errorf("writer func failed: %s", err) } if !strings.EqualFold(messageBuf.String(), "This is a test attachment\n") { - t.Errorf("GetParts: expected message body to be %s, got: %s", "This is a test attachment\n", + t.Errorf("expected message body to be %s, got: %s", "This is a test attachment\n", messageBuf.String()) } }) @@ -3376,35 +3384,35 @@ func TestMsg_SetAttachments(t *testing.T) { message.SetAttachments([]*File{file1, file2}) attachments := message.GetAttachments() if len(attachments) != 2 { - t.Fatalf("GetAttachments: expected 2 attachment, got: %d", len(attachments)) + t.Fatalf("expected 2 attachment, got: %d", len(attachments)) } if attachments[0] == nil || attachments[1] == nil { - t.Fatalf("GetAttachments: expected attachment, got nil") + t.Fatalf("expected attachment, got nil") } if attachments[0].Name != "attachment.txt" { - t.Errorf("GetAttachments: expected attachment name to be %s, got: %s", "attachment.txt", + t.Errorf("expected attachment name to be %s, got: %s", "attachment.txt", attachments[0].Name) } if attachments[1].Name != "attachment2.txt" { - t.Errorf("GetAttachments: expected attachment name to be %s, got: %s", "attachment2.txt", + t.Errorf("expected attachment name to be %s, got: %s", "attachment2.txt", attachments[1].Name) } messageBuf := bytes.NewBuffer(nil) _, err := attachments[0].Writer(messageBuf) if err != nil { - t.Errorf("GetAttachments: Writer func failed: %s", err) + t.Errorf("writer func failed: %s", err) } if !strings.EqualFold(messageBuf.String(), "This is a test attachment\n") { - t.Errorf("GetParts: expected message body to be %s, got: %s", "This is a test attachment\n", + t.Errorf("expected message body to be %s, got: %s", "This is a test attachment\n", messageBuf.String()) } messageBuf.Reset() _, err = attachments[1].Writer(messageBuf) if err != nil { - t.Errorf("GetAttachments: Writer func failed: %s", err) + t.Errorf("writer func failed: %s", err) } if !strings.EqualFold(messageBuf.String(), "This is also a test attachment\n") { - t.Errorf("GetParts: expected message body to be %s, got: %s", "This is also a test attachment\n", + t.Errorf("expected message body to be %s, got: %s", "This is also a test attachment\n", messageBuf.String()) } }) @@ -3416,7 +3424,7 @@ func TestMsg_SetAttachments(t *testing.T) { message.SetAttachments(nil) attachments := message.GetAttachments() if len(attachments) != 0 { - t.Fatalf("GetAttachments: expected 0 attachment, got: %d", len(attachments)) + t.Fatalf("expected 0 attachment, got: %d", len(attachments)) } }) } @@ -3455,11 +3463,11 @@ func TestMsg_UnsetAllAttachments(t *testing.T) { message.SetAttachments([]*File{file1, file2}) message.UnsetAllAttachments() if message.attachments != nil { - t.Errorf("UnsetAllAttachments: expected attachments to be nil, got: %v", message.attachments) + t.Errorf("expected attachments to be nil, got: %v", message.attachments) } attachments := message.GetAttachments() if len(attachments) != 0 { - t.Fatalf("GetAttachments: expected 0 attachment, got: %d", len(attachments)) + t.Fatalf("expected 0 attachment, got: %d", len(attachments)) } } @@ -3472,22 +3480,22 @@ func TestMsg_GetEmbeds(t *testing.T) { message.EmbedFile("testdata/embed.txt") embeds := message.GetEmbeds() if len(embeds) != 1 { - t.Fatalf("GetEmbeds: expected 1 embed, got: %d", len(embeds)) + t.Fatalf("expected 1 embed, got: %d", len(embeds)) } if embeds[0] == nil { - t.Fatalf("GetEmbeds: expected embed, got nil") + t.Fatalf("expected embed, got nil") } if embeds[0].Name != "embed.txt" { - t.Errorf("GetEmbeds: expected embed name to be %s, got: %s", "embed.txt", + t.Errorf("expected embed name to be %s, got: %s", "embed.txt", embeds[0].Name) } messageBuf := bytes.NewBuffer(nil) _, err := embeds[0].Writer(messageBuf) if err != nil { - t.Errorf("GetEmbeds: Writer func failed: %s", err) + t.Errorf("Writer func failed: %s", err) } if !strings.EqualFold(messageBuf.String(), "This is a test embed\n") { - t.Errorf("GetParts: expected message body to be %s, got: %s", "This is a test embed\n", + t.Errorf("expected message body to be %s, got: %s", "This is a test embed\n", messageBuf.String()) } }) @@ -3500,35 +3508,35 @@ func TestMsg_GetEmbeds(t *testing.T) { message.EmbedFile("testdata/embed.txt", WithFileName("embed2.txt")) embeds := message.GetEmbeds() if len(embeds) != 2 { - t.Fatalf("GetEmbeds: expected 2 embed, got: %d", len(embeds)) + t.Fatalf("expected 2 embed, got: %d", len(embeds)) } if embeds[0] == nil || embeds[1] == nil { - t.Fatalf("GetEmbeds: expected embed, got nil") + t.Fatalf("expected embed, got nil") } if embeds[0].Name != "embed.txt" { - t.Errorf("GetEmbeds: expected embed name to be %s, got: %s", "embed.txt", + t.Errorf("expected embed name to be %s, got: %s", "embed.txt", embeds[0].Name) } if embeds[1].Name != "embed2.txt" { - t.Errorf("GetEmbeds: expected embed name to be %s, got: %s", "embed2.txt", + t.Errorf("expected embed name to be %s, got: %s", "embed2.txt", embeds[1].Name) } messageBuf := bytes.NewBuffer(nil) _, err := embeds[0].Writer(messageBuf) if err != nil { - t.Errorf("GetEmbeds: Writer func failed: %s", err) + t.Errorf("Writer func failed: %s", err) } if !strings.EqualFold(messageBuf.String(), "This is a test embed\n") { - t.Errorf("GetParts: expected message body to be %s, got: %s", "This is a test embed\n", + t.Errorf("expected message body to be %s, got: %s", "This is a test embed\n", messageBuf.String()) } messageBuf.Reset() _, err = embeds[1].Writer(messageBuf) if err != nil { - t.Errorf("GetEmbeds: Writer func failed: %s", err) + t.Errorf("Writer func failed: %s", err) } if !strings.EqualFold(messageBuf.String(), "This is a test embed\n") { - t.Errorf("GetParts: expected message body to be %s, got: %s", "This is a test embed\n", + t.Errorf("expected message body to be %s, got: %s", "This is a test embed\n", messageBuf.String()) } }) @@ -3539,7 +3547,7 @@ func TestMsg_GetEmbeds(t *testing.T) { } embeds := message.GetEmbeds() if len(embeds) != 0 { - t.Fatalf("GetEmbeds: expected 1 embeds, got: %d", len(embeds)) + t.Fatalf("expected 1 embeds, got: %d", len(embeds)) } }) } @@ -3563,22 +3571,22 @@ func TestMsg_SetEmbeds(t *testing.T) { message.SetEmbeds([]*File{file}) embeds := message.GetEmbeds() if len(embeds) != 1 { - t.Fatalf("GetEmbeds: expected 1 embed, got: %d", len(embeds)) + t.Fatalf("expected 1 embed, got: %d", len(embeds)) } if embeds[0] == nil { - t.Fatalf("GetEmbeds: expected embed, got nil") + t.Fatalf("expected embed, got nil") } if embeds[0].Name != "embed.txt" { - t.Errorf("GetEmbeds: expected embed name to be %s, got: %s", "embed.txt", + t.Errorf("expected embed name to be %s, got: %s", "embed.txt", embeds[0].Name) } messageBuf := bytes.NewBuffer(nil) _, err := embeds[0].Writer(messageBuf) if err != nil { - t.Errorf("GetEmbeds: Writer func failed: %s", err) + t.Errorf("Writer func failed: %s", err) } if !strings.EqualFold(messageBuf.String(), "This is a test embed\n") { - t.Errorf("GetParts: expected message body to be %s, got: %s", "This is a test embed\n", + t.Errorf("expected message body to be %s, got: %s", "This is a test embed\n", messageBuf.String()) } }) @@ -3610,35 +3618,35 @@ func TestMsg_SetEmbeds(t *testing.T) { message.SetEmbeds([]*File{file1, file2}) embeds := message.GetEmbeds() if len(embeds) != 2 { - t.Fatalf("GetEmbeds: expected 2 embed, got: %d", len(embeds)) + t.Fatalf("expected 2 embed, got: %d", len(embeds)) } if embeds[0] == nil || embeds[1] == nil { - t.Fatalf("GetEmbeds: expected embed, got nil") + t.Fatalf("expected embed, got nil") } if embeds[0].Name != "embed.txt" { - t.Errorf("GetEmbeds: expected embed name to be %s, got: %s", "embed.txt", + t.Errorf("expected embed name to be %s, got: %s", "embed.txt", embeds[0].Name) } if embeds[1].Name != "embed2.txt" { - t.Errorf("GetEmbeds: expected embed name to be %s, got: %s", "embed2.txt", + t.Errorf("expected embed name to be %s, got: %s", "embed2.txt", embeds[1].Name) } messageBuf := bytes.NewBuffer(nil) _, err := embeds[0].Writer(messageBuf) if err != nil { - t.Errorf("GetEmbeds: Writer func failed: %s", err) + t.Errorf("Writer func failed: %s", err) } if !strings.EqualFold(messageBuf.String(), "This is a test embed\n") { - t.Errorf("GetParts: expected message body to be %s, got: %s", "This is a test embed\n", + t.Errorf("expected message body to be %s, got: %s", "This is a test embed\n", messageBuf.String()) } messageBuf.Reset() _, err = embeds[1].Writer(messageBuf) if err != nil { - t.Errorf("GetEmbeds: Writer func failed: %s", err) + t.Errorf("Writer func failed: %s", err) } if !strings.EqualFold(messageBuf.String(), "This is also a test embed\n") { - t.Errorf("GetParts: expected message body to be %s, got: %s", "This is also a test embed\n", + t.Errorf("expected message body to be %s, got: %s", "This is also a test embed\n", messageBuf.String()) } }) @@ -3650,7 +3658,7 @@ func TestMsg_SetEmbeds(t *testing.T) { message.SetEmbeds(nil) embeds := message.GetEmbeds() if len(embeds) != 0 { - t.Fatalf("GetEmbeds: expected 0 embed, got: %d", len(embeds)) + t.Fatalf("expected 0 embed, got: %d", len(embeds)) } }) } @@ -3683,11 +3691,11 @@ func TestMsg_UnsetAllEmbeds(t *testing.T) { message.SetEmbeds([]*File{file1, file2}) message.UnsetAllEmbeds() if message.embeds != nil { - t.Errorf("UnsetAllEmbeds: expected embeds to be nil, got: %v", message.embeds) + t.Errorf("expected embeds to be nil, got: %v", message.embeds) } embeds := message.GetEmbeds() if len(embeds) != 0 { - t.Fatalf("GetEmbeds: expected 0 embed, got: %d", len(embeds)) + t.Fatalf("expected 0 embed, got: %d", len(embeds)) } } @@ -3720,18 +3728,123 @@ func TestMsg_UnsetAllParts(t *testing.T) { message.SetEmbeds([]*File{file2}) message.UnsetAllParts() if message.embeds != nil || message.attachments != nil { - t.Error("UnsetAllParts: expected attachments/embeds to be nil, got: value") + t.Error("expected attachments/embeds to be nil, got: value") } embeds := message.GetEmbeds() if len(embeds) != 0 { - t.Fatalf("GetEmbeds: expected 0 embed, got: %d", len(embeds)) + t.Fatalf("expected 0 embed, got: %d", len(embeds)) } attachments := message.GetAttachments() if len(attachments) != 0 { - t.Fatalf("GetAttachments: expected 0 attachments, got: %d", len(attachments)) + t.Fatalf("expected 0 attachments, got: %d", len(attachments)) } } +func TestMsg_SetBodyString(t *testing.T) { + t.Run("SetBodyString on all types", func(t *testing.T) { + for _, tt := range contentTypeTests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetBodyString(tt.ctype, "test") + parts := message.GetParts() + if len(parts) != 1 { + t.Fatalf("expected 1 part, got: %d", len(parts)) + } + if parts[0] == nil { + t.Fatal("expected part to be not nil") + } + if parts[0].contentType != tt.ctype { + t.Errorf("expected contentType to be %s, got: %s", tt.ctype, + parts[0].contentType) + } + messageBuf := bytes.NewBuffer(nil) + _, err := parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "test") { + t.Errorf("expected message body to be %s, got: %s", "test", messageBuf.String()) + } + }) + } + }) +} + +func TestMsg_SetBodyWriter(t *testing.T) { + writerFunc := func(w io.Writer) (int64, error) { + buffer := bytes.NewBufferString("test") + n, err := w.Write(buffer.Bytes()) + return int64(n), err + } + t.Run("SetBodyWriter on all types", func(t *testing.T) { + for _, tt := range contentTypeTests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetBodyWriter(tt.ctype, writerFunc) + parts := message.GetParts() + if len(parts) != 1 { + t.Fatalf("expected 1 part, got: %d", len(parts)) + } + if parts[0] == nil { + t.Fatal("expected part to be not nil") + } + if parts[0].contentType != tt.ctype { + t.Errorf("expected contentType to be %s, got: %s", tt.ctype, + parts[0].contentType) + } + messageBuf := bytes.NewBuffer(nil) + _, err := parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "test") { + t.Errorf("expected message body to be %s, got: %s", "test", messageBuf.String()) + } + }) + } + }) + t.Run("SetBodyWriter WithPartCharset", func(t *testing.T) { + for _, tt := range charsetTests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetBodyWriter(TypeTextPlain, writerFunc, WithPartCharset(tt.value)) + parts := message.GetParts() + if len(parts) != 1 { + t.Fatalf("expected 1 part, got: %d", len(parts)) + } + if parts[0] == nil { + t.Fatal("expected part to be not nil") + } + if parts[0].contentType != TypeTextPlain { + t.Errorf("expected contentType to be %s, got: %s", TypeTextPlain, + parts[0].contentType) + } + if parts[0].charset != tt.value { + t.Errorf("expected charset to be %s, got: %s", tt.value, parts[0].charset) + } + messageBuf := bytes.NewBuffer(nil) + _, err := parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "test") { + t.Errorf("expected message body to be %s, got: %s", "test", messageBuf.String()) + } + }) + } + }) + +} + // checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. // It checks whether the AddrHeader contains the correct address, name, and number of fields. func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, From cb5ac8b0e259f3daea1c28497911a30bfb9876b8 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 11:32:15 +0100 Subject: [PATCH 129/188] 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. --- msg_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msg_test.go b/msg_test.go index af05cf8..0411675 100644 --- a/msg_test.go +++ b/msg_test.go @@ -3431,6 +3431,7 @@ func TestMsg_SetAttachments(t *testing.T) { func TestMsg_SetAttachements(t *testing.T) { message := NewMsg() + //goland:noinspection GoDeprecation message.SetAttachements(nil) t.Skip("SetAttachements is deprecated and fully tested by SetAttachments already") } @@ -3842,7 +3843,6 @@ func TestMsg_SetBodyWriter(t *testing.T) { }) } }) - } // checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. From 056ec607345fde23a3801e74e26c32db8b2e643d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 12:05:18 +0100 Subject: [PATCH 130/188] 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. --- msg_test.go | 96 ++++++++++++++++++++++++++--------------------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/msg_test.go b/msg_test.go index 0411675..5ba0dab 100644 --- a/msg_test.go +++ b/msg_test.go @@ -3239,9 +3239,9 @@ func TestMsg_GetAttachments(t *testing.T) { if err != nil { t.Errorf("writer func failed: %s", err) } - if !strings.EqualFold(messageBuf.String(), "This is a test attachment\n") { - t.Errorf("expected message body to be %s, got: %s", "This is a test attachment\n", - messageBuf.String()) + got := strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "This is a test attachment") { + t.Errorf("expected message body to be %s, got: %s", "This is a test attachment", got) } }) t.Run("GetAttachments with multiple attachments", func(t *testing.T) { @@ -3271,18 +3271,18 @@ func TestMsg_GetAttachments(t *testing.T) { if err != nil { t.Errorf("writer func failed: %s", err) } - if !strings.EqualFold(messageBuf.String(), "This is a test attachment\n") { - t.Errorf("expected message body to be %s, got: %s", "This is a test attachment\n", - messageBuf.String()) + got := strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "This is a test attachment") { + t.Errorf("expected message body to be %s, got: %s", "This is a test attachment", got) } messageBuf.Reset() _, err = attachments[1].Writer(messageBuf) if err != nil { t.Errorf("writer func failed: %s", err) } - if !strings.EqualFold(messageBuf.String(), "This is a test attachment\n") { - t.Errorf("expected message body to be %s, got: %s", "This is a test attachment\n", - messageBuf.String()) + got = strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "This is a test attachment") { + t.Errorf("expected message body to be %s, got: %s", "This is a test attachment", got) } }) t.Run("GetAttachments with no attachment", func(t *testing.T) { @@ -3329,7 +3329,7 @@ func TestMsg_SetAttachments(t *testing.T) { Desc: "Test file", Name: "attachment.txt", Writer: func(w io.Writer) (int64, error) { - buf := bytes.NewBuffer([]byte("This is a test attachment\n")) + buf := bytes.NewBuffer([]byte("This is a test attachment")) n, err := w.Write(buf.Bytes()) return int64(n), err }, @@ -3351,9 +3351,9 @@ func TestMsg_SetAttachments(t *testing.T) { if err != nil { t.Errorf("writer func failed: %s", err) } - if !strings.EqualFold(messageBuf.String(), "This is a test attachment\n") { - t.Errorf("expected message body to be %s, got: %s", "This is a test attachment\n", - messageBuf.String()) + got := strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "This is a test attachment") { + t.Errorf("expected message body to be %s, got: %s", "This is a test attachment", got) } }) t.Run("SetAttachments with multiple files", func(t *testing.T) { @@ -3366,7 +3366,7 @@ func TestMsg_SetAttachments(t *testing.T) { Desc: "Test file", Name: "attachment.txt", Writer: func(w io.Writer) (int64, error) { - buf := bytes.NewBuffer([]byte("This is a test attachment\n")) + buf := bytes.NewBuffer([]byte("This is a test attachment")) n, err := w.Write(buf.Bytes()) return int64(n), err }, @@ -3376,7 +3376,7 @@ func TestMsg_SetAttachments(t *testing.T) { Desc: "Test file no. 2", Name: "attachment2.txt", Writer: func(w io.Writer) (int64, error) { - buf := bytes.NewBuffer([]byte("This is also a test attachment\n")) + buf := bytes.NewBuffer([]byte("This is also a test attachment")) n, err := w.Write(buf.Bytes()) return int64(n), err }, @@ -3402,18 +3402,18 @@ func TestMsg_SetAttachments(t *testing.T) { if err != nil { t.Errorf("writer func failed: %s", err) } - if !strings.EqualFold(messageBuf.String(), "This is a test attachment\n") { - t.Errorf("expected message body to be %s, got: %s", "This is a test attachment\n", - messageBuf.String()) + got := strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(messageBuf.String(), "This is a test attachment") { + t.Errorf("expected message body to be %s, got: %s", "This is a test attachment", got) } messageBuf.Reset() _, err = attachments[1].Writer(messageBuf) if err != nil { t.Errorf("writer func failed: %s", err) } - if !strings.EqualFold(messageBuf.String(), "This is also a test attachment\n") { - t.Errorf("expected message body to be %s, got: %s", "This is also a test attachment\n", - messageBuf.String()) + got = strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "This is also a test attachment") { + t.Errorf("expected message body to be %s, got: %s", "This is also a test attachment", got) } }) t.Run("SetAttachments with no file", func(t *testing.T) { @@ -3446,7 +3446,7 @@ func TestMsg_UnsetAllAttachments(t *testing.T) { Desc: "Test file", Name: "attachment.txt", Writer: func(w io.Writer) (int64, error) { - buf := bytes.NewBuffer([]byte("This is a test attachment\n")) + buf := bytes.NewBuffer([]byte("This is a test attachment")) n, err := w.Write(buf.Bytes()) return int64(n), err }, @@ -3456,7 +3456,7 @@ func TestMsg_UnsetAllAttachments(t *testing.T) { Desc: "Test file no. 2", Name: "attachment2.txt", Writer: func(w io.Writer) (int64, error) { - buf := bytes.NewBuffer([]byte("This is also a test attachment\n")) + buf := bytes.NewBuffer([]byte("This is also a test attachment")) n, err := w.Write(buf.Bytes()) return int64(n), err }, @@ -3495,9 +3495,9 @@ func TestMsg_GetEmbeds(t *testing.T) { if err != nil { t.Errorf("Writer func failed: %s", err) } - if !strings.EqualFold(messageBuf.String(), "This is a test embed\n") { - t.Errorf("expected message body to be %s, got: %s", "This is a test embed\n", - messageBuf.String()) + got := strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "This is a test embed") { + t.Errorf("expected message body to be %s, got: %s", "This is a test embed", got) } }) t.Run("GetEmbeds with multiple embeds", func(t *testing.T) { @@ -3527,18 +3527,18 @@ func TestMsg_GetEmbeds(t *testing.T) { if err != nil { t.Errorf("Writer func failed: %s", err) } - if !strings.EqualFold(messageBuf.String(), "This is a test embed\n") { - t.Errorf("expected message body to be %s, got: %s", "This is a test embed\n", - messageBuf.String()) + got := strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "This is a test embed") { + t.Errorf("expected message body to be %s, got: %s", "This is a test embed", got) } messageBuf.Reset() _, err = embeds[1].Writer(messageBuf) if err != nil { t.Errorf("Writer func failed: %s", err) } - if !strings.EqualFold(messageBuf.String(), "This is a test embed\n") { - t.Errorf("expected message body to be %s, got: %s", "This is a test embed\n", - messageBuf.String()) + got = strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "This is a test embed") { + t.Errorf("expected message body to be %s, got: %s", "This is a test embed", got) } }) t.Run("GetEmbeds with no embeds", func(t *testing.T) { @@ -3564,7 +3564,7 @@ func TestMsg_SetEmbeds(t *testing.T) { Desc: "Test file", Name: "embed.txt", Writer: func(w io.Writer) (int64, error) { - buf := bytes.NewBuffer([]byte("This is a test embed\n")) + buf := bytes.NewBuffer([]byte("This is a test embed")) n, err := w.Write(buf.Bytes()) return int64(n), err }, @@ -3586,9 +3586,9 @@ func TestMsg_SetEmbeds(t *testing.T) { if err != nil { t.Errorf("Writer func failed: %s", err) } - if !strings.EqualFold(messageBuf.String(), "This is a test embed\n") { - t.Errorf("expected message body to be %s, got: %s", "This is a test embed\n", - messageBuf.String()) + got := strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "This is a test embed") { + t.Errorf("expected message body to be %s, got: %s", "This is a test embed", got) } }) t.Run("SetEmbeds with multiple files", func(t *testing.T) { @@ -3601,7 +3601,7 @@ func TestMsg_SetEmbeds(t *testing.T) { Desc: "Test file", Name: "embed.txt", Writer: func(w io.Writer) (int64, error) { - buf := bytes.NewBuffer([]byte("This is a test embed\n")) + buf := bytes.NewBuffer([]byte("This is a test embed")) n, err := w.Write(buf.Bytes()) return int64(n), err }, @@ -3611,7 +3611,7 @@ func TestMsg_SetEmbeds(t *testing.T) { Desc: "Test file no. 2", Name: "embed2.txt", Writer: func(w io.Writer) (int64, error) { - buf := bytes.NewBuffer([]byte("This is also a test embed\n")) + buf := bytes.NewBuffer([]byte("This is also a test embed")) n, err := w.Write(buf.Bytes()) return int64(n), err }, @@ -3637,18 +3637,18 @@ func TestMsg_SetEmbeds(t *testing.T) { if err != nil { t.Errorf("Writer func failed: %s", err) } - if !strings.EqualFold(messageBuf.String(), "This is a test embed\n") { - t.Errorf("expected message body to be %s, got: %s", "This is a test embed\n", - messageBuf.String()) + got := strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "This is a test embed") { + t.Errorf("expected message body to be %s, got: %s", "This is a test embed", got) } messageBuf.Reset() _, err = embeds[1].Writer(messageBuf) if err != nil { t.Errorf("Writer func failed: %s", err) } - if !strings.EqualFold(messageBuf.String(), "This is also a test embed\n") { - t.Errorf("expected message body to be %s, got: %s", "This is also a test embed\n", - messageBuf.String()) + got = strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "This is also a test embed") { + t.Errorf("expected message body to be %s, got: %s", "This is also a test embed", got) } }) t.Run("SetEmbeds with no file", func(t *testing.T) { @@ -3674,7 +3674,7 @@ func TestMsg_UnsetAllEmbeds(t *testing.T) { Desc: "Test file", Name: "embed.txt", Writer: func(w io.Writer) (int64, error) { - buf := bytes.NewBuffer([]byte("This is a test embed\n")) + buf := bytes.NewBuffer([]byte("This is a test embed")) n, err := w.Write(buf.Bytes()) return int64(n), err }, @@ -3684,7 +3684,7 @@ func TestMsg_UnsetAllEmbeds(t *testing.T) { Desc: "Test file no. 2", Name: "embed2.txt", Writer: func(w io.Writer) (int64, error) { - buf := bytes.NewBuffer([]byte("This is also a test embed\n")) + buf := bytes.NewBuffer([]byte("This is also a test embed")) n, err := w.Write(buf.Bytes()) return int64(n), err }, @@ -3710,7 +3710,7 @@ func TestMsg_UnsetAllParts(t *testing.T) { Desc: "Test file", Name: "embed.txt", Writer: func(w io.Writer) (int64, error) { - buf := bytes.NewBuffer([]byte("This is a test embed\n")) + buf := bytes.NewBuffer([]byte("This is a test embed")) n, err := w.Write(buf.Bytes()) return int64(n), err }, @@ -3720,7 +3720,7 @@ func TestMsg_UnsetAllParts(t *testing.T) { Desc: "Test file no. 2", Name: "embed2.txt", Writer: func(w io.Writer) (int64, error) { - buf := bytes.NewBuffer([]byte("This is also a test embed\n")) + buf := bytes.NewBuffer([]byte("This is also a test embed")) n, err := w.Write(buf.Bytes()) return int64(n), err }, From b510d2654c7f83d24bbbb78aa3f79b48f097ae0e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 12:10:51 +0100 Subject: [PATCH 131/188] 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. --- client_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client_test.go b/client_test.go index 70032ba..cebadc5 100644 --- a/client_test.go +++ b/client_test.go @@ -2579,6 +2579,10 @@ func TestClient_Send(t *testing.T) { } t.Cleanup(func() { if err := client.Close(); 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.Errorf("failed to close client: %s", err) } }) From 78ee1a2a8111c0c4a7398c49086107e9de9c32cf Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 12:11:48 +0100 Subject: [PATCH 132/188] 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. --- client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client_test.go b/client_test.go index cebadc5..46c7dcd 100644 --- a/client_test.go +++ b/client_test.go @@ -2581,7 +2581,7 @@ func TestClient_Send(t *testing.T) { if err := client.Close(); 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.Skip("failed to close the test server connection due to timeout") } t.Errorf("failed to close client: %s", err) } From 0aa81d724b73e4c9fab11b2a29facb8c39e7f271 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 14:20:44 +0100 Subject: [PATCH 133/188] 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. --- msg_test.go | 292 ++++++++-------------------------------------------- 1 file changed, 45 insertions(+), 247 deletions(-) diff --git a/msg_test.go b/msg_test.go index 5ba0dab..61c37a4 100644 --- a/msg_test.go +++ b/msg_test.go @@ -3845,51 +3845,6 @@ func TestMsg_SetBodyWriter(t *testing.T) { }) } -// checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. -// It checks whether the AddrHeader contains the correct address, name, and number of fields. -func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, - wantMail, wantName string, -) { - t.Helper() - addresses, ok := message.addrHeader[header] - if !ok { - t.Fatalf("failed to set %s, addrHeader field is not set", fn) - } - if len(addresses) != wantFields { - t.Fatalf("failed to set %s, addrHeader value count is: %d, want: %d", fn, len(addresses), field) - } - if addresses[field].Address != wantMail { - t.Errorf("failed to set %s, addrHeader value is %s, want: %s", fn, addresses[field].Address, wantMail) - } - wantString := fmt.Sprintf(`<%s>`, wantMail) - if wantName != "" { - wantString = fmt.Sprintf(`%q <%s>`, wantName, wantMail) - } - if addresses[field].String() != wantString { - t.Errorf("failed to set %s, addrHeader value is %s, want: %s", fn, addresses[field].String(), wantString) - } - if addresses[field].Name != wantName { - t.Errorf("failed to set %s, addrHeader name is %s, want: %s", fn, addresses[field].Name, wantName) - } -} - -// checkGenHeader validates the generated header in an email message, verifying its presence and expected values. -func checkGenHeader(t *testing.T, message *Msg, header Header, fn string, field, wantFields int, - wantVal string, -) { - t.Helper() - values, ok := message.genHeader[header] - if !ok { - t.Fatalf("failed to set %s, genHeader field is not set", fn) - } - if len(values) != wantFields { - t.Fatalf("failed to set %s, genHeader value count is: %d, want: %d", fn, len(values), field) - } - if values[field] != wantVal { - t.Errorf("failed to set %s, genHeader value is %s, want: %s", fn, values[field], wantVal) - } -} - /* // TestNewMsgWithMiddleware tests WithMiddleware @@ -3940,42 +3895,6 @@ func checkGenHeader(t *testing.T, message *Msg, header Header, fn string, field, } } -// TestMsg_SetBodyString tests the Msg.SetBodyString method - - func TestMsg_SetBodyString(t *testing.T) { - tests := []struct { - name string - ct ContentType - value string - want string - sf bool - }{ - {"Body: test", TypeTextPlain, "test", "test", false}, - { - "Body: with Umlauts", TypeTextHTML, "üäöß", - "üäöß", false, - }, - {"Body: with emoji", TypeTextPlain, "📧", "📧", false}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m.SetBodyString(tt.ct, tt.value) - if len(m.parts) != 1 { - t.Errorf("SetBodyString() failed: no mail parts found") - } - part := m.parts[0] - res := bytes.Buffer{} - if _, err := part.writeFunc(&res); err != nil && !tt.sf { - t.Errorf("WriteFunc of part failed: %s", err) - } - if res.String() != tt.want { - t.Errorf("SetBodyString() failed. Expecteding: %s, got: %s", tt.want, res.String()) - } - }) - } - } - // TestMsg_AddAlternativeString tests the Msg.AddAlternativeString method func TestMsg_AddAlternativeString(t *testing.T) { @@ -4055,88 +3974,6 @@ func checkGenHeader(t *testing.T, message *Msg, header Header, fn string, field, } } -// TestMsg_UnsetAllAttachments tests the Msg.UnsetAllAttachments method - - func TestMsg_UnsetAllAttachments(t *testing.T) { - tests := []struct { - name string - attachments []string - }{ - {"File: one file", []string{"README.md"}}, - {"File: two files", []string{"README.md", "doc.go"}}, - {"File: nil", nil}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var files []*File - for _, f := range tt.attachments { - files = append(files, &File{Name: f}) - } - m.SetAttachments(files) - - if len(m.attachments) != len(files) { - t.Errorf("SetAttachements() failed. Number of attachments expected: %d, got: %d", len(files), - len(m.attachments)) - return - } - m.UnsetAllAttachments() - if m.attachments != nil { - t.Errorf("UnsetAllAttachments() failed. The attachments file's pointer is not nil") - return - } - m.Reset() - }) - } - } - -// TestMsg_GetEmbeds tests the Msg.GetEmbeds method - - func TestMsg_GetEmbeds(t *testing.T) { - tests := []struct { - name string - files []string - }{ - {"File: README.md", []string{"README.md"}}, - {"File: doc.go", []string{"doc.go"}}, - {"File: README.md and doc.go", []string{"README.md", "doc.go"}}, - {"File: nonexisting", nil}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - for _, f := range tt.files { - m.EmbedFile(f, WithFileName(f), nil) - } - if len(m.embeds) != len(tt.files) { - t.Errorf("EmbedFile() failed. Number of embedded files expected: %d, got: %d", len(tt.files), - len(m.embeds)) - return - } - ff := m.GetEmbeds() - if len(m.embeds) != len(ff) { - t.Errorf("GetEmbeds() failed. Number of embedded files expected: %d, got: %d", len(m.embeds), - len(ff)) - return - } - var fn []string - for _, f := range ff { - fn = append(fn, f.Name) - } - sort.Strings(fn) - sort.Strings(tt.files) - for i, f := range tt.files { - if f != fn[i] { - t.Errorf("GetEmbeds() failed. Embedded file name expected: %s, got: %s", f, - fn[i]) - return - } - } - m.Reset() - }) - } - } - // TestMsg_SetEmbeds tests the Msg.GetEmbeds method func TestMsg_SetEmbeds(t *testing.T) { @@ -4185,90 +4022,6 @@ func checkGenHeader(t *testing.T, message *Msg, header Header, fn string, field, } } -// TestMsg_UnsetAllEmbeds tests the Msg.TestMsg_UnsetAllEmbeds method - - func TestMsg_UnsetAllEmbeds(t *testing.T) { - tests := []struct { - name string - embeds []string - }{ - {"File: one file", []string{"README.md"}}, - {"File: two files", []string{"README.md", "doc.go"}}, - {"File: nil", nil}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var files []*File - for _, f := range tt.embeds { - files = append(files, &File{Name: f}) - } - m.SetEmbeds(files) - if len(m.embeds) != len(files) { - t.Errorf("SetEmbeds() failed. Number of embedded files expected: %d, got: %d", len(files), - len(m.embeds)) - return - } - m.UnsetAllEmbeds() - if m.embeds != nil { - t.Errorf("UnsetAllEmbeds() failed. The embeds file's point is not nil") - return - } - m.Reset() - }) - } - } - -// TestMsg_UnsetAllParts tests the Msg.TestMsg_UnsetAllParts method - - func TestMsg_UnsetAllParts(t *testing.T) { - tests := []struct { - name string - attachments []string - embeds []string - }{ - {"File: both is exist", []string{"README.md"}, []string{"doc.go"}}, - {"File: both is nil", nil, nil}, - {"File: attachment exist, embed nil", []string{"README.md"}, nil}, - {"File: attachment nil, embed exist", nil, []string{"README.md"}}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var attachments []*File - for _, f := range tt.attachments { - attachments = append(attachments, &File{Name: f}) - } - m.SetAttachments(attachments) - if len(m.attachments) != len(attachments) { - t.Errorf("SetAttachements() failed. Number of attachments files expected: %d, got: %d", - len(attachments), len(m.attachments)) - return - } - var embeds []*File - for _, f := range tt.embeds { - embeds = append(embeds, &File{Name: f}) - } - m.SetEmbeds(embeds) - if len(m.embeds) != len(embeds) { - t.Errorf("SetEmbeds() failed. Number of embedded files expected: %d, got: %d", len(embeds), - len(m.embeds)) - return - } - m.UnsetAllParts() - if m.attachments != nil { - t.Errorf("UnsetAllParts() failed. The attachments file's point is not nil") - return - } - if m.embeds != nil { - t.Errorf("UnsetAllParts() failed. The embeds file's point is not nil") - return - } - m.Reset() - }) - } - } - // TestMsg_AttachFromEmbedFS tests the Msg.AttachFromEmbedFS and the WithFilename FileOption method func TestMsg_AttachFromEmbedFS(t *testing.T) { @@ -5977,3 +5730,48 @@ func (mw encodeMiddleware) Handle(m *Msg) *Msg { func (mw encodeMiddleware) Type() MiddlewareType { return "encode" } + +// checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. +// It checks whether the AddrHeader contains the correct address, name, and number of fields. +func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, + wantMail, wantName string, +) { + t.Helper() + addresses, ok := message.addrHeader[header] + if !ok { + t.Fatalf("failed to set %s, addrHeader field is not set", fn) + } + if len(addresses) != wantFields { + t.Fatalf("failed to set %s, addrHeader value count is: %d, want: %d", fn, len(addresses), field) + } + if addresses[field].Address != wantMail { + t.Errorf("failed to set %s, addrHeader value is %s, want: %s", fn, addresses[field].Address, wantMail) + } + wantString := fmt.Sprintf(`<%s>`, wantMail) + if wantName != "" { + wantString = fmt.Sprintf(`%q <%s>`, wantName, wantMail) + } + if addresses[field].String() != wantString { + t.Errorf("failed to set %s, addrHeader value is %s, want: %s", fn, addresses[field].String(), wantString) + } + if addresses[field].Name != wantName { + t.Errorf("failed to set %s, addrHeader name is %s, want: %s", fn, addresses[field].Name, wantName) + } +} + +// checkGenHeader validates the generated header in an email message, verifying its presence and expected values. +func checkGenHeader(t *testing.T, message *Msg, header Header, fn string, field, wantFields int, + wantVal string, +) { + t.Helper() + values, ok := message.genHeader[header] + if !ok { + t.Fatalf("failed to set %s, genHeader field is not set", fn) + } + if len(values) != wantFields { + t.Fatalf("failed to set %s, genHeader value count is: %d, want: %d", fn, len(values), field) + } + if values[field] != wantVal { + t.Errorf("failed to set %s, genHeader value is %s, want: %s", fn, values[field], wantVal) + } +} From 432e21f162e1efd44523b8de07aa2b5cb81c3e2c Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 14:33:00 +0100 Subject: [PATCH 134/188] 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. --- msg.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/msg.go b/msg.go index c770973..1fd97a4 100644 --- a/msg.go +++ b/msg.go @@ -1681,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 } From 35ffc9510201cc7902ebc3ee4cdc2a29b6949d04 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 14:50:14 +0100 Subject: [PATCH 135/188] 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. --- msg_test.go | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/msg_test.go b/msg_test.go index 61c37a4..e9488bc 100644 --- a/msg_test.go +++ b/msg_test.go @@ -9,6 +9,7 @@ import ( "context" "errors" "fmt" + ht "html/template" "io" "net" "reflect" @@ -3843,6 +3844,137 @@ func TestMsg_SetBodyWriter(t *testing.T) { }) } }) + t.Run("SetBodyWriter WithPartEncoding", func(t *testing.T) { + for _, tt := range encodingTests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetBodyWriter(TypeTextPlain, writerFunc, WithPartEncoding(tt.value)) + parts := message.GetParts() + if len(parts) != 1 { + t.Fatalf("expected 1 part, got: %d", len(parts)) + } + if parts[0] == nil { + t.Fatal("expected part to be not nil") + } + if parts[0].contentType != TypeTextPlain { + t.Errorf("expected contentType to be %s, got: %s", TypeTextPlain, + parts[0].contentType) + } + if parts[0].encoding != tt.value { + t.Errorf("expected encoding to be %s, got: %s", tt.value, parts[0].encoding) + } + messageBuf := bytes.NewBuffer(nil) + _, err := parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "test") { + t.Errorf("expected message body to be %s, got: %s", "test", messageBuf.String()) + } + }) + } + }) + t.Run("SetBodyWriter WithPartContentDescription", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetBodyWriter(TypeTextPlain, writerFunc, WithPartContentDescription("description")) + parts := message.GetParts() + if len(parts) != 1 { + t.Fatalf("expected 1 part, got: %d", len(parts)) + } + if parts[0] == nil { + t.Fatal("expected part to be not nil") + } + if parts[0].contentType != TypeTextPlain { + t.Errorf("expected contentType to be %s, got: %s", TypeTextPlain, + parts[0].contentType) + } + if parts[0].description != "description" { + t.Errorf("expected description to be %s, got: %s", "description", parts[0].description) + } + messageBuf := bytes.NewBuffer(nil) + _, err := parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "test") { + t.Errorf("expected message body to be %s, got: %s", "test", messageBuf.String()) + } + }) +} + +func TestMsg_SetBodyHTMLTemplate(t *testing.T) { + tplString := `

{{.teststring}}

` + invalidTplString := `

{{call $.invalid .teststring}}

` + data := map[string]interface{}{"teststring": "this is a test"} + htmlTpl, err := ht.New("htmltpl").Parse(tplString) + if err != nil { + t.Fatalf("failed to parse HTML template: %s", err) + } + invalidTpl, err := ht.New("htmltpl").Parse(invalidTplString) + if err != nil { + t.Fatalf("failed to parse invalid HTML template: %s", err) + } + t.Run("SetBodyHTMLTemplate default", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err = message.SetBodyHTMLTemplate(htmlTpl, data); err != nil { + t.Fatalf("failed to set body HTML template: %s", err) + } + parts := message.GetParts() + if len(parts) != 1 { + t.Fatalf("expected 1 part, got: %d", len(parts)) + } + if parts[0] == nil { + t.Fatal("expected part to be not nil") + } + if parts[0].contentType != TypeTextHTML { + t.Errorf("expected contentType to be %s, got: %s", TypeTextHTML, parts[0].contentType) + } + messageBuf := bytes.NewBuffer(nil) + _, err = parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "

this is a test

") { + t.Errorf("expected message body to be %s, got: %s", "

this is a test

", messageBuf.String()) + } + }) + t.Run("SetBodyHTMLTemplate with nil tpl", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err = message.SetBodyHTMLTemplate(nil, nil) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.EqualFold(err.Error(), errTplPointerNil) { + t.Errorf("expected error to be %s, got: %s", errTplPointerNil, err.Error()) + } + }) + t.Run("SetBodyHTMLTemplate with invalid tpl", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err = message.SetBodyHTMLTemplate(invalidTpl, data) + if err == nil { + t.Fatal("expected error, got nil") + } + expectErr := `failed to execute template: template: htmltpl:1:5: executing "htmltpl" at : error calling call: call of nil` + if !strings.EqualFold(err.Error(), expectErr) { + t.Errorf("expected error to be %s, got: %s", expectErr, err.Error()) + } + }) } /* From f8caa5599b0e3fe322ae7bd26c929b9ad47439cc Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 14:55:51 +0100 Subject: [PATCH 136/188] 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. --- msg.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/msg.go b/msg.go index 1fd97a4..4a4714d 100644 --- a/msg.go +++ b/msg.go @@ -1712,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 } From 4db66696a6ed004a42c3c78623750d445def7b1f Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 14:56:34 +0100 Subject: [PATCH 137/188] 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. --- msg_test.go | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/msg_test.go b/msg_test.go index e9488bc..cee2f95 100644 --- a/msg_test.go +++ b/msg_test.go @@ -15,6 +15,7 @@ import ( "reflect" "strings" "testing" + ttpl "text/template" "time" ) @@ -3977,6 +3978,75 @@ func TestMsg_SetBodyHTMLTemplate(t *testing.T) { }) } +func TestMsg_SetBodyTextTemplate(t *testing.T) { + tplString := `Teststring: {{.teststring}}` + invalidTplString := `Teststring: {{call $.invalid .teststring}}` + data := map[string]interface{}{"teststring": "this is a test"} + textTpl, err := ttpl.New("texttpl").Parse(tplString) + if err != nil { + t.Fatalf("failed to parse Text template: %s", err) + } + invalidTpl, err := ttpl.New("texttpl").Parse(invalidTplString) + if err != nil { + t.Fatalf("failed to parse invalid Text template: %s", err) + } + t.Run("SetBodyTextTemplate default", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err = message.SetBodyTextTemplate(textTpl, data); err != nil { + t.Fatalf("failed to set body text template: %s", err) + } + parts := message.GetParts() + if len(parts) != 1 { + t.Fatalf("expected 1 part, got: %d", len(parts)) + } + if parts[0] == nil { + t.Fatal("expected part to be not nil") + } + if parts[0].contentType != TypeTextPlain { + t.Errorf("expected contentType to be %s, got: %s", TypeTextPlain, parts[0].contentType) + } + messageBuf := bytes.NewBuffer(nil) + _, err = parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "Teststring: this is a test") { + t.Errorf("expected message body to be %s, got: %s", "Teststring: this is a test", messageBuf.String()) + } + }) + t.Run("SetBodyTextTemplate with nil tpl", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err = message.SetBodyTextTemplate(nil, nil) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.EqualFold(err.Error(), errTplPointerNil) { + t.Errorf("expected error to be %s, got: %s", errTplPointerNil, err.Error()) + } + }) + t.Run("SetBodyTextTemplate with invalid tpl", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err = message.SetBodyTextTemplate(invalidTpl, data) + if err == nil { + t.Fatal("expected error, got nil") + } + expectErr := `failed to execute template: template: texttpl:1:14: executing "texttpl" at : error calling call: call of nil` + if !strings.EqualFold(err.Error(), expectErr) { + t.Errorf("expected error to be %s, got: %s", expectErr, err.Error()) + } + }) +} + /* // TestNewMsgWithMiddleware tests WithMiddleware From 602f8a6e299be091e4cb0f2a5682127a053dc1df Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 15:10:01 +0100 Subject: [PATCH 138/188] 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. --- msg_test.go | 210 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) diff --git a/msg_test.go b/msg_test.go index cee2f95..951f1d0 100644 --- a/msg_test.go +++ b/msg_test.go @@ -4047,6 +4047,216 @@ func TestMsg_SetBodyTextTemplate(t *testing.T) { }) } +func TestMsg_AddAlternativeString(t *testing.T) { + t.Run("AddAlternativeString on all types", func(t *testing.T) { + for _, tt := range contentTypeTests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.AddAlternativeString(tt.ctype, "test") + parts := message.GetParts() + if len(parts) != 1 { + t.Fatalf("expected 1 part, got: %d", len(parts)) + } + if parts[0] == nil { + t.Fatal("expected part to be not nil") + } + if parts[0].contentType != tt.ctype { + t.Errorf("expected contentType to be %s, got: %s", tt.ctype, + parts[0].contentType) + } + messageBuf := bytes.NewBuffer(nil) + _, err := parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "test") { + t.Errorf("expected message body to be %s, got: %s", "test", messageBuf.String()) + } + }) + } + }) +} + +func TestMsg_AddAlternativeWriter(t *testing.T) { + writerFunc := func(w io.Writer) (int64, error) { + buffer := bytes.NewBufferString("test") + n, err := w.Write(buffer.Bytes()) + return int64(n), err + } + t.Run("AddAlternativeWriter on all types", func(t *testing.T) { + for _, tt := range contentTypeTests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.AddAlternativeWriter(tt.ctype, writerFunc) + parts := message.GetParts() + if len(parts) != 1 { + t.Fatalf("expected 1 part, got: %d", len(parts)) + } + if parts[0] == nil { + t.Fatal("expected part to be not nil") + } + if parts[0].contentType != tt.ctype { + t.Errorf("expected contentType to be %s, got: %s", tt.ctype, + parts[0].contentType) + } + messageBuf := bytes.NewBuffer(nil) + _, err := parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "test") { + t.Errorf("expected message body to be %s, got: %s", "test", messageBuf.String()) + } + }) + } + }) + t.Run("AddAlternativeWriter WithPartCharset", func(t *testing.T) { + for _, tt := range charsetTests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.AddAlternativeWriter(TypeTextPlain, writerFunc, WithPartCharset(tt.value)) + parts := message.GetParts() + if len(parts) != 1 { + t.Fatalf("expected 1 part, got: %d", len(parts)) + } + if parts[0] == nil { + t.Fatal("expected part to be not nil") + } + if parts[0].contentType != TypeTextPlain { + t.Errorf("expected contentType to be %s, got: %s", TypeTextPlain, + parts[0].contentType) + } + if parts[0].charset != tt.value { + t.Errorf("expected charset to be %s, got: %s", tt.value, parts[0].charset) + } + messageBuf := bytes.NewBuffer(nil) + _, err := parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "test") { + t.Errorf("expected message body to be %s, got: %s", "test", messageBuf.String()) + } + }) + } + }) + t.Run("AddAlternativeWriter WithPartEncoding", func(t *testing.T) { + for _, tt := range encodingTests { + t.Run(tt.name, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.AddAlternativeWriter(TypeTextPlain, writerFunc, WithPartEncoding(tt.value)) + parts := message.GetParts() + if len(parts) != 1 { + t.Fatalf("expected 1 part, got: %d", len(parts)) + } + if parts[0] == nil { + t.Fatal("expected part to be not nil") + } + if parts[0].contentType != TypeTextPlain { + t.Errorf("expected contentType to be %s, got: %s", TypeTextPlain, + parts[0].contentType) + } + if parts[0].encoding != tt.value { + t.Errorf("expected encoding to be %s, got: %s", tt.value, parts[0].encoding) + } + messageBuf := bytes.NewBuffer(nil) + _, err := parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "test") { + t.Errorf("expected message body to be %s, got: %s", "test", messageBuf.String()) + } + }) + } + }) + t.Run("AddAlternativeWriter WithPartContentDescription", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.AddAlternativeWriter(TypeTextPlain, writerFunc, WithPartContentDescription("description")) + parts := message.GetParts() + if len(parts) != 1 { + t.Fatalf("expected 1 part, got: %d", len(parts)) + } + if parts[0] == nil { + t.Fatal("expected part to be not nil") + } + if parts[0].contentType != TypeTextPlain { + t.Errorf("expected contentType to be %s, got: %s", TypeTextPlain, + parts[0].contentType) + } + if parts[0].description != "description" { + t.Errorf("expected description to be %s, got: %s", "description", parts[0].description) + } + messageBuf := bytes.NewBuffer(nil) + _, err := parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "test") { + t.Errorf("expected message body to be %s, got: %s", "test", messageBuf.String()) + } + }) + t.Run("AddAlternativeWriter with body string set", func(t *testing.T) { + writerFunc = func(w io.Writer) (int64, error) { + buffer := bytes.NewBufferString("

alternative body

") + n, err := w.Write(buffer.Bytes()) + return int64(n), err + } + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetBodyString(TypeTextPlain, "body string") + message.AddAlternativeWriter(TypeTextHTML, writerFunc) + parts := message.GetParts() + if len(parts) != 2 { + t.Fatalf("expected 1 part, got: %d", len(parts)) + } + if parts[0] == nil || parts[1] == nil { + t.Fatal("expected part to be not nil") + } + if parts[0].contentType != TypeTextPlain { + t.Errorf("expected contentType to be %s, got: %s", TypeTextPlain, + parts[0].contentType) + } + if parts[1].contentType != TypeTextHTML { + t.Errorf("expected alternative contentType to be %s, got: %s", TypeTextHTML, + parts[1].contentType) + } + messageBuf := bytes.NewBuffer(nil) + _, err := parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "body string") { + t.Errorf("expected message body to be %s, got: %s", "body string", messageBuf.String()) + } + messageBuf.Reset() + _, err = parts[1].writeFunc(messageBuf) + if err != nil { + t.Errorf("writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "

alternative body

") { + t.Errorf("expected alternative message body to be %s, got: %s", "

alternative body

", messageBuf.String()) + } + }) +} + /* // TestNewMsgWithMiddleware tests WithMiddleware From e00ddda3a33b1e6ce29343d046e83b142be95606 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 15:13:57 +0100 Subject: [PATCH 139/188] 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. --- msg_test.go | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/msg_test.go b/msg_test.go index 951f1d0..8303f87 100644 --- a/msg_test.go +++ b/msg_test.go @@ -4225,7 +4225,7 @@ func TestMsg_AddAlternativeWriter(t *testing.T) { message.AddAlternativeWriter(TypeTextHTML, writerFunc) parts := message.GetParts() if len(parts) != 2 { - t.Fatalf("expected 1 part, got: %d", len(parts)) + t.Fatalf("expected 2 part, got: %d", len(parts)) } if parts[0] == nil || parts[1] == nil { t.Fatal("expected part to be not nil") @@ -4257,6 +4257,114 @@ func TestMsg_AddAlternativeWriter(t *testing.T) { }) } +func TestMsg_AddAlternativeHTMLTemplate(t *testing.T) { + tplString := `

{{.teststring}}

` + invalidTplString := `

{{call $.invalid .teststring}}

` + data := map[string]interface{}{"teststring": "this is a test"} + htmlTpl, err := ht.New("htmltpl").Parse(tplString) + if err != nil { + t.Fatalf("failed to parse HTML template: %s", err) + } + invalidTpl, err := ht.New("htmltpl").Parse(invalidTplString) + if err != nil { + t.Fatalf("failed to parse invalid HTML template: %s", err) + } + t.Run("AddAlternativeHTMLTemplate default", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err = message.AddAlternativeHTMLTemplate(htmlTpl, data); err != nil { + t.Fatalf("failed to set body HTML template: %s", err) + } + parts := message.GetParts() + if len(parts) != 1 { + t.Fatalf("expected 1 part, got: %d", len(parts)) + } + if parts[0] == nil { + t.Fatal("expected part to be not nil") + } + if parts[0].contentType != TypeTextHTML { + t.Errorf("expected contentType to be %s, got: %s", TypeTextHTML, parts[0].contentType) + } + messageBuf := bytes.NewBuffer(nil) + _, err = parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "

this is a test

") { + t.Errorf("expected message body to be %s, got: %s", "

this is a test

", messageBuf.String()) + } + }) + t.Run("AddAlternativeHTMLTemplate with body string", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetBodyString(TypeTextPlain, "body string") + if err = message.AddAlternativeHTMLTemplate(htmlTpl, data); err != nil { + t.Fatalf("failed to set body HTML template: %s", err) + } + parts := message.GetParts() + if len(parts) != 2 { + t.Fatalf("expected 2 part, got: %d", len(parts)) + } + if parts[0] == nil || parts[1] == nil { + t.Fatal("expected part to be not nil") + } + if parts[0].contentType != TypeTextPlain { + t.Errorf("expected contentType to be %s, got: %s", TypeTextPlain, parts[0].contentType) + } + if parts[1].contentType != TypeTextHTML { + t.Errorf("expected contentType to be %s, got: %s", TypeTextHTML, parts[1].contentType) + } + messageBuf := bytes.NewBuffer(nil) + _, err = parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "body string") { + t.Errorf("expected message body to be %s, got: %s", "body string", messageBuf.String()) + } + messageBuf.Reset() + _, err = parts[1].writeFunc(messageBuf) + if err != nil { + t.Errorf("writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "

this is a test

") { + t.Errorf("expected message body to be %s, got: %s", "

this is a test

", messageBuf.String()) + } + }) + t.Run("AddAlternativeHTMLTemplate with nil tpl", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err = message.AddAlternativeHTMLTemplate(nil, nil) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.EqualFold(err.Error(), errTplPointerNil) { + t.Errorf("expected error to be %s, got: %s", errTplPointerNil, err.Error()) + } + }) + t.Run("AddAlternativeHTMLTemplate with invalid tpl", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err = message.AddAlternativeHTMLTemplate(invalidTpl, data) + if err == nil { + t.Fatal("expected error, got nil") + } + expectErr := `failed to execute template: template: htmltpl:1:5: executing "htmltpl" at : error calling call: call of nil` + if !strings.EqualFold(err.Error(), expectErr) { + t.Errorf("expected error to be %s, got: %s", expectErr, err.Error()) + } + }) +} + /* // TestNewMsgWithMiddleware tests WithMiddleware From 254dc8170609f0d07a97edda6e15617f70b31eec Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 15:16:49 +0100 Subject: [PATCH 140/188] 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. --- msg_test.go | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/msg_test.go b/msg_test.go index 8303f87..bc2a35c 100644 --- a/msg_test.go +++ b/msg_test.go @@ -4365,6 +4365,114 @@ func TestMsg_AddAlternativeHTMLTemplate(t *testing.T) { }) } +func TestMsg_AddAlternativeTextTemplate(t *testing.T) { + tplString := `Teststring: {{.teststring}}` + invalidTplString := `Teststring: {{call $.invalid .teststring}}` + data := map[string]interface{}{"teststring": "this is a test"} + textTpl, err := ttpl.New("texttpl").Parse(tplString) + if err != nil { + t.Fatalf("failed to parse Text template: %s", err) + } + invalidTpl, err := ttpl.New("texttpl").Parse(invalidTplString) + if err != nil { + t.Fatalf("failed to parse invalid Text template: %s", err) + } + t.Run("AddAlternativeTextTemplate default", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err = message.AddAlternativeTextTemplate(textTpl, data); err != nil { + t.Fatalf("failed to set body text template: %s", err) + } + parts := message.GetParts() + if len(parts) != 1 { + t.Fatalf("expected 1 part, got: %d", len(parts)) + } + if parts[0] == nil { + t.Fatal("expected part to be not nil") + } + if parts[0].contentType != TypeTextPlain { + t.Errorf("expected contentType to be %s, got: %s", TypeTextPlain, parts[0].contentType) + } + messageBuf := bytes.NewBuffer(nil) + _, err = parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "Teststring: this is a test") { + t.Errorf("expected message body to be %s, got: %s", "Teststring: this is a test", messageBuf.String()) + } + }) + t.Run("AddAlternativeTextTemplate with body string", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetBodyString(TypeTextPlain, "body string") + if err = message.AddAlternativeTextTemplate(textTpl, data); err != nil { + t.Fatalf("failed to set body text template: %s", err) + } + parts := message.GetParts() + if len(parts) != 2 { + t.Fatalf("expected 2 part, got: %d", len(parts)) + } + if parts[0] == nil || parts[1] == nil { + t.Fatal("expected part to be not nil") + } + if parts[0].contentType != TypeTextPlain { + t.Errorf("expected contentType to be %s, got: %s", TypeTextPlain, parts[0].contentType) + } + if parts[1].contentType != TypeTextPlain { + t.Errorf("expected contentType to be %s, got: %s", TypeTextPlain, parts[1].contentType) + } + messageBuf := bytes.NewBuffer(nil) + _, err = parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "body string") { + t.Errorf("expected message body to be %s, got: %s", "body string", messageBuf.String()) + } + messageBuf.Reset() + _, err = parts[1].writeFunc(messageBuf) + if err != nil { + t.Errorf("writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "Teststring: this is a test") { + t.Errorf("expected message body to be %s, got: %s", "Teststring: this is a test", messageBuf.String()) + } + }) + t.Run("AddAlternativeTextTemplate with nil tpl", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err = message.AddAlternativeTextTemplate(nil, nil) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.EqualFold(err.Error(), errTplPointerNil) { + t.Errorf("expected error to be %s, got: %s", errTplPointerNil, err.Error()) + } + }) + t.Run("AddAlternativeTextTemplate with invalid tpl", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err = message.AddAlternativeTextTemplate(invalidTpl, data) + if err == nil { + t.Fatal("expected error, got nil") + } + expectErr := `failed to execute template: template: texttpl:1:14: executing "texttpl" at : error calling call: call of nil` + if !strings.EqualFold(err.Error(), expectErr) { + t.Errorf("expected error to be %s, got: %s", expectErr, err.Error()) + } + }) +} + /* // TestNewMsgWithMiddleware tests WithMiddleware From 84f9d0583d5ea0cfc447f82b60d18fcb381abf37 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 15:17:30 +0100 Subject: [PATCH 141/188] 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. --- msg.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/msg.go b/msg.go index 4a4714d..4c42058 100644 --- a/msg.go +++ b/msg.go @@ -1786,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 } @@ -1816,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 } From 0cf636ee9b503f0d56642f72983547cbd6d07fa2 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 15:28:42 +0100 Subject: [PATCH 142/188] 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. --- msg_test.go | 77 ++++++++++++++++++++++++++--------------------------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/msg_test.go b/msg_test.go index bc2a35c..c940c84 100644 --- a/msg_test.go +++ b/msg_test.go @@ -1683,7 +1683,7 @@ func TestMsg_GetMessageID(t *testing.T) { func TestMsg_SetMessageIDWithValue(t *testing.T) { // We have already covered SetMessageIDWithValue in SetMessageID and GetMessageID - t.Skip("SetMessageIDWithValue is fully covered by TestMsg_GetMessageID") + t.Log("SetMessageIDWithValue is fully covered by TestMsg_GetMessageID") } func TestMsg_SetBulk(t *testing.T) { @@ -1775,7 +1775,8 @@ func TestMsg_SetImportance(t *testing.T) { } message.SetImportance(tt.importance) if tt.importance == ImportanceNormal { - t.Skip("ImportanceNormal is does currently not set any values") + t.Log("ImportanceNormal is does currently not set any values") + return } checkGenHeader(t, message, HeaderImportance, "SetImportance", 0, 1, tt.importance.String()) checkGenHeader(t, message, HeaderPriority, "SetImportance", 0, 1, tt.importance.NumString()) @@ -3435,7 +3436,7 @@ func TestMsg_SetAttachements(t *testing.T) { message := NewMsg() //goland:noinspection GoDeprecation message.SetAttachements(nil) - t.Skip("SetAttachements is deprecated and fully tested by SetAttachments already") + t.Log("SetAttachements is deprecated and fully tested by SetAttachments already") } func TestMsg_UnsetAllAttachments(t *testing.T) { @@ -4473,6 +4474,40 @@ func TestMsg_AddAlternativeTextTemplate(t *testing.T) { }) } +func TestMsg_AttachFile(t *testing.T) { + t.Run("AttachFile with file", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.AttachFile("testdata/attachment.txt") + attachments := message.GetAttachments() + if len(attachments) != 1 { + t.Fatalf("failed to retrieve attachments list") + } + if attachments[0] == nil { + t.Fatal("expected attachment to be not nil") + } + if attachments[0].Name != "attachment.txt" { + t.Errorf("expected attachment name to be %s, got: %s", "attachment.txt", attachments[0].Name) + } + }) + t.Run("AttachFile with non-existant file", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.AttachFile("testdata/non-existant-file.txt") + attachments := message.GetAttachments() + if len(attachments) != 0 { + t.Fatalf("failed to retrieve attachments list") + } + }) + t.Run("AttachFile with options", func(t *testing.T) { + t.Log("all options have already been tested in file_test.go") + }) +} + /* // TestNewMsgWithMiddleware tests WithMiddleware @@ -4523,42 +4558,6 @@ func TestMsg_AddAlternativeTextTemplate(t *testing.T) { } } -// TestMsg_AddAlternativeString tests the Msg.AddAlternativeString method - - func TestMsg_AddAlternativeString(t *testing.T) { - tests := []struct { - name string - value string - want string - sf bool - }{ - {"Body: test", "test", "test", false}, - {"Body: with Umlauts", "üäöß", "üäöß", false}, - {"Body: with emoji", "📧", "📧", false}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m.SetBodyString(TypeTextPlain, tt.value) - if len(m.parts) != 1 { - t.Errorf("AddAlternativeString() => SetBodyString() failed: no mail parts found") - } - m.AddAlternativeString(TypeTextHTML, tt.value) - if len(m.parts) != 2 { - t.Errorf("AddAlternativeString() failed: no alternative mail parts found") - } - apart := m.parts[1] - res := bytes.Buffer{} - if _, err := apart.writeFunc(&res); err != nil && !tt.sf { - t.Errorf("WriteFunc of part failed: %s", err) - } - if res.String() != tt.want { - t.Errorf("AddAlternativeString() failed. Expecteding: %s, got: %s", tt.want, res.String()) - } - }) - } - } - // TestMsg_AttachFile tests the Msg.AttachFile and the WithFilename FileOption method func TestMsg_AttachFile(t *testing.T) { From 946ad294cebe682fa8c82889973f056a8ed5bd85 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 15:54:39 +0100 Subject: [PATCH 143/188] 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. --- msg_nowin_test.go | 71 +++++++++++++++++++++++++++++++++++++++++++++++ msg_test.go | 67 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) diff --git a/msg_nowin_test.go b/msg_nowin_test.go index b0ef87b..e56e65e 100644 --- a/msg_nowin_test.go +++ b/msg_nowin_test.go @@ -8,12 +8,83 @@ package mail import ( + "bytes" "context" + "errors" "os" "testing" "time" ) +func TestMsg_AttachFile_unixOnly(t *testing.T) { + t.Run("AttachFile with fileFromFS fails on open", func(t *testing.T) { + tempfile, err := os.CreateTemp("", "attachfile-unable-to-open.*.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(), 0000); err != nil { + t.Fatalf("failed to chmod temp file to 0000: %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) + } + }) + t.Run("AttachFile with fileFromFS fails on copy", func(t *testing.T) { + tempfile, err := os.CreateTemp("", "attachfile-close-early.*.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) + } + }) + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.AttachFile("testdata/attachment.txt") + attachments := message.GetAttachments() + if len(attachments) != 1 { + t.Fatalf("failed to get attachments, expected 1, got: %d", len(attachments)) + } + messageBuf, err := os.Open(tempfile.Name()) + if err != nil { + t.Fatalf("failed to open temp file: %s", err) + } + // We close early to cause an error during io.Copy + if err = messageBuf.Close(); err != nil { + t.Fatalf("failed to close temp file: %s", err) + } + _, err = attachments[0].Writer(messageBuf) + if err == nil { + t.Error("writer func expected to fail, but didn't") + } + if !errors.Is(err, os.ErrClosed) { + t.Errorf("expected error to be %s, got: %s", os.ErrClosed, err) + } + }) +} + // TestMsg_WriteToSendmailWithContext tests the WriteToSendmailWithContext() method of the Msg func TestMsg_WriteToSendmailWithContext(t *testing.T) { if os.Getenv("TEST_SENDMAIL") != "true" { diff --git a/msg_test.go b/msg_test.go index c940c84..09e0465 100644 --- a/msg_test.go +++ b/msg_test.go @@ -12,6 +12,7 @@ import ( ht "html/template" "io" "net" + "os" "reflect" "strings" "testing" @@ -4491,6 +4492,15 @@ func TestMsg_AttachFile(t *testing.T) { if attachments[0].Name != "attachment.txt" { t.Errorf("expected attachment name to be %s, got: %s", "attachment.txt", attachments[0].Name) } + messageBuf := bytes.NewBuffer(nil) + _, err := attachments[0].Writer(messageBuf) + if err != nil { + t.Errorf("writer func failed: %s", err) + } + got := strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "This is a test attachment") { + t.Errorf("expected message body to be %s, got: %s", "This is a test attachment", got) + } }) t.Run("AttachFile with non-existant file", func(t *testing.T) { message := NewMsg() @@ -4508,6 +4518,63 @@ func TestMsg_AttachFile(t *testing.T) { }) } +func TestMsg_AttachReader(t *testing.T) { + t.Run("AttachReader with file", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + file, err := os.Open("testdata/attachment.txt") + if err != nil { + t.Fatalf("failed to open file: %s", err) + } + t.Cleanup(func() { + if err := file.Close(); err != nil { + t.Errorf("failed to close file: %s", err) + } + }) + if err := message.AttachReader("attachment.txt", file); err != nil { + t.Fatalf("failed to attach reader: %s", err) + } + attachments := message.GetAttachments() + if len(attachments) != 1 { + t.Fatalf("failed to retrieve attachments list") + } + if attachments[0] == nil { + t.Fatal("expected attachment to be not nil") + } + if attachments[0].Name != "attachment.txt" { + t.Errorf("expected attachment name to be %s, got: %s", "attachment.txt", attachments[0].Name) + } + messageBuf := bytes.NewBuffer(nil) + _, err = attachments[0].Writer(messageBuf) + if err != nil { + t.Errorf("writer func failed: %s", err) + } + got := strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "This is a test attachment") { + t.Errorf("expected message body to be %s, got: %s", "This is a test attachment", got) + } + }) + /* + t.Run("AttachReader with non-existant file", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.AttachReader("testdata/non-existant-file.txt") + attachments := message.GetAttachments() + if len(attachments) != 0 { + t.Fatalf("failed to retrieve attachments list") + } + }) + t.Run("AttachReader with options", func(t *testing.T) { + t.Log("all options have already been tested in file_test.go") + }) + + */ +} + /* // TestNewMsgWithMiddleware tests WithMiddleware From f7cfe5289a513e7a9cbed40d395eb83dad9ed67a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 16:08:50 +0100 Subject: [PATCH 144/188] 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. --- msg_nowin_test.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++ msg_test.go | 19 +----------------- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/msg_nowin_test.go b/msg_nowin_test.go index e56e65e..1964b41 100644 --- a/msg_nowin_test.go +++ b/msg_nowin_test.go @@ -85,6 +85,55 @@ func TestMsg_AttachFile_unixOnly(t *testing.T) { }) } +func TestMsg_AttachReader_unixOnly(t *testing.T) { + t.Run("AttachReader with fileFromReader fails on copy", func(t *testing.T) { + tempfile, err := os.CreateTemp("", "attachfile-close-early.*.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) + } + }) + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + file, err := os.Open("testdata/attachment.txt") + if err != nil { + t.Fatalf("failed to open file: %s", err) + } + t.Cleanup(func() { + if err := file.Close(); err != nil { + t.Errorf("failed to close file: %s", err) + } + }) + if err = message.AttachReader("attachment.txt", file); err != nil { + t.Fatalf("failed to attach reader: %s", err) + } + attachments := message.GetAttachments() + if len(attachments) != 1 { + t.Fatalf("failed to get attachments, expected 1, got: %d", len(attachments)) + } + messageBuf, err := os.Open(tempfile.Name()) + if err != nil { + t.Fatalf("failed to open temp file: %s", err) + } + // We close early to cause an error during io.Copy + if err = messageBuf.Close(); err != nil { + t.Fatalf("failed to close temp file: %s", err) + } + _, err = attachments[0].Writer(messageBuf) + if err == nil { + t.Error("writer func expected to fail, but didn't") + } + if !errors.Is(err, os.ErrClosed) { + t.Errorf("expected error to be %s, got: %s", os.ErrClosed, err) + } + }) +} + // TestMsg_WriteToSendmailWithContext tests the WriteToSendmailWithContext() method of the Msg func TestMsg_WriteToSendmailWithContext(t *testing.T) { if os.Getenv("TEST_SENDMAIL") != "true" { diff --git a/msg_test.go b/msg_test.go index 09e0465..0f1b656 100644 --- a/msg_test.go +++ b/msg_test.go @@ -4533,7 +4533,7 @@ func TestMsg_AttachReader(t *testing.T) { t.Errorf("failed to close file: %s", err) } }) - if err := message.AttachReader("attachment.txt", file); err != nil { + if err = message.AttachReader("attachment.txt", file); err != nil { t.Fatalf("failed to attach reader: %s", err) } attachments := message.GetAttachments() @@ -4556,23 +4556,6 @@ func TestMsg_AttachReader(t *testing.T) { t.Errorf("expected message body to be %s, got: %s", "This is a test attachment", got) } }) - /* - t.Run("AttachReader with non-existant file", func(t *testing.T) { - message := NewMsg() - if message == nil { - t.Fatal("message is nil") - } - message.AttachReader("testdata/non-existant-file.txt") - attachments := message.GetAttachments() - if len(attachments) != 0 { - t.Fatalf("failed to retrieve attachments list") - } - }) - t.Run("AttachReader with options", func(t *testing.T) { - t.Log("all options have already been tested in file_test.go") - }) - - */ } /* From 3be41b1aeaa20b4e73d13ce9fad7c67390b47966 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 16:10:46 +0100 Subject: [PATCH 145/188] 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. --- msg_nowin_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msg_nowin_test.go b/msg_nowin_test.go index 1964b41..127a7fc 100644 --- a/msg_nowin_test.go +++ b/msg_nowin_test.go @@ -27,7 +27,7 @@ func TestMsg_AttachFile_unixOnly(t *testing.T) { t.Errorf("failed to remove temp file: %s", err) } }) - if err = os.Chmod(tempfile.Name(), 0000); err != nil { + if err = os.Chmod(tempfile.Name(), 0o000); err != nil { t.Fatalf("failed to chmod temp file to 0000: %s", err) } message := NewMsg() From eed3dec7d6d5d493d86fcb4ffe759b6b3a17d604 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 16:19:16 +0100 Subject: [PATCH 146/188] 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. --- msg_nowin_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/msg_nowin_test.go b/msg_nowin_test.go index 127a7fc..4875787 100644 --- a/msg_nowin_test.go +++ b/msg_nowin_test.go @@ -79,9 +79,6 @@ func TestMsg_AttachFile_unixOnly(t *testing.T) { if err == nil { t.Error("writer func expected to fail, but didn't") } - if !errors.Is(err, os.ErrClosed) { - t.Errorf("expected error to be %s, got: %s", os.ErrClosed, err) - } }) } @@ -128,9 +125,6 @@ func TestMsg_AttachReader_unixOnly(t *testing.T) { if err == nil { t.Error("writer func expected to fail, but didn't") } - if !errors.Is(err, os.ErrClosed) { - t.Errorf("expected error to be %s, got: %s", os.ErrClosed, err) - } }) } From 89f29b241e615685d7887b2940e5d887a9406f62 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 17:49:04 +0100 Subject: [PATCH 147/188] 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. --- .github/workflows/ci.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39a0452..271f49c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,6 +136,31 @@ jobs: - 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: ['13.4', '14.0', '14.1'] + 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: Run go test on FreeBSD + uses: vmactions/freebsd-vm@v1 + - name: Run go test + with: + usesh: true + prepare: | + pkg install -y go + run: | + go test -race -shuffle=on ./... reuse: name: REUSE Compliance Check runs-on: ubuntu-latest From bdffa22ad8d76dc9fddca7650e91183c9d397494 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 17:50:36 +0100 Subject: [PATCH 148/188] 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. --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 271f49c..5917dbc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -154,7 +154,6 @@ jobs: uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master - name: Run go test on FreeBSD uses: vmactions/freebsd-vm@v1 - - name: Run go test with: usesh: true prepare: | From 7b9df7de47d6a6b3bcaf26ecf20578775f133b13 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 17:55:12 +0100 Subject: [PATCH 149/188] 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. --- .github/workflows/ci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5917dbc..1c968ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -146,10 +146,6 @@ jobs: matrix: osver: ['13.4', '14.0', '14.1'] 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: Run go test on FreeBSD From 8fb05a33ffac3a80a050a36e18feb942439d86ee Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 18:02:37 +0100 Subject: [PATCH 150/188] 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. --- msg_nowin_test.go | 4 ++-- testdata/tmp/.gitkeep | 0 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 testdata/tmp/.gitkeep diff --git a/msg_nowin_test.go b/msg_nowin_test.go index 4875787..7aed2e7 100644 --- a/msg_nowin_test.go +++ b/msg_nowin_test.go @@ -18,7 +18,7 @@ import ( func TestMsg_AttachFile_unixOnly(t *testing.T) { t.Run("AttachFile with fileFromFS fails on open", func(t *testing.T) { - tempfile, err := os.CreateTemp("", "attachfile-unable-to-open.*.txt") + tempfile, err := os.CreateTemp("testdata/tmp", "attachfile-unable-to-open.*.txt") if err != nil { t.Fatalf("failed to create temp file: %s", err) } @@ -49,7 +49,7 @@ func TestMsg_AttachFile_unixOnly(t *testing.T) { } }) t.Run("AttachFile with fileFromFS fails on copy", func(t *testing.T) { - tempfile, err := os.CreateTemp("", "attachfile-close-early.*.txt") + tempfile, err := os.CreateTemp("testdata/tmp", "attachfile-close-early.*.txt") if err != nil { t.Fatalf("failed to create temp file: %s", err) } diff --git a/testdata/tmp/.gitkeep b/testdata/tmp/.gitkeep new file mode 100644 index 0000000..e69de29 From 9682755e25bb2267768b8901c7574fe7134f86d7 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 18:03:09 +0100 Subject: [PATCH 151/188] 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. --- .cirrus.yml | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .cirrus.yml diff --git a/.cirrus.yml b/.cirrus.yml deleted file mode 100644 index 8ca7373..0000000 --- a/.cirrus.yml +++ /dev/null @@ -1,23 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Winni Neessen -# -# 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 ./... \ No newline at end of file From e74adb8b9056a797b4dc9cee61faa71e05b9b452 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 18:22:22 +0100 Subject: [PATCH 152/188] 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. --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c968ad..ed2356c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -144,7 +144,8 @@ jobs: cancel-in-progress: true strategy: matrix: - osver: ['13.4', '14.0', '14.1'] + osver: ['14.1'] + #osver: ['13.4', '14.0', '14.1'] steps: - name: Checkout Code uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master @@ -152,9 +153,11 @@ jobs: 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 From 6a43cf4aafce2c6d2fd4a0a7fd4ad5a98d208744 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 20:46:16 +0100 Subject: [PATCH 153/188] 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. --- msg_nowin_test.go => msg_unix_test.go | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) rename msg_nowin_test.go => msg_unix_test.go (90%) diff --git a/msg_nowin_test.go b/msg_unix_test.go similarity index 90% rename from msg_nowin_test.go rename to msg_unix_test.go index 7aed2e7..be702e7 100644 --- a/msg_nowin_test.go +++ b/msg_unix_test.go @@ -2,45 +2,34 @@ // // SPDX-License-Identifier: MIT -//go:build !windows -// +build !windows +//go:build linux || freebsd +// +build linux freebsd package mail import ( "bytes" - "context" "errors" "os" "testing" - "time" ) func TestMsg_AttachFile_unixOnly(t *testing.T) { t.Run("AttachFile with fileFromFS fails on open", func(t *testing.T) { - tempfile, err := os.CreateTemp("testdata/tmp", "attachfile-unable-to-open.*.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 to 0000: %s", err) - } message := NewMsg() if message == nil { t.Fatal("message is nil") } - message.AttachFile(tempfile.Name()) + // The /dev/mem device should not be readable on normal UNIX systems. We choose this + // approach over os.Chmod(0000) on a temp file, since Github runners give full access + // to the file system + message.AttachFile("/dev/mem") 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) + _, err := attachments[0].Writer(messageBuf) if err == nil { t.Error("writer func expected to fail, but didn't") } @@ -128,6 +117,7 @@ func TestMsg_AttachReader_unixOnly(t *testing.T) { }) } +/* // TestMsg_WriteToSendmailWithContext tests the WriteToSendmailWithContext() method of the Msg func TestMsg_WriteToSendmailWithContext(t *testing.T) { if os.Getenv("TEST_SENDMAIL") != "true" { @@ -197,3 +187,4 @@ func TestMsg_WriteToTempFileFailed(t *testing.T) { t.Errorf("WriteToTempFile() did not fail as expected") } } +*/ From c0c4049964e0079849d4f55a97374298fafff496 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 20:59:24 +0100 Subject: [PATCH 154/188] 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'. --- .github/workflows/ci.yml | 3 ++- msg_unix_test.go | 28 +++++++++++++++++++++------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed2356c..8d07bef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,7 @@ jobs: go: ['1.23'] env: PERFORM_ONLINE_TEST: ${{ vars.PERFORM_ONLINE_TEST }} + PERFORM_UNIX_OPEN_WRITE_TESTS: "true" TEST_SENDMAIL: ${{ vars.TEST_SENDMAIL }} TEST_HOST: ${{ secrets.TEST_HOST }} TEST_USER: ${{ secrets.TEST_USER }} @@ -145,7 +146,7 @@ jobs: strategy: matrix: osver: ['14.1'] - #osver: ['13.4', '14.0', '14.1'] + osver: ['14.1', '14.0', 13.4'] steps: - name: Checkout Code uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master diff --git a/msg_unix_test.go b/msg_unix_test.go index be702e7..f671d97 100644 --- a/msg_unix_test.go +++ b/msg_unix_test.go @@ -9,27 +9,43 @@ package mail import ( "bytes" + "context" "errors" "os" "testing" + "time" ) 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("testdata/tmp", "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") } - // The /dev/mem device should not be readable on normal UNIX systems. We choose this - // approach over os.Chmod(0000) on a temp file, since Github runners give full access - // to the file system - message.AttachFile("/dev/mem") + 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) + _, err = attachments[0].Writer(messageBuf) if err == nil { t.Error("writer func expected to fail, but didn't") } @@ -117,7 +133,6 @@ func TestMsg_AttachReader_unixOnly(t *testing.T) { }) } -/* // TestMsg_WriteToSendmailWithContext tests the WriteToSendmailWithContext() method of the Msg func TestMsg_WriteToSendmailWithContext(t *testing.T) { if os.Getenv("TEST_SENDMAIL") != "true" { @@ -187,4 +202,3 @@ func TestMsg_WriteToTempFileFailed(t *testing.T) { t.Errorf("WriteToTempFile() did not fail as expected") } } -*/ From e7e0fe03bbce4921675e6ccaccf176cb6a812ead Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 27 Oct 2024 21:00:22 +0100 Subject: [PATCH 155/188] 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. --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d07bef..798b03e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -145,7 +145,6 @@ jobs: cancel-in-progress: true strategy: matrix: - osver: ['14.1'] osver: ['14.1', '14.0', 13.4'] steps: - name: Checkout Code From 466c2892bf8980c7870fa452a66816f1d724d354 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 09:34:05 +0100 Subject: [PATCH 156/188] 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. --- msg_test.go | 38 ++++++++++++++++++++++++++++++++++++++ msg_unix_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/msg_test.go b/msg_test.go index 0f1b656..6065ba2 100644 --- a/msg_test.go +++ b/msg_test.go @@ -4558,6 +4558,44 @@ func TestMsg_AttachReader(t *testing.T) { }) } +func TestMsg_AttachReadSeeker(t *testing.T) { + t.Run("AttachReadSeeker with file", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + file, err := os.Open("testdata/attachment.txt") + if err != nil { + t.Fatalf("failed to open file: %s", err) + } + t.Cleanup(func() { + if err := file.Close(); err != nil { + t.Errorf("failed to close file: %s", err) + } + }) + message.AttachReadSeeker("attachment.txt", file) + attachments := message.GetAttachments() + if len(attachments) != 1 { + t.Fatalf("failed to retrieve attachments list") + } + if attachments[0] == nil { + t.Fatal("expected attachment to be not nil") + } + if attachments[0].Name != "attachment.txt" { + t.Errorf("expected attachment name to be %s, got: %s", "attachment.txt", attachments[0].Name) + } + messageBuf := bytes.NewBuffer(nil) + _, err = attachments[0].Writer(messageBuf) + if err != nil { + t.Errorf("writer func failed: %s", err) + } + got := strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "This is a test attachment") { + t.Errorf("expected message body to be %s, got: %s", "This is a test attachment", got) + } + }) +} + /* // TestNewMsgWithMiddleware tests WithMiddleware diff --git a/msg_unix_test.go b/msg_unix_test.go index f671d97..ae4d018 100644 --- a/msg_unix_test.go +++ b/msg_unix_test.go @@ -133,6 +133,50 @@ func TestMsg_AttachReader_unixOnly(t *testing.T) { }) } +func TestMsg_AttachReadSeeker_unixOnly(t *testing.T) { + t.Run("AttachReadSeeker with fileFromReadSeeker fails on copy", func(t *testing.T) { + tempfile, err := os.CreateTemp("", "attachfile-close-early.*.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) + } + }) + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + file, err := os.Open("testdata/attachment.txt") + if err != nil { + t.Fatalf("failed to open file: %s", err) + } + t.Cleanup(func() { + if err := file.Close(); err != nil { + t.Errorf("failed to close file: %s", err) + } + }) + message.AttachReadSeeker("attachment.txt", file) + attachments := message.GetAttachments() + if len(attachments) != 1 { + t.Fatalf("failed to get attachments, expected 1, got: %d", len(attachments)) + } + messageBuf, err := os.Open(tempfile.Name()) + if err != nil { + t.Fatalf("failed to open temp file: %s", err) + } + // We close early to cause an error during io.Copy + if err = messageBuf.Close(); err != nil { + t.Fatalf("failed to close temp file: %s", err) + } + _, err = attachments[0].Writer(messageBuf) + if err == nil { + t.Error("writer func expected to fail, but didn't") + } + }) +} + // TestMsg_WriteToSendmailWithContext tests the WriteToSendmailWithContext() method of the Msg func TestMsg_WriteToSendmailWithContext(t *testing.T) { if os.Getenv("TEST_SENDMAIL") != "true" { From f576b92ce2a0d80015ce029ee038e7c93951b6bb Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 11:30:01 +0100 Subject: [PATCH 157/188] 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. --- codecov.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/codecov.yml b/codecov.yml index a7460c1..a9f998e 100644 --- a/codecov.yml +++ b/codecov.yml @@ -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 From e779777c9b709d17cf8010027114bac20d1c5182 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 11:30:34 +0100 Subject: [PATCH 158/188] 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. --- msg_test.go | 180 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 177 insertions(+), 3 deletions(-) diff --git a/msg_test.go b/msg_test.go index 6065ba2..9cab403 100644 --- a/msg_test.go +++ b/msg_test.go @@ -7,6 +7,7 @@ package mail import ( "bytes" "context" + "embed" "errors" "fmt" ht "html/template" @@ -135,10 +136,8 @@ var ( } ) -/* -//go:embed README.md +//go:embed testdata/attachment.txt var efs embed.FS -*/ func TestNewMsg(t *testing.T) { t.Run("create new message", func(t *testing.T) { @@ -4596,6 +4595,181 @@ func TestMsg_AttachReadSeeker(t *testing.T) { }) } +func TestMsg_AttachHTMLTemplate(t *testing.T) { + tplString := `

{{.teststring}}

` + invalidTplString := `

{{call $.invalid .teststring}}

` + data := map[string]interface{}{"teststring": "this is a test"} + htmlTpl, err := ht.New("htmltpl").Parse(tplString) + if err != nil { + t.Fatalf("failed to parse HTML template: %s", err) + } + invalidTpl, err := ht.New("htmltpl").Parse(invalidTplString) + if err != nil { + t.Fatalf("failed to parse invalid HTML template: %s", err) + } + t.Run("AttachHTMLTemplate with valid template", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err = message.AttachHTMLTemplate("attachment.html", htmlTpl, data); err != nil { + t.Fatalf("failed to set body HTML template: %s", err) + } + attachments := message.GetAttachments() + if len(attachments) != 1 { + t.Fatalf("failed to retrieve attachments list") + } + if attachments[0] == nil { + t.Fatal("expected attachment to be not nil") + } + if attachments[0].Name != "attachment.html" { + t.Errorf("expected attachment name to be %s, got: %s", "attachment.html", attachments[0].Name) + } + messageBuf := bytes.NewBuffer(nil) + _, err = attachments[0].Writer(messageBuf) + if err != nil { + t.Errorf("writer func failed: %s", err) + } + got := strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "

this is a test

") { + t.Errorf("expected message body to be %s, got: %s", "

this is a test

", got) + } + }) + t.Run("AttachHTMLTemplate with invalid template", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err = message.AttachHTMLTemplate("attachment.html", invalidTpl, data) + if err == nil { + t.Fatal("expected error, got nil") + } + expectErr := `failed to attach template: failed to execute template: template: htmltpl:1:5: executing "htmltpl" ` + + `at : error calling call: call of nil` + if !strings.EqualFold(err.Error(), expectErr) { + t.Errorf("expected error to be %s, got: %s", expectErr, err.Error()) + } + }) + t.Run("AttachHTMLTemplate with nil template", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err = message.AttachHTMLTemplate("attachment.html", nil, data) + if err == nil { + t.Fatal("expected error, got nil") + } + expectedErr := `failed to attach template: ` + errTplPointerNil + if !strings.EqualFold(err.Error(), expectedErr) { + t.Errorf("expected error to be %s, got: %s", expectedErr, err.Error()) + } + }) +} + +func TestMsg_AttachTextTemplate(t *testing.T) { + tplString := `Teststring: {{.teststring}}` + invalidTplString := `Teststring: {{call $.invalid .teststring}}` + data := map[string]interface{}{"teststring": "this is a test"} + textTpl, err := ttpl.New("texttpl").Parse(tplString) + if err != nil { + t.Fatalf("failed to parse Text template: %s", err) + } + invalidTpl, err := ttpl.New("texttpl").Parse(invalidTplString) + if err != nil { + t.Fatalf("failed to parse invalid Text template: %s", err) + } + t.Run("AttachTextTemplate with valid template", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err = message.AttachTextTemplate("attachment.txt", textTpl, data); err != nil { + t.Fatalf("failed to set body HTML template: %s", err) + } + attachments := message.GetAttachments() + if len(attachments) != 1 { + t.Fatalf("failed to retrieve attachments list") + } + if attachments[0] == nil { + t.Fatal("expected attachment to be not nil") + } + if attachments[0].Name != "attachment.txt" { + t.Errorf("expected attachment name to be %s, got: %s", "attachment.txt", attachments[0].Name) + } + messageBuf := bytes.NewBuffer(nil) + _, err = attachments[0].Writer(messageBuf) + if err != nil { + t.Errorf("writer func failed: %s", err) + } + got := strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "Teststring: this is a test") { + t.Errorf("expected message body to be %s, got: %s", "Teststring: this is a test", got) + } + }) + t.Run("AttachTextTemplate with invalid template", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err = message.AttachTextTemplate("attachment.txt", invalidTpl, data) + if err == nil { + t.Fatal("expected error, got nil") + } + expectErr := `failed to attach template: failed to execute template: template: texttpl:1:14: executing "texttpl" ` + + `at : error calling call: call of nil` + if !strings.EqualFold(err.Error(), expectErr) { + t.Errorf("expected error to be %s, got: %s", expectErr, err.Error()) + } + }) + t.Run("AttachTextTemplate with nil template", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err = message.AttachTextTemplate("attachment.html", nil, data) + if err == nil { + t.Fatal("expected error, got nil") + } + expectedErr := `failed to attach template: ` + errTplPointerNil + if !strings.EqualFold(err.Error(), expectedErr) { + t.Errorf("expected error to be %s, got: %s", expectedErr, err.Error()) + } + }) +} + +func TestMsg_AttachFromEmbedFS(t *testing.T) { + t.Run("AttachFromEmbedFS successful", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.AttachFromEmbedFS("testdata/attachment.txt", &efs, + WithFileName("attachment.txt")); err != nil { + t.Fatalf("failed to attach from embed FS: %s", err) + } + attachments := message.GetAttachments() + if len(attachments) != 1 { + t.Fatalf("failed to retrieve attachments list") + } + if attachments[0] == nil { + t.Fatal("expected attachment to be not nil") + } + if attachments[0].Name != "attachment.txt" { + t.Errorf("expected attachment name to be %s, got: %s", "attachment.txt", attachments[0].Name) + } + messageBuf := bytes.NewBuffer(nil) + _, err := attachments[0].Writer(messageBuf) + if err != nil { + t.Errorf("writer func failed: %s", err) + } + got := strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "This is a test attachment") { + t.Errorf("expected message body to be %s, got: %s", "This is a test attachment", got) + } + + }) +} + /* // TestNewMsgWithMiddleware tests WithMiddleware From d02f4696588aab63980ac099e72f1d91380e81e3 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 11:35:05 +0100 Subject: [PATCH 159/188] 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. --- msg_test.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/msg_test.go b/msg_test.go index 9cab403..80bec04 100644 --- a/msg_test.go +++ b/msg_test.go @@ -4766,8 +4766,28 @@ func TestMsg_AttachFromEmbedFS(t *testing.T) { if !strings.EqualFold(got, "This is a test attachment") { t.Errorf("expected message body to be %s, got: %s", "This is a test attachment", got) } - }) + t.Run("AttachFromEmbedFS with invalid path", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err := message.AttachFromEmbedFS("testdata/invalid.txt", &efs, WithFileName("attachment.txt")) + if err == nil { + t.Fatal("expected error, got nil") + } + }) + t.Run("AttachFromEmbedFS with nil embed FS", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err := message.AttachFromEmbedFS("testdata/invalid.txt", nil, WithFileName("attachment.txt")) + if err == nil { + t.Fatal("expected error, got nil") + } + }) + } /* From ae7b6a68c5254769746179f0d01d303554a97398 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 11:37:04 +0100 Subject: [PATCH 160/188] 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. --- msg_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/msg_test.go b/msg_test.go index 80bec04..b353558 100644 --- a/msg_test.go +++ b/msg_test.go @@ -4787,7 +4787,49 @@ func TestMsg_AttachFromEmbedFS(t *testing.T) { t.Fatal("expected error, got nil") } }) +} +func TestMsg_EmbedFile(t *testing.T) { + t.Run("EmbedFile with file", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.EmbedFile("testdata/embed.txt") + embeds := message.GetEmbeds() + if len(embeds) != 1 { + t.Fatalf("failed to retrieve embeds list") + } + if embeds[0] == nil { + t.Fatal("expected embed to be not nil") + } + if embeds[0].Name != "embed.txt" { + t.Errorf("expected embed name to be %s, got: %s", "embed.txt", embeds[0].Name) + } + messageBuf := bytes.NewBuffer(nil) + _, err := embeds[0].Writer(messageBuf) + if err != nil { + t.Errorf("writer func failed: %s", err) + } + got := strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "This is a test embed") { + t.Errorf("expected message body to be %s, got: %s", "This is a test embed", got) + } + }) + t.Run("EmbedFile with non-existant file", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.EmbedFile("testdata/non-existant-file.txt") + embeds := message.GetEmbeds() + if len(embeds) != 0 { + t.Fatalf("failed to retrieve attachments list") + } + }) + t.Run("EmbedFile with options", func(t *testing.T) { + t.Log("all options have already been tested in file_test.go") + }) } /* From c395c83a06f6973c0f89668282e6956b64e236ec Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 12:01:31 +0100 Subject: [PATCH 161/188] 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. --- msg_test.go | 40 ++++++++++++++ msg_unix_test.go | 138 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+) diff --git a/msg_test.go b/msg_test.go index b353558..b50edf9 100644 --- a/msg_test.go +++ b/msg_test.go @@ -4832,6 +4832,46 @@ func TestMsg_EmbedFile(t *testing.T) { }) } +func TestMsg_EmbedReader(t *testing.T) { + t.Run("EmbedReader with file", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + file, err := os.Open("testdata/embed.txt") + if err != nil { + t.Fatalf("failed to open file: %s", err) + } + t.Cleanup(func() { + if err := file.Close(); err != nil { + t.Errorf("failed to close file: %s", err) + } + }) + if err = message.EmbedReader("embed.txt", file); err != nil { + t.Fatalf("failed to embed reader: %s", err) + } + embeds := message.GetEmbeds() + if len(embeds) != 1 { + t.Fatalf("failed to retrieve embeds list") + } + if embeds[0] == nil { + t.Fatal("expected embed to be not nil") + } + if embeds[0].Name != "embed.txt" { + t.Errorf("expected embed name to be %s, got: %s", "embed.txt", embeds[0].Name) + } + messageBuf := bytes.NewBuffer(nil) + _, err = embeds[0].Writer(messageBuf) + if err != nil { + t.Errorf("writer func failed: %s", err) + } + got := strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "This is a test embed") { + t.Errorf("expected message body to be %s, got: %s", "This is a test embed", got) + } + }) +} + /* // TestNewMsgWithMiddleware tests WithMiddleware diff --git a/msg_unix_test.go b/msg_unix_test.go index ae4d018..0200470 100644 --- a/msg_unix_test.go +++ b/msg_unix_test.go @@ -177,6 +177,144 @@ func TestMsg_AttachReadSeeker_unixOnly(t *testing.T) { }) } +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("testdata/tmp", "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) + } + }) + t.Run("EmbedFile with fileFromFS fails on copy", func(t *testing.T) { + tempfile, err := os.CreateTemp("testdata/tmp", "embedfile-close-early.*.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) + } + }) + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.EmbedFile("testdata/embed.txt") + emebeds := message.GetEmbeds() + if len(emebeds) != 1 { + t.Fatalf("failed to get emebeds, expected 1, got: %d", len(emebeds)) + } + messageBuf, err := os.Open(tempfile.Name()) + if err != nil { + t.Fatalf("failed to open temp file: %s", err) + } + // We close early to cause an error during io.Copy + if err = messageBuf.Close(); err != nil { + t.Fatalf("failed to close temp file: %s", err) + } + _, err = emebeds[0].Writer(messageBuf) + if err == nil { + t.Error("writer func expected to fail, but didn't") + } + }) +} + +func TestMsg_EmbedReader_unixOnly(t *testing.T) { + t.Run("EmbedReader with fileFromReader fails on copy", func(t *testing.T) { + tempfile, err := os.CreateTemp("", "embedfile-close-early.*.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) + } + }) + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + file, err := os.Open("testdata/embed.txt") + if err != nil { + t.Fatalf("failed to open file: %s", err) + } + t.Cleanup(func() { + if err := file.Close(); err != nil { + t.Errorf("failed to close file: %s", err) + } + }) + if err = message.EmbedReader("embed.txt", file); err != nil { + t.Fatalf("failed to embed reader: %s", err) + } + embeds := message.GetEmbeds() + if len(embeds) != 1 { + t.Fatalf("failed to get embeds, expected 1, got: %d", len(embeds)) + } + messageBuf, err := os.Open(tempfile.Name()) + if err != nil { + t.Fatalf("failed to open temp file: %s", err) + } + // We close early to cause an error during io.Copy + if err = messageBuf.Close(); err != nil { + t.Fatalf("failed to close temp file: %s", err) + } + _, err = embeds[0].Writer(messageBuf) + if err == nil { + t.Error("writer func expected to fail, but didn't") + } + }) + t.Run("EmbedReader with fileFromReader on closed reader", func(t *testing.T) { + tempfile, err := os.CreateTemp("", "embedfile-close-early.*.txt") + if err != nil { + t.Fatalf("failed to create temp file: %s", err) + } + if err = tempfile.Close(); err != nil { + t.Fatalf("failed to close temp file: %s", err) + } + t.Cleanup(func() { + if err := os.Remove(tempfile.Name()); err != nil { + t.Errorf("failed to remove temp file: %s", err) + } + }) + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err = message.EmbedReader("embed.txt", tempfile); err == nil { + t.Fatalf("expected error, got nil") + } + }) +} + // TestMsg_WriteToSendmailWithContext tests the WriteToSendmailWithContext() method of the Msg func TestMsg_WriteToSendmailWithContext(t *testing.T) { if os.Getenv("TEST_SENDMAIL") != "true" { From f48ff6e1502e8b6e2e749ea02c7d23b0e1858683 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 12:17:20 +0100 Subject: [PATCH 162/188] 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. --- msg_test.go | 234 ++++++++++++++++++++++++++++++++++++++++++++++- msg_unix_test.go | 44 +++++++++ 2 files changed, 277 insertions(+), 1 deletion(-) diff --git a/msg_test.go b/msg_test.go index b50edf9..7b52688 100644 --- a/msg_test.go +++ b/msg_test.go @@ -136,7 +136,7 @@ var ( } ) -//go:embed testdata/attachment.txt +//go:embed testdata/attachment.txt testdata/embed.txt var efs embed.FS func TestNewMsg(t *testing.T) { @@ -4872,6 +4872,238 @@ func TestMsg_EmbedReader(t *testing.T) { }) } +func TestMsg_EmbedReadSeeker(t *testing.T) { + t.Run("EmbedReadSeeker with file", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + file, err := os.Open("testdata/embed.txt") + if err != nil { + t.Fatalf("failed to open file: %s", err) + } + t.Cleanup(func() { + if err := file.Close(); err != nil { + t.Errorf("failed to close file: %s", err) + } + }) + message.EmbedReadSeeker("embed.txt", file) + embeds := message.GetEmbeds() + if len(embeds) != 1 { + t.Fatalf("failed to retrieve embeds list") + } + if embeds[0] == nil { + t.Fatal("expected embed to be not nil") + } + if embeds[0].Name != "embed.txt" { + t.Errorf("expected embed name to be %s, got: %s", "embed.txt", embeds[0].Name) + } + messageBuf := bytes.NewBuffer(nil) + _, err = embeds[0].Writer(messageBuf) + if err != nil { + t.Errorf("writer func failed: %s", err) + } + got := strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "This is a test embed") { + t.Errorf("expected message body to be %s, got: %s", "This is a test embed", got) + } + }) +} + +func TestMsg_EmbedHTMLTemplate(t *testing.T) { + tplString := `

{{.teststring}}

` + invalidTplString := `

{{call $.invalid .teststring}}

` + data := map[string]interface{}{"teststring": "this is a test"} + htmlTpl, err := ht.New("htmltpl").Parse(tplString) + if err != nil { + t.Fatalf("failed to parse HTML template: %s", err) + } + invalidTpl, err := ht.New("htmltpl").Parse(invalidTplString) + if err != nil { + t.Fatalf("failed to parse invalid HTML template: %s", err) + } + t.Run("EmbedHTMLTemplate with valid template", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err = message.EmbedHTMLTemplate("embed.html", htmlTpl, data); err != nil { + t.Fatalf("failed to set body HTML template: %s", err) + } + embeds := message.GetEmbeds() + if len(embeds) != 1 { + t.Fatalf("failed to retrieve embeds list") + } + if embeds[0] == nil { + t.Fatal("expected embed to be not nil") + } + if embeds[0].Name != "embed.html" { + t.Errorf("expected embed name to be %s, got: %s", "embed.html", embeds[0].Name) + } + messageBuf := bytes.NewBuffer(nil) + _, err = embeds[0].Writer(messageBuf) + if err != nil { + t.Errorf("writer func failed: %s", err) + } + got := strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "

this is a test

") { + t.Errorf("expected message body to be %s, got: %s", "

this is a test

", got) + } + }) + t.Run("EmbedHTMLTemplate with invalid template", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err = message.EmbedHTMLTemplate("embed.html", invalidTpl, data) + if err == nil { + t.Fatal("expected error, got nil") + } + expectErr := `failed to embed template: failed to execute template: template: htmltpl:1:5: executing "htmltpl" ` + + `at : error calling call: call of nil` + if !strings.EqualFold(err.Error(), expectErr) { + t.Errorf("expected error to be %s, got: %s", expectErr, err.Error()) + } + }) + t.Run("EmbedHTMLTemplate with nil template", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err = message.EmbedHTMLTemplate("embed.html", nil, data) + if err == nil { + t.Fatal("expected error, got nil") + } + expectedErr := `failed to embed template: ` + errTplPointerNil + if !strings.EqualFold(err.Error(), expectedErr) { + t.Errorf("expected error to be %s, got: %s", expectedErr, err.Error()) + } + }) +} + +func TestMsg_EmbedTextTemplate(t *testing.T) { + tplString := `Teststring: {{.teststring}}` + invalidTplString := `Teststring: {{call $.invalid .teststring}}` + data := map[string]interface{}{"teststring": "this is a test"} + textTpl, err := ttpl.New("texttpl").Parse(tplString) + if err != nil { + t.Fatalf("failed to parse Text template: %s", err) + } + invalidTpl, err := ttpl.New("texttpl").Parse(invalidTplString) + if err != nil { + t.Fatalf("failed to parse invalid Text template: %s", err) + } + t.Run("EmbedTextTemplate with valid template", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err = message.EmbedTextTemplate("embed.txt", textTpl, data); err != nil { + t.Fatalf("failed to set body HTML template: %s", err) + } + embeds := message.GetEmbeds() + if len(embeds) != 1 { + t.Fatalf("failed to retrieve embeds list") + } + if embeds[0] == nil { + t.Fatal("expected embed to be not nil") + } + if embeds[0].Name != "embed.txt" { + t.Errorf("expected embed name to be %s, got: %s", "embed.txt", embeds[0].Name) + } + messageBuf := bytes.NewBuffer(nil) + _, err = embeds[0].Writer(messageBuf) + if err != nil { + t.Errorf("writer func failed: %s", err) + } + got := strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "Teststring: this is a test") { + t.Errorf("expected message body to be %s, got: %s", "Teststring: this is a test", got) + } + }) + t.Run("EmbedTextTemplate with invalid template", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err = message.EmbedTextTemplate("embed.txt", invalidTpl, data) + if err == nil { + t.Fatal("expected error, got nil") + } + expectErr := `failed to embed template: failed to execute template: template: texttpl:1:14: executing "texttpl" ` + + `at : error calling call: call of nil` + if !strings.EqualFold(err.Error(), expectErr) { + t.Errorf("expected error to be %s, got: %s", expectErr, err.Error()) + } + }) + t.Run("EmbedTextTemplate with nil template", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err = message.EmbedTextTemplate("embed.html", nil, data) + if err == nil { + t.Fatal("expected error, got nil") + } + expectedErr := `failed to embed template: ` + errTplPointerNil + if !strings.EqualFold(err.Error(), expectedErr) { + t.Errorf("expected error to be %s, got: %s", expectedErr, err.Error()) + } + }) +} + +func TestMsg_EmbedFromEmbedFS(t *testing.T) { + t.Run("EmbedFromEmbedFS successful", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.EmbedFromEmbedFS("testdata/embed.txt", &efs, + WithFileName("embed.txt")); err != nil { + t.Fatalf("failed to embed from embed FS: %s", err) + } + embeds := message.GetEmbeds() + if len(embeds) != 1 { + t.Fatalf("failed to retrieve embeds list") + } + if embeds[0] == nil { + t.Fatal("expected embed to be not nil") + } + if embeds[0].Name != "embed.txt" { + t.Errorf("expected embed name to be %s, got: %s", "embed.txt", embeds[0].Name) + } + messageBuf := bytes.NewBuffer(nil) + _, err := embeds[0].Writer(messageBuf) + if err != nil { + t.Errorf("writer func failed: %s", err) + } + got := strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "This is a test embed") { + t.Errorf("expected message body to be %s, got: %s", "This is a test embed", got) + } + }) + t.Run("EmbedFromEmbedFS with invalid path", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err := message.EmbedFromEmbedFS("testdata/invalid.txt", &efs, WithFileName("embed.txt")) + if err == nil { + t.Fatal("expected error, got nil") + } + }) + t.Run("EmbedFromEmbedFS with nil embed FS", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err := message.EmbedFromEmbedFS("testdata/invalid.txt", nil, WithFileName("embed.txt")) + if err == nil { + t.Fatal("expected error, got nil") + } + }) +} + /* // TestNewMsgWithMiddleware tests WithMiddleware diff --git a/msg_unix_test.go b/msg_unix_test.go index 0200470..eec15ff 100644 --- a/msg_unix_test.go +++ b/msg_unix_test.go @@ -315,6 +315,50 @@ func TestMsg_EmbedReader_unixOnly(t *testing.T) { }) } +func TestMsg_EmbedReadSeeker_unixOnly(t *testing.T) { + t.Run("EmbedReadSeeker with fileFromReadSeeker fails on copy", func(t *testing.T) { + tempfile, err := os.CreateTemp("", "embedfile-close-early.*.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) + } + }) + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + file, err := os.Open("testdata/embed.txt") + if err != nil { + t.Fatalf("failed to open file: %s", err) + } + t.Cleanup(func() { + if err := file.Close(); err != nil { + t.Errorf("failed to close file: %s", err) + } + }) + message.EmbedReadSeeker("embed.txt", file) + embeds := message.GetEmbeds() + if len(embeds) != 1 { + t.Fatalf("failed to get embeds, expected 1, got: %d", len(embeds)) + } + messageBuf, err := os.Open(tempfile.Name()) + if err != nil { + t.Fatalf("failed to open temp file: %s", err) + } + // We close early to cause an error during io.Copy + if err = messageBuf.Close(); err != nil { + t.Fatalf("failed to close temp file: %s", err) + } + _, err = embeds[0].Writer(messageBuf) + if err == nil { + t.Error("writer func expected to fail, but didn't") + } + }) +} + // TestMsg_WriteToSendmailWithContext tests the WriteToSendmailWithContext() method of the Msg func TestMsg_WriteToSendmailWithContext(t *testing.T) { if os.Getenv("TEST_SENDMAIL") != "true" { From 80bf7240b4225e11736f57fc821015e8c3f41ef9 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 12:21:58 +0100 Subject: [PATCH 163/188] 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. --- msg_test.go | 342 ---------------------------------------------------- 1 file changed, 342 deletions(-) diff --git a/msg_test.go b/msg_test.go index 7b52688..78382e8 100644 --- a/msg_test.go +++ b/msg_test.go @@ -5154,348 +5154,6 @@ func TestMsg_EmbedFromEmbedFS(t *testing.T) { } } -// TestMsg_AttachFile tests the Msg.AttachFile and the WithFilename FileOption method - - func TestMsg_AttachFile(t *testing.T) { - tests := []struct { - name string - file string - fn string - sf bool - }{ - {"File: README.md", "README.md", "README.md", false}, - {"File: doc.go", "doc.go", "foo.go", false}, - {"File: nonexisting", "", "invalid.file", true}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m.AttachFile(tt.file, WithFileName(tt.fn), nil) - if len(m.attachments) != 1 && !tt.sf { - t.Errorf("AttachFile() failed. Number of attachments expected: %d, got: %d", 1, - len(m.attachments)) - return - } - if !tt.sf { - file := m.attachments[0] - if file == nil { - t.Errorf("AttachFile() failed. Attachment file pointer is nil") - return - } - if file.Name != tt.fn { - t.Errorf("AttachFile() failed. Filename of attachment expected: %s, got: %s", tt.fn, - file.Name) - } - buf := bytes.Buffer{} - if _, err := file.Writer(&buf); err != nil { - t.Errorf("failed to execute WriterFunc: %s", err) - return - } - } - m.Reset() - }) - } - } - -// TestMsg_SetEmbeds tests the Msg.GetEmbeds method - - func TestMsg_SetEmbeds(t *testing.T) { - tests := []struct { - name string - embeds []string - files []string - }{ - {"File: replace README.md with doc.go", []string{"README.md"}, []string{"doc.go"}}, - {"File: add README.md with doc.go ", []string{"doc.go"}, []string{"README.md", "doc.go"}}, - {"File: remove README.md and doc.go", []string{"README.md", "doc.go"}, nil}, - {"File: add README.md and doc.go", nil, []string{"README.md", "doc.go"}}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - sort.Strings(tt.embeds) - sort.Strings(tt.files) - for _, a := range tt.embeds { - m.EmbedFile(a, WithFileName(a), nil) - } - if len(m.embeds) != len(tt.embeds) { - t.Errorf("EmbedFile() failed. Number of embedded files expected: %d, got: %d", len(tt.files), - len(m.embeds)) - return - } - var files []*File - for _, f := range tt.files { - files = append(files, &File{Name: f}) - } - m.SetEmbeds(files) - if len(m.embeds) != len(files) { - t.Errorf("SetEmbeds() failed. Number of embedded files expected: %d, got: %d", len(files), - len(m.embeds)) - return - } - for i, f := range tt.files { - if f != m.embeds[i].Name { - t.Errorf("SetEmbeds() failed. Embedded file name expected: %s, got: %s", f, - m.embeds[i].Name) - return - } - } - m.Reset() - }) - } - } - -// TestMsg_AttachFromEmbedFS tests the Msg.AttachFromEmbedFS and the WithFilename FileOption method - - func TestMsg_AttachFromEmbedFS(t *testing.T) { - tests := []struct { - name string - file string - fn string - sf bool - }{ - {"File: README.md", "README.md", "README.md", false}, - {"File: nonexisting", "", "invalid.file", true}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := m.AttachFromEmbedFS(tt.file, &efs, WithFileName(tt.fn)); err != nil && !tt.sf { - t.Errorf("AttachFromEmbedFS() failed: %s", err) - return - } - if len(m.attachments) != 1 && !tt.sf { - t.Errorf("AttachFile() failed. Number of attachments expected: %d, got: %d", 1, - len(m.attachments)) - return - } - if !tt.sf { - file := m.attachments[0] - if file == nil { - t.Errorf("AttachFile() failed. Attachment file pointer is nil") - return - } - if file.Name != tt.fn { - t.Errorf("AttachFile() failed. Filename of attachment expected: %s, got: %s", tt.fn, - file.Name) - } - buf := bytes.Buffer{} - if _, err := file.Writer(&buf); err != nil { - t.Errorf("failed to execute WriterFunc: %s", err) - return - } - } - m.Reset() - }) - } - } - -// TestMsg_AttachFileBrokenFunc tests WriterFunc of the Msg.AttachFile method - - func TestMsg_AttachFileBrokenFunc(t *testing.T) { - m := NewMsg() - m.AttachFile("README.md") - if len(m.attachments) != 1 { - t.Errorf("AttachFile() failed. Number of attachments expected: %d, got: %d", 1, - len(m.attachments)) - return - } - file := m.attachments[0] - if file == nil { - t.Errorf("AttachFile() failed. Attachment file pointer is nil") - return - } - file.Writer = func(io.Writer) (int64, error) { - return 0, fmt.Errorf("failing intentionally") - } - buf := bytes.Buffer{} - if _, err := file.Writer(&buf); err == nil { - t.Errorf("execute WriterFunc did not fail, but was expected to fail") - } - } - -// TestMsg_AttachReader tests the Msg.AttachReader method - - func TestMsg_AttachReader(t *testing.T) { - m := NewMsg() - ts := "This is a test string" - rbuf := bytes.Buffer{} - rbuf.WriteString(ts) - r := bufio.NewReader(&rbuf) - if err := m.AttachReader("testfile.txt", r); err != nil { - t.Errorf("AttachReader() failed. Expected no error, got: %s", err.Error()) - return - } - if len(m.attachments) != 1 { - t.Errorf("AttachReader() failed. Number of attachments expected: %d, got: %d", 1, - len(m.attachments)) - return - } - file := m.attachments[0] - if file == nil { - t.Errorf("AttachReader() failed. Attachment file pointer is nil") - return - } - if file.Name != "testfile.txt" { - t.Errorf("AttachReader() failed. Expected file name: %s, got: %s", "testfile.txt", - file.Name) - } - wbuf := bytes.Buffer{} - if _, err := file.Writer(&wbuf); err != nil { - t.Errorf("execute WriterFunc failed: %s", err) - } - if wbuf.String() != ts { - t.Errorf("AttachReader() failed. Expected string: %q, got: %q", ts, wbuf.String()) - } - } - -// TestMsg_EmbedFile tests the Msg.EmbedFile and the WithFilename FileOption method - - func TestMsg_EmbedFile(t *testing.T) { - tests := []struct { - name string - file string - fn string - sf bool - }{ - {"File: README.md", "README.md", "README.md", false}, - {"File: doc.go", "doc.go", "foo.go", false}, - {"File: nonexisting", "", "invalid.file", true}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m.EmbedFile(tt.file, WithFileName(tt.fn), nil) - if len(m.embeds) != 1 && !tt.sf { - t.Errorf("EmbedFile() failed. Number of embeds expected: %d, got: %d", 1, - len(m.embeds)) - return - } - if !tt.sf { - file := m.embeds[0] - if file == nil { - t.Errorf("EmbedFile() failed. Embedded file pointer is nil") - return - } - if file.Name != tt.fn { - t.Errorf("EmbedFile() failed. Filename of embeds expected: %s, got: %s", tt.fn, - file.Name) - } - buf := bytes.Buffer{} - if _, err := file.Writer(&buf); err != nil { - t.Errorf("failed to execute WriterFunc: %s", err) - return - } - } - m.Reset() - }) - } - } - -// TestMsg_EmbedFromEmbedFS tests the Msg.EmbedFromEmbedFS and the WithFilename FileOption method - - func TestMsg_EmbedFromEmbedFS(t *testing.T) { - tests := []struct { - name string - file string - fn string - sf bool - }{ - {"File: README.md", "README.md", "README.md", false}, - {"File: nonexisting", "", "invalid.file", true}, - } - m := NewMsg() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := m.EmbedFromEmbedFS(tt.file, &efs, WithFileName(tt.fn)); err != nil && !tt.sf { - t.Errorf("EmbedFromEmbedFS() failed: %s", err) - return - } - if len(m.embeds) != 1 && !tt.sf { - t.Errorf("EmbedFile() failed. Number of embeds expected: %d, got: %d", 1, - len(m.embeds)) - return - } - if !tt.sf { - file := m.embeds[0] - if file == nil { - t.Errorf("EmbedFile() failed. Embedded file pointer is nil") - return - } - if file.Name != tt.fn { - t.Errorf("EmbedFile() failed. Filename of embeds expected: %s, got: %s", tt.fn, - file.Name) - } - buf := bytes.Buffer{} - if _, err := file.Writer(&buf); err != nil { - t.Errorf("failed to execute WriterFunc: %s", err) - return - } - } - m.Reset() - }) - } - } - -// TestMsg_EmbedFileBrokenFunc tests WriterFunc of the Msg.EmbedFile method - - func TestMsg_EmbedFileBrokenFunc(t *testing.T) { - m := NewMsg() - m.EmbedFile("README.md") - if len(m.embeds) != 1 { - t.Errorf("EmbedFile() failed. Number of embeds expected: %d, got: %d", 1, - len(m.embeds)) - return - } - file := m.embeds[0] - if file == nil { - t.Errorf("EmbedFile() failed. Embedded file pointer is nil") - return - } - file.Writer = func(io.Writer) (int64, error) { - return 0, fmt.Errorf("failing intentionally") - } - buf := bytes.Buffer{} - if _, err := file.Writer(&buf); err == nil { - t.Errorf("execute WriterFunc did not fail, but was expected to fail") - } - } - -// TestMsg_EmbedReader tests the Msg.EmbedReader method - - func TestMsg_EmbedReader(t *testing.T) { - m := NewMsg() - ts := "This is a test string" - rbuf := bytes.Buffer{} - rbuf.WriteString(ts) - r := bufio.NewReader(&rbuf) - if err := m.EmbedReader("testfile.txt", r); err != nil { - t.Errorf("EmbedReader() failed. Expected no error, got: %s", err.Error()) - return - } - if len(m.embeds) != 1 { - t.Errorf("EmbedReader() failed. Number of embeds expected: %d, got: %d", 1, - len(m.embeds)) - return - } - file := m.embeds[0] - if file == nil { - t.Errorf("EmbedReader() failed. Embedded file pointer is nil") - return - } - if file.Name != "testfile.txt" { - t.Errorf("EmbedReader() failed. Expected file name: %s, got: %s", "testfile.txt", - file.Name) - } - wbuf := bytes.Buffer{} - if _, err := file.Writer(&wbuf); err != nil { - t.Errorf("execute WriterFunc failed: %s", err) - } - if wbuf.String() != ts { - t.Errorf("EmbedReader() failed. Expected string: %q, got: %q", ts, wbuf.String()) - } - } - // TestMsg_hasAlt tests the hasAlt() method of the Msg func TestMsg_hasAlt(t *testing.T) { From ae44d37d03939bc671c684a2d9aacc52bc7cc4f8 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 12:30:13 +0100 Subject: [PATCH 164/188] 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. --- msg_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/msg_test.go b/msg_test.go index 78382e8..42682b5 100644 --- a/msg_test.go +++ b/msg_test.go @@ -5104,6 +5104,44 @@ func TestMsg_EmbedFromEmbedFS(t *testing.T) { }) } +func TestMsg_Reset(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.From("toni.tester@example.com"); err != nil { + t.Fatalf("failed to set From address: %s", err) + } + if err := message.To("tina.tester@example.com"); err != nil { + t.Fatalf("failed to set To address: %s", err) + } + message.Subject("This is the subject") + message.SetBodyString(TypeTextPlain, "This is the body") + message.AddAlternativeString(TypeTextPlain, "This is the alternative string") + message.EmbedFile("testdata/embed.txt") + message.AttachFile("testdata/attach.txt") + + message.Reset() + if len(message.GetFromString()) != 0 { + t.Errorf("expected message From address to be empty, got: %+v", message.GetFromString()) + } + if len(message.GetToString()) != 0 { + t.Errorf("expected message To address to be empty, got: %+v", message.GetFromString()) + } + if len(message.GetGenHeader(HeaderSubject)) != 0 { + t.Errorf("expected message Subject to be empty, got: %+v", message.GetGenHeader(HeaderSubject)) + } + if len(message.GetAttachments()) != 0 { + t.Errorf("expected message Attachments to be empty, got: %d", len(message.GetAttachments())) + } + if len(message.GetEmbeds()) != 0 { + t.Errorf("expected message Embeds to be empty, got: %d", len(message.GetEmbeds())) + } + if len(message.GetParts()) != 0 { + t.Errorf("expected message Parts to be empty, got: %d", len(message.GetParts())) + } +} + /* // TestNewMsgWithMiddleware tests WithMiddleware From 4a519a3b1f06c4804a6499d937fb382b74ac9bfc Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 14:54:42 +0100 Subject: [PATCH 165/188] 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. --- msg_test.go | 162 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 137 insertions(+), 25 deletions(-) diff --git a/msg_test.go b/msg_test.go index 42682b5..a8a3db6 100644 --- a/msg_test.go +++ b/msg_test.go @@ -5142,24 +5142,136 @@ func TestMsg_Reset(t *testing.T) { } } +func TestMsg_applyMiddlewares(t *testing.T) { + t.Run("new message with middleware: uppercase", func(t *testing.T) { + tests := []struct { + subject string + want string + }{ + {"This is test subject", "THIS IS TEST SUBJECT"}, + {"This is also a test subject", "THIS IS ALSO A TEST SUBJECT"}, + } + + for _, tt := range tests { + t.Run(tt.subject, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if len(message.middlewares) != 0 { + t.Errorf("NewMsg() failed. Expected empty middlewares, got: %d", len(message.middlewares)) + } + message = NewMsg(WithMiddleware(uppercaseMiddleware{})) + if len(message.middlewares) != 1 { + t.Errorf("NewMsg(WithMiddleware(uppercaseMiddleware{})) failed. Expected 1 middleware, got: %d", + len(message.middlewares)) + } + message.Subject(tt.subject) + checkGenHeader(t, message, HeaderSubject, "applyMiddleware", 0, 1, tt.subject) + message = message.applyMiddlewares(message) + checkGenHeader(t, message, HeaderSubject, "applyMiddleware", 0, 1, tt.want) + }) + } + }) + t.Run("new message with middleware: encode", func(t *testing.T) { + tests := []struct { + subject string + want string + }{ + {"This is a test subject", "This is @ test subject"}, + {"This is also a test subject", "This is @lso @ test subject"}, + } + + for _, tt := range tests { + t.Run(tt.subject, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if len(message.middlewares) != 0 { + t.Errorf("NewMsg() failed. Expected empty middlewares, got: %d", len(message.middlewares)) + } + message = NewMsg(WithMiddleware(encodeMiddleware{})) + if len(message.middlewares) != 1 { + t.Errorf("NewMsg(WithMiddleware(encodeMiddleware{})) failed. Expected 1 middleware, got: %d", + len(message.middlewares)) + } + message.Subject(tt.subject) + checkGenHeader(t, message, HeaderSubject, "applyMiddleware", 0, 1, tt.subject) + message = message.applyMiddlewares(message) + checkGenHeader(t, message, HeaderSubject, "applyMiddleware", 0, 1, tt.want) + }) + } + }) + t.Run("new message with middleware: uppercase and encode", func(t *testing.T) { + tests := []struct { + subject string + want string + }{ + {"This is a test subject", "THIS IS @ TEST SUBJECT"}, + {"This is also a test subject", "THIS IS @LSO @ TEST SUBJECT"}, + } + + for _, tt := range tests { + t.Run(tt.subject, func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if len(message.middlewares) != 0 { + t.Errorf("NewMsg() failed. Expected empty middlewares, got: %d", len(message.middlewares)) + } + message = NewMsg(WithMiddleware(encodeMiddleware{}), WithMiddleware(uppercaseMiddleware{})) + if len(message.middlewares) != 2 { + t.Errorf("NewMsg(WithMiddleware(encodeMiddleware{})) failed. Expected 2 middlewares, got: %d", + len(message.middlewares)) + } + message.Subject(tt.subject) + checkGenHeader(t, message, HeaderSubject, "applyMiddleware", 0, 1, tt.subject) + message = message.applyMiddlewares(message) + checkGenHeader(t, message, HeaderSubject, "applyMiddleware", 0, 1, tt.want) + }) + } + }) +} + +func TestMsg_WriteTo(t *testing.T) { + t.Run("WriteTo memory buffer with normal mail parts", func(t *testing.T) { + message := testMessage(t) + buffer := bytes.NewBuffer(nil) + if _, err := message.WriteTo(buffer); err != nil { + t.Fatalf("failed to write message to buffer: %s", err) + } + parsed, err := EMLToMsgFromReader(buffer) + if err != nil { + t.Fatalf("failed to parse message in buffer: %s", err) + } + checkAddrHeader(t, parsed, HeaderFrom, "WriteTo", 0, 1, TestSenderValid, "") + checkAddrHeader(t, parsed, HeaderTo, "WriteTo", 0, 1, TestRcptValid, "") + checkGenHeader(t, parsed, HeaderSubject, "WriteTo", 0, 1, "Testmail") + parts := parsed.GetParts() + if len(parts) != 1 { + t.Fatalf("expected 1 parts, got: %d", len(parts)) + } + if parts[0].contentType != TypeTextPlain { + t.Errorf("expected contentType to be %s, got: %s", TypeTextPlain, parts[0].contentType) + } + if parts[0].encoding != EncodingQP { + t.Errorf("expected encoding to be %s, got: %s", EncodingQP, parts[0].encoding) + } + messageBuf := bytes.NewBuffer(nil) + _, err = parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writer func failed: %s", err) + } + got := strings.TrimSpace(messageBuf.String()) + if !strings.HasSuffix(got, "Testmail") { + t.Errorf("expected message buffer to contain Testmail, got: %s", got) + } + }) +} + /* -// TestNewMsgWithMiddleware tests WithMiddleware - - func TestNewMsgWithMiddleware(t *testing.T) { - m := NewMsg() - if len(m.middlewares) != 0 { - t.Errorf("empty middlewares failed. m.middlewares expected to be: empty, got: %d middleware", len(m.middlewares)) - } - m = NewMsg(WithMiddleware(uppercaseMiddleware{})) - if len(m.middlewares) != 1 { - t.Errorf("empty middlewares failed. m.middlewares expected to be: 1, got: %d middleware", len(m.middlewares)) - } - m = NewMsg(WithMiddleware(uppercaseMiddleware{}), WithMiddleware(encodeMiddleware{})) - if len(m.middlewares) != 2 { - t.Errorf("empty middlewares failed. m.middlewares expected to be: 2, got: %d middleware", len(m.middlewares)) - } - } - // TestApplyMiddlewares tests the applyMiddlewares for the Msg object func TestApplyMiddlewares(t *testing.T) { @@ -6658,23 +6770,23 @@ func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, f t.Helper() addresses, ok := message.addrHeader[header] if !ok { - t.Fatalf("failed to set %s, addrHeader field is not set", fn) + t.Fatalf("failed to exec %s, addrHeader field is not set", fn) } if len(addresses) != wantFields { - t.Fatalf("failed to set %s, addrHeader value count is: %d, want: %d", fn, len(addresses), field) + t.Fatalf("failed to exec %s, addrHeader value count is: %d, want: %d", fn, len(addresses), field) } if addresses[field].Address != wantMail { - t.Errorf("failed to set %s, addrHeader value is %s, want: %s", fn, addresses[field].Address, wantMail) + t.Errorf("failed to exec %s, addrHeader value is %s, want: %s", fn, addresses[field].Address, wantMail) } wantString := fmt.Sprintf(`<%s>`, wantMail) if wantName != "" { wantString = fmt.Sprintf(`%q <%s>`, wantName, wantMail) } if addresses[field].String() != wantString { - t.Errorf("failed to set %s, addrHeader value is %s, want: %s", fn, addresses[field].String(), wantString) + t.Errorf("failed to exec %s, addrHeader value is %s, want: %s", fn, addresses[field].String(), wantString) } if addresses[field].Name != wantName { - t.Errorf("failed to set %s, addrHeader name is %s, want: %s", fn, addresses[field].Name, wantName) + t.Errorf("failed to exec %s, addrHeader name is %s, want: %s", fn, addresses[field].Name, wantName) } } @@ -6685,12 +6797,12 @@ func checkGenHeader(t *testing.T, message *Msg, header Header, fn string, field, t.Helper() values, ok := message.genHeader[header] if !ok { - t.Fatalf("failed to set %s, genHeader field is not set", fn) + t.Fatalf("failed to exec %s, genHeader field is not set", fn) } if len(values) != wantFields { - t.Fatalf("failed to set %s, genHeader value count is: %d, want: %d", fn, len(values), field) + t.Fatalf("failed to exec %s, genHeader value count is: %d, want: %d", fn, len(values), field) } if values[field] != wantVal { - t.Errorf("failed to set %s, genHeader value is %s, want: %s", fn, values[field], wantVal) + t.Errorf("failed to exec %s, genHeader value is %s, want: %s", fn, values[field], wantVal) } } From afa65585a01c140efb4bf732446c9eb7ec52180e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 14:58:55 +0100 Subject: [PATCH 166/188] 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. --- msg_test.go | 94 +++++++++++++++++++++++++++-------------------------- 1 file changed, 48 insertions(+), 46 deletions(-) diff --git a/msg_test.go b/msg_test.go index a8a3db6..e5fee38 100644 --- a/msg_test.go +++ b/msg_test.go @@ -5271,39 +5271,57 @@ func TestMsg_WriteTo(t *testing.T) { }) } +func TestMsg_WriteToSkipMiddleware(t *testing.T) { + t.Run("WriteToSkipMiddleware with two middlewares, skipping uppercase", func(t *testing.T) { + message := NewMsg(WithMiddleware(encodeMiddleware{}), WithMiddleware(uppercaseMiddleware{})) + if message == nil { + t.Fatal("failed to create new message") + } + if err := message.From(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("This is a test subject") + message.SetBodyString(TypeTextPlain, "Testmail") + + buffer := bytes.NewBuffer(nil) + if _, err := message.WriteToSkipMiddleware(buffer, uppercaseMiddleware{}.Type()); err != nil { + t.Fatalf("failed to write message with middleware to buffer: %s", err) + } + parsed, err := EMLToMsgFromReader(buffer) + if err != nil { + t.Fatalf("failed to parse message in buffer: %s", err) + } + checkAddrHeader(t, parsed, HeaderFrom, "WriteTo", 0, 1, TestSenderValid, "") + checkAddrHeader(t, parsed, HeaderTo, "WriteTo", 0, 1, TestRcptValid, "") + checkGenHeader(t, parsed, HeaderSubject, "WriteTo", 0, 1, "This is @ test subject") + parts := parsed.GetParts() + if len(parts) != 1 { + t.Fatalf("expected 1 parts, got: %d", len(parts)) + } + if parts[0].contentType != TypeTextPlain { + t.Errorf("expected contentType to be %s, got: %s", TypeTextPlain, parts[0].contentType) + } + if parts[0].encoding != EncodingQP { + t.Errorf("expected encoding to be %s, got: %s", EncodingQP, parts[0].encoding) + } + messageBuf := bytes.NewBuffer(nil) + _, err = parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writer func failed: %s", err) + } + got := strings.TrimSpace(messageBuf.String()) + if !strings.HasSuffix(got, "Testmail") { + t.Errorf("expected message buffer to contain Testmail, got: %s", got) + } + }) +} + /* // TestApplyMiddlewares tests the applyMiddlewares for the Msg object - func TestApplyMiddlewares(t *testing.T) { - tests := []struct { - name string - sub string - want string - }{ - {"normal subject", "This is a test subject", "THIS IS @ TEST SUBJECT"}, - {"subject with one middleware effect", "This is test subject", "THIS IS TEST SUBJECT"}, - {"subject with one middleware effect", "This is A test subject", "THIS IS A TEST SUBJECT"}, - } - m := NewMsg(WithMiddleware(encodeMiddleware{}), WithMiddleware(uppercaseMiddleware{})) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m.Subject(tt.sub) - if m.genHeader[HeaderSubject] == nil { - t.Errorf("Subject() method failed in applyMiddlewares() test. Generic header for subject is empty") - return - } - m = m.applyMiddlewares(m) - s, ok := m.genHeader[HeaderSubject] - if !ok { - t.Errorf("failed to get subject header") - } - if s[0] != tt.want { - t.Errorf("applyMiddlewares() method failed. Expected: %s, got: %s", tt.want, s[0]) - } - }) - } - } - // TestMsg_hasAlt tests the hasAlt() method of the Msg func TestMsg_hasAlt(t *testing.T) { @@ -5337,22 +5355,6 @@ func TestMsg_WriteTo(t *testing.T) { } } -// TestMsg_WriteTo tests the WriteTo() method of the Msg - - func TestMsg_WriteTo(t *testing.T) { - m := NewMsg() - m.SetBodyString(TypeTextPlain, "Plain") - wbuf := bytes.Buffer{} - n, err := m.WriteTo(&wbuf) - if err != nil { - t.Errorf("WriteTo() failed: %s", err) - return - } - if n != int64(wbuf.Len()) { - t.Errorf("WriteTo() failed: expected written byte length: %d, got: %d", n, wbuf.Len()) - } - } - // TestMsg_WriteToSkipMiddleware tests the WriteTo() method of the Msg func TestMsg_WriteToSkipMiddleware(t *testing.T) { From 4eb9d8a1fa42ea5a1ecdafdb7a2a8fe1f9d8acb0 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 15:14:43 +0100 Subject: [PATCH 167/188] 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. --- msg_test.go | 75 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 33 deletions(-) diff --git a/msg_test.go b/msg_test.go index e5fee38..8d106ed 100644 --- a/msg_test.go +++ b/msg_test.go @@ -5269,6 +5269,24 @@ func TestMsg_WriteTo(t *testing.T) { t.Errorf("expected message buffer to contain Testmail, got: %s", got) } }) + t.Run("WriteTo fails to write", func(t *testing.T) { + message := testMessage(t) + _, err := message.WriteTo(failReadWriteSeekCloser{}) + if err == nil { + t.Fatalf("writing to failReadWriteSeekCloser should fail") + } + if strings.EqualFold(err.Error(), "failed to write message to buffer: intentional write failure") { + t.Fatalf("expected error to be: failed to write message to buffer: intentional write failure, got: %s", + err) + } + }) +} +func TestMsg_Write(t *testing.T) { + message := testMessage(t) + if _, err := message.Write(io.Discard); err != nil { + t.Fatalf("failed to write message to io.Discard: %s", err) + } + t.Log("Write() is just an alias to WriteTo(), which has already been tested.") } func TestMsg_WriteToSkipMiddleware(t *testing.T) { @@ -5355,39 +5373,6 @@ func TestMsg_WriteToSkipMiddleware(t *testing.T) { } } -// TestMsg_WriteToSkipMiddleware tests the WriteTo() method of the Msg - - func TestMsg_WriteToSkipMiddleware(t *testing.T) { - m := NewMsg(WithMiddleware(encodeMiddleware{}), WithMiddleware(uppercaseMiddleware{})) - m.Subject("This is a test") - m.SetBodyString(TypeTextPlain, "Plain") - wbuf := bytes.Buffer{} - n, err := m.WriteToSkipMiddleware(&wbuf, "uppercase") - if err != nil { - t.Errorf("WriteToSkipMiddleware() failed: %s", err) - return - } - if n != int64(wbuf.Len()) { - t.Errorf("WriteToSkipMiddleware() failed: expected written byte length: %d, got: %d", n, wbuf.Len()) - } - if !strings.Contains(wbuf.String(), "Subject: This is @ test") { - t.Errorf("WriteToSkipMiddleware failed. Unable to find encoded subject") - } - - wbuf2 := bytes.Buffer{} - n, err = m.WriteTo(&wbuf2) - if err != nil { - t.Errorf("WriteTo() failed: %s", err) - return - } - if n != int64(wbuf2.Len()) { - t.Errorf("WriteTo() failed: expected written byte length: %d, got: %d", n, wbuf2.Len()) - } - if !strings.Contains(wbuf2.String(), "Subject: THIS IS @ TEST") { - t.Errorf("WriteToSkipMiddleware failed. Unable to find encoded and upperchase subject") - } - } - // TestMsg_WriteTo_fails tests the WriteTo() method of the Msg but with a failing body writer function func TestMsg_WriteTo_fails(t *testing.T) { @@ -6764,6 +6749,30 @@ func (mw encodeMiddleware) Type() MiddlewareType { return "encode" } +// failReadWriteSeekCloser is a type that always returns an error. It satisfies the io.Reader, io.Writer +// io.Closer, io.Seeker, io.WriteSeeker, io.ReadSeeker, io.ReadCloser and io.WriteCloser interfaces +type failReadWriteSeekCloser struct{} + +// Write satisfies the io.Writer interface for the failReadWriteSeekCloser type +func (failReadWriteSeekCloser) Write([]byte) (int, error) { + return 0, errors.New("intentional write failure") +} + +// Read satisfies the io.Reader interface for the failReadWriteSeekCloser type +func (failReadWriteSeekCloser) Read([]byte) (int, error) { + return 0, errors.New("intentional read failure") +} + +// Seek satisfies the io.Seeker interface for the failReadWriteSeekCloser type +func (failReadWriteSeekCloser) Seek(int64, int) (int64, error) { + return 0, errors.New("intentional seek failure") +} + +// Close satisfies the io.Closer interface for the failReadWriteSeekCloser type +func (failReadWriteSeekCloser) Close() error { + return errors.New("intentional close failure") +} + // checkAddrHeader verifies the correctness of an AddrHeader in a Msg based on the provided criteria. // It checks whether the AddrHeader contains the correct address, name, and number of fields. func checkAddrHeader(t *testing.T, message *Msg, header AddrHeader, fn string, field, wantFields int, From 759452f346be3c58576cffadd7cfbd5212e3cd98 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 15:42:20 +0100 Subject: [PATCH 168/188] 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. --- msg_test.go | 151 ++++++++++++++++++++++++++ msg_unix_test.go | 269 +---------------------------------------------- 2 files changed, 153 insertions(+), 267 deletions(-) diff --git a/msg_test.go b/msg_test.go index 8d106ed..11c9010 100644 --- a/msg_test.go +++ b/msg_test.go @@ -4515,6 +4515,21 @@ func TestMsg_AttachFile(t *testing.T) { t.Run("AttachFile with options", func(t *testing.T) { t.Log("all options have already been tested in file_test.go") }) + t.Run("AttachFile with fileFromFS fails on copy", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.AttachFile("testdata/attachment.txt") + attachments := message.GetAttachments() + if len(attachments) != 1 { + t.Fatalf("failed to get attachments, expected 1, got: %d", len(attachments)) + } + _, err := attachments[0].Writer(failReadWriteSeekCloser{}) + if err == nil { + t.Error("writer func expected to fail, but didn't") + } + }) } func TestMsg_AttachReader(t *testing.T) { @@ -4555,6 +4570,32 @@ func TestMsg_AttachReader(t *testing.T) { t.Errorf("expected message body to be %s, got: %s", "This is a test attachment", got) } }) + t.Run("AttachReader with fileFromReader fails on copy", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + file, err := os.Open("testdata/attachment.txt") + if err != nil { + t.Fatalf("failed to open file: %s", err) + } + t.Cleanup(func() { + if err := file.Close(); err != nil { + t.Errorf("failed to close file: %s", err) + } + }) + if err = message.AttachReader("attachment.txt", file); err != nil { + t.Fatalf("failed to attach reader: %s", err) + } + attachments := message.GetAttachments() + if len(attachments) != 1 { + t.Fatalf("failed to get attachments, expected 1, got: %d", len(attachments)) + } + _, err = attachments[0].Writer(failReadWriteSeekCloser{}) + if err == nil { + t.Error("writer func expected to fail, but didn't") + } + }) } func TestMsg_AttachReadSeeker(t *testing.T) { @@ -4593,6 +4634,30 @@ func TestMsg_AttachReadSeeker(t *testing.T) { t.Errorf("expected message body to be %s, got: %s", "This is a test attachment", got) } }) + t.Run("AttachReadSeeker with fileFromReadSeeker fails on copy", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + file, err := os.Open("testdata/attachment.txt") + if err != nil { + t.Fatalf("failed to open file: %s", err) + } + t.Cleanup(func() { + if err := file.Close(); err != nil { + t.Errorf("failed to close file: %s", err) + } + }) + message.AttachReadSeeker("attachment.txt", file) + attachments := message.GetAttachments() + if len(attachments) != 1 { + t.Fatalf("failed to get attachments, expected 1, got: %d", len(attachments)) + } + _, err = attachments[0].Writer(failReadWriteSeekCloser{}) + if err == nil { + t.Error("writer func expected to fail, but didn't") + } + }) } func TestMsg_AttachHTMLTemplate(t *testing.T) { @@ -4827,6 +4892,21 @@ func TestMsg_EmbedFile(t *testing.T) { t.Fatalf("failed to retrieve attachments list") } }) + t.Run("EmbedFile with fileFromFS fails on copy", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.EmbedFile("testdata/embed.txt") + emebeds := message.GetEmbeds() + if len(emebeds) != 1 { + t.Fatalf("failed to get emebeds, expected 1, got: %d", len(emebeds)) + } + _, err := emebeds[0].Writer(failReadWriteSeekCloser{}) + if err == nil { + t.Error("writer func expected to fail, but didn't") + } + }) t.Run("EmbedFile with options", func(t *testing.T) { t.Log("all options have already been tested in file_test.go") }) @@ -4870,6 +4950,53 @@ func TestMsg_EmbedReader(t *testing.T) { t.Errorf("expected message body to be %s, got: %s", "This is a test embed", got) } }) + t.Run("EmbedReader with fileFromReader fails on copy", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + file, err := os.Open("testdata/embed.txt") + if err != nil { + t.Fatalf("failed to open file: %s", err) + } + t.Cleanup(func() { + if err := file.Close(); err != nil { + t.Errorf("failed to close file: %s", err) + } + }) + if err = message.EmbedReader("embed.txt", file); err != nil { + t.Fatalf("failed to embed reader: %s", err) + } + embeds := message.GetEmbeds() + if len(embeds) != 1 { + t.Fatalf("failed to get embeds, expected 1, got: %d", len(embeds)) + } + _, err = embeds[0].Writer(failReadWriteSeekCloser{}) + if err == nil { + t.Error("writer func expected to fail, but didn't") + } + }) + t.Run("EmbedReader with fileFromReader on closed reader", func(t *testing.T) { + tempfile, err := os.CreateTemp("", "embedfile-close-reader.*.txt") + if err != nil { + t.Fatalf("failed to create temp file: %s", err) + } + if err = tempfile.Close(); err != nil { + t.Fatalf("failed to close temp file: %s", err) + } + t.Cleanup(func() { + if err := os.Remove(tempfile.Name()); err != nil { + t.Errorf("failed to remove temp file: %s", err) + } + }) + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err = message.EmbedReader("embed.txt", tempfile); err == nil { + t.Fatalf("expected error, got nil") + } + }) } func TestMsg_EmbedReadSeeker(t *testing.T) { @@ -4908,6 +5035,30 @@ func TestMsg_EmbedReadSeeker(t *testing.T) { t.Errorf("expected message body to be %s, got: %s", "This is a test embed", got) } }) + t.Run("EmbedReadSeeker with fileFromReadSeeker fails on copy", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + file, err := os.Open("testdata/embed.txt") + if err != nil { + t.Fatalf("failed to open file: %s", err) + } + t.Cleanup(func() { + if err := file.Close(); err != nil { + t.Errorf("failed to close file: %s", err) + } + }) + message.EmbedReadSeeker("embed.txt", file) + embeds := message.GetEmbeds() + if len(embeds) != 1 { + t.Fatalf("failed to get embeds, expected 1, got: %d", len(embeds)) + } + _, err = embeds[0].Writer(failReadWriteSeekCloser{}) + if err == nil { + t.Error("writer func expected to fail, but didn't") + } + }) } func TestMsg_EmbedHTMLTemplate(t *testing.T) { diff --git a/msg_unix_test.go b/msg_unix_test.go index eec15ff..2f5e2d6 100644 --- a/msg_unix_test.go +++ b/msg_unix_test.go @@ -22,7 +22,7 @@ func TestMsg_AttachFile_unixOnly(t *testing.T) { t.Skipf("PERFORM_UNIX_OPEN_WRITE_TESTS variable is not set. Skipping unix open/write tests") } - tempFile, err := os.CreateTemp("testdata/tmp", "attachfile-open-write-test.*.txt") + tempFile, err := os.CreateTemp("", "attachfile-open-write-test.*.txt") if err != nil { t.Fatalf("failed to create temp file: %s", err) } @@ -53,128 +53,6 @@ func TestMsg_AttachFile_unixOnly(t *testing.T) { t.Errorf("expected error to be %s, got: %s", os.ErrPermission, err) } }) - t.Run("AttachFile with fileFromFS fails on copy", func(t *testing.T) { - tempfile, err := os.CreateTemp("testdata/tmp", "attachfile-close-early.*.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) - } - }) - message := NewMsg() - if message == nil { - t.Fatal("message is nil") - } - message.AttachFile("testdata/attachment.txt") - attachments := message.GetAttachments() - if len(attachments) != 1 { - t.Fatalf("failed to get attachments, expected 1, got: %d", len(attachments)) - } - messageBuf, err := os.Open(tempfile.Name()) - if err != nil { - t.Fatalf("failed to open temp file: %s", err) - } - // We close early to cause an error during io.Copy - if err = messageBuf.Close(); err != nil { - t.Fatalf("failed to close temp file: %s", err) - } - _, err = attachments[0].Writer(messageBuf) - if err == nil { - t.Error("writer func expected to fail, but didn't") - } - }) -} - -func TestMsg_AttachReader_unixOnly(t *testing.T) { - t.Run("AttachReader with fileFromReader fails on copy", func(t *testing.T) { - tempfile, err := os.CreateTemp("", "attachfile-close-early.*.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) - } - }) - message := NewMsg() - if message == nil { - t.Fatal("message is nil") - } - file, err := os.Open("testdata/attachment.txt") - if err != nil { - t.Fatalf("failed to open file: %s", err) - } - t.Cleanup(func() { - if err := file.Close(); err != nil { - t.Errorf("failed to close file: %s", err) - } - }) - if err = message.AttachReader("attachment.txt", file); err != nil { - t.Fatalf("failed to attach reader: %s", err) - } - attachments := message.GetAttachments() - if len(attachments) != 1 { - t.Fatalf("failed to get attachments, expected 1, got: %d", len(attachments)) - } - messageBuf, err := os.Open(tempfile.Name()) - if err != nil { - t.Fatalf("failed to open temp file: %s", err) - } - // We close early to cause an error during io.Copy - if err = messageBuf.Close(); err != nil { - t.Fatalf("failed to close temp file: %s", err) - } - _, err = attachments[0].Writer(messageBuf) - if err == nil { - t.Error("writer func expected to fail, but didn't") - } - }) -} - -func TestMsg_AttachReadSeeker_unixOnly(t *testing.T) { - t.Run("AttachReadSeeker with fileFromReadSeeker fails on copy", func(t *testing.T) { - tempfile, err := os.CreateTemp("", "attachfile-close-early.*.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) - } - }) - message := NewMsg() - if message == nil { - t.Fatal("message is nil") - } - file, err := os.Open("testdata/attachment.txt") - if err != nil { - t.Fatalf("failed to open file: %s", err) - } - t.Cleanup(func() { - if err := file.Close(); err != nil { - t.Errorf("failed to close file: %s", err) - } - }) - message.AttachReadSeeker("attachment.txt", file) - attachments := message.GetAttachments() - if len(attachments) != 1 { - t.Fatalf("failed to get attachments, expected 1, got: %d", len(attachments)) - } - messageBuf, err := os.Open(tempfile.Name()) - if err != nil { - t.Fatalf("failed to open temp file: %s", err) - } - // We close early to cause an error during io.Copy - if err = messageBuf.Close(); err != nil { - t.Fatalf("failed to close temp file: %s", err) - } - _, err = attachments[0].Writer(messageBuf) - if err == nil { - t.Error("writer func expected to fail, but didn't") - } - }) } func TestMsg_EmbedFile_unixOnly(t *testing.T) { @@ -183,7 +61,7 @@ func TestMsg_EmbedFile_unixOnly(t *testing.T) { t.Skipf("PERFORM_UNIX_OPEN_WRITE_TESTS variable is not set. Skipping unix open/write tests") } - tempFile, err := os.CreateTemp("testdata/tmp", "embedfile-open-write-test.*.txt") + tempFile, err := os.CreateTemp("", "embedfile-open-write-test.*.txt") if err != nil { t.Fatalf("failed to create temp file: %s", err) } @@ -214,149 +92,6 @@ func TestMsg_EmbedFile_unixOnly(t *testing.T) { t.Errorf("expected error to be %s, got: %s", os.ErrPermission, err) } }) - t.Run("EmbedFile with fileFromFS fails on copy", func(t *testing.T) { - tempfile, err := os.CreateTemp("testdata/tmp", "embedfile-close-early.*.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) - } - }) - message := NewMsg() - if message == nil { - t.Fatal("message is nil") - } - message.EmbedFile("testdata/embed.txt") - emebeds := message.GetEmbeds() - if len(emebeds) != 1 { - t.Fatalf("failed to get emebeds, expected 1, got: %d", len(emebeds)) - } - messageBuf, err := os.Open(tempfile.Name()) - if err != nil { - t.Fatalf("failed to open temp file: %s", err) - } - // We close early to cause an error during io.Copy - if err = messageBuf.Close(); err != nil { - t.Fatalf("failed to close temp file: %s", err) - } - _, err = emebeds[0].Writer(messageBuf) - if err == nil { - t.Error("writer func expected to fail, but didn't") - } - }) -} - -func TestMsg_EmbedReader_unixOnly(t *testing.T) { - t.Run("EmbedReader with fileFromReader fails on copy", func(t *testing.T) { - tempfile, err := os.CreateTemp("", "embedfile-close-early.*.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) - } - }) - message := NewMsg() - if message == nil { - t.Fatal("message is nil") - } - file, err := os.Open("testdata/embed.txt") - if err != nil { - t.Fatalf("failed to open file: %s", err) - } - t.Cleanup(func() { - if err := file.Close(); err != nil { - t.Errorf("failed to close file: %s", err) - } - }) - if err = message.EmbedReader("embed.txt", file); err != nil { - t.Fatalf("failed to embed reader: %s", err) - } - embeds := message.GetEmbeds() - if len(embeds) != 1 { - t.Fatalf("failed to get embeds, expected 1, got: %d", len(embeds)) - } - messageBuf, err := os.Open(tempfile.Name()) - if err != nil { - t.Fatalf("failed to open temp file: %s", err) - } - // We close early to cause an error during io.Copy - if err = messageBuf.Close(); err != nil { - t.Fatalf("failed to close temp file: %s", err) - } - _, err = embeds[0].Writer(messageBuf) - if err == nil { - t.Error("writer func expected to fail, but didn't") - } - }) - t.Run("EmbedReader with fileFromReader on closed reader", func(t *testing.T) { - tempfile, err := os.CreateTemp("", "embedfile-close-early.*.txt") - if err != nil { - t.Fatalf("failed to create temp file: %s", err) - } - if err = tempfile.Close(); err != nil { - t.Fatalf("failed to close temp file: %s", err) - } - t.Cleanup(func() { - if err := os.Remove(tempfile.Name()); err != nil { - t.Errorf("failed to remove temp file: %s", err) - } - }) - message := NewMsg() - if message == nil { - t.Fatal("message is nil") - } - if err = message.EmbedReader("embed.txt", tempfile); err == nil { - t.Fatalf("expected error, got nil") - } - }) -} - -func TestMsg_EmbedReadSeeker_unixOnly(t *testing.T) { - t.Run("EmbedReadSeeker with fileFromReadSeeker fails on copy", func(t *testing.T) { - tempfile, err := os.CreateTemp("", "embedfile-close-early.*.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) - } - }) - message := NewMsg() - if message == nil { - t.Fatal("message is nil") - } - file, err := os.Open("testdata/embed.txt") - if err != nil { - t.Fatalf("failed to open file: %s", err) - } - t.Cleanup(func() { - if err := file.Close(); err != nil { - t.Errorf("failed to close file: %s", err) - } - }) - message.EmbedReadSeeker("embed.txt", file) - embeds := message.GetEmbeds() - if len(embeds) != 1 { - t.Fatalf("failed to get embeds, expected 1, got: %d", len(embeds)) - } - messageBuf, err := os.Open(tempfile.Name()) - if err != nil { - t.Fatalf("failed to open temp file: %s", err) - } - // We close early to cause an error during io.Copy - if err = messageBuf.Close(); err != nil { - t.Fatalf("failed to close temp file: %s", err) - } - _, err = embeds[0].Writer(messageBuf) - if err == nil { - t.Error("writer func expected to fail, but didn't") - } - }) } // TestMsg_WriteToSendmailWithContext tests the WriteToSendmailWithContext() method of the Msg From 4506472319ba3f664fc0625ada6276d0944e31fa Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 15:43:32 +0100 Subject: [PATCH 169/188] 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. --- msg_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/msg_test.go b/msg_test.go index 11c9010..7fb5bbe 100644 --- a/msg_test.go +++ b/msg_test.go @@ -5432,6 +5432,7 @@ func TestMsg_WriteTo(t *testing.T) { } }) } + func TestMsg_Write(t *testing.T) { message := testMessage(t) if _, err := message.Write(io.Discard); err != nil { From 6cbb6745bb01000be5c12b887f2c8ec27b405c41 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 15:58:14 +0100 Subject: [PATCH 170/188] 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. --- msg_test.go | 159 ++++++++++++++-------------------------------------- 1 file changed, 41 insertions(+), 118 deletions(-) diff --git a/msg_test.go b/msg_test.go index 7fb5bbe..0ccde30 100644 --- a/msg_test.go +++ b/msg_test.go @@ -4515,6 +4515,32 @@ func TestMsg_AttachFile(t *testing.T) { t.Run("AttachFile with options", func(t *testing.T) { t.Log("all options have already been tested in file_test.go") }) + t.Run("AttachFile with normal file and nil option", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.AttachFile("testdata/attachment.txt", nil) + attachments := message.GetAttachments() + if len(attachments) != 1 { + t.Fatalf("failed to retrieve attachments list") + } + if attachments[0] == nil { + t.Fatal("expected attachment to be not nil") + } + if attachments[0].Name != "attachment.txt" { + t.Errorf("expected attachment name to be %s, got: %s", "attachment.txt", attachments[0].Name) + } + messageBuf := bytes.NewBuffer(nil) + _, err := attachments[0].Writer(messageBuf) + if err != nil { + t.Errorf("writer func failed: %s", err) + } + got := strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "This is a test attachment") { + t.Errorf("expected message body to be %s, got: %s", "This is a test attachment", got) + } + }) t.Run("AttachFile with fileFromFS fails on copy", func(t *testing.T) { message := NewMsg() if message == nil { @@ -5431,6 +5457,21 @@ func TestMsg_WriteTo(t *testing.T) { err) } }) + t.Run("WriteTo with long headers", func(t *testing.T) { + message := testMessage(t) + message.SetGenHeader(HeaderContentLang, "de", "en", "fr", "es", "xxxx", "yyyy", "de", "en", "fr", + "es", "xxxx", "yyyy", "de", "en", "fr", "es", "xxxx", "yyyy", "de", "en", "fr") + message.SetGenHeader(HeaderContentID, "XXXXXXXXXXXXXXX XXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXX "+ + "XXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXX") + messageBuffer := bytes.NewBuffer(nil) + n, err := message.WriteTo(messageBuffer) + if err != nil { + t.Fatalf("failed to write message to buffer: %s", err) + } + if n != int64(messageBuffer.Len()) { + t.Errorf("expected written bytes: %d, got: %d", n, messageBuffer.Len()) + } + }) } func TestMsg_Write(t *testing.T) { @@ -5525,124 +5566,6 @@ func TestMsg_WriteToSkipMiddleware(t *testing.T) { } } -// TestMsg_WriteTo_fails tests the WriteTo() method of the Msg but with a failing body writer function - - func TestMsg_WriteTo_fails(t *testing.T) { - m := NewMsg() - m.SetBodyWriter(TypeTextPlain, func(io.Writer) (int64, error) { - return 0, errors.New("failed") - }) - _, err := m.WriteTo(io.Discard) - if err == nil { - t.Errorf("WriteTo() with failing BodyWriter function was supposed to fail, but didn't") - return - } - - // NoEncoding handles the errors separately - m = NewMsg(WithEncoding(NoEncoding)) - m.SetBodyWriter(TypeTextPlain, func(io.Writer) (int64, error) { - return 0, errors.New("failed") - }) - _, err = m.WriteTo(io.Discard) - if err == nil { - t.Errorf("WriteTo() (no encoding) with failing BodyWriter function was supposed to fail, but didn't") - return - } - } - -// TestMsg_Write tests the Write() method of the Msg - - func TestMsg_Write(t *testing.T) { - m := NewMsg() - m.SetBodyString(TypeTextPlain, "Plain") - wbuf := bytes.Buffer{} - n, err := m.Write(&wbuf) - if err != nil { - t.Errorf("WriteTo() failed: %s", err) - return - } - if n != int64(wbuf.Len()) { - t.Errorf("WriteTo() failed: expected written byte length: %d, got: %d", n, wbuf.Len()) - } - } - -// TestMsg_WriteWithLongHeader tests the WriteTo() method of the Msg with a long header - - func TestMsg_WriteWithLongHeader(t *testing.T) { - m := NewMsg() - m.SetBodyString(TypeTextPlain, "Plain") - m.SetGenHeader(HeaderContentLang, "de", "en", "fr", "es", "xxxx", "yyyy", "de", "en", "fr", - "es", "xxxx", "yyyy", "de", "en", "fr", "es", "xxxx", "yyyy", "de", "en", "fr") - m.SetGenHeader(HeaderContentID, "XXXXXXXXXXXXXXX XXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXX", - "XXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXX") - wbuf := bytes.Buffer{} - n, err := m.WriteTo(&wbuf) - if err != nil { - t.Errorf("WriteTo() failed: %s", err) - return - } - if n != int64(wbuf.Len()) { - t.Errorf("WriteTo() failed: expected written byte length: %d, got: %d", n, wbuf.Len()) - } - } - -// TestMsg_WriteDiffEncoding tests the WriteTo() method of the Msg with different Encoding - - func TestMsg_WriteDiffEncoding(t *testing.T) { - tests := []struct { - name string - ct ContentType - en Encoding - alt bool - wa bool - we bool - }{ - {"Plain/QP/NoAlt/NoAttach/NoEmbed", TypeTextPlain, EncodingQP, false, false, false}, - {"Plain/B64/NoAlt/NoAttach/NoEmbed", TypeTextPlain, EncodingB64, false, false, false}, - {"Plain/No/NoAlt/NoAttach/NoEmbed", TypeTextPlain, NoEncoding, false, false, false}, - {"HTML/QP/NoAlt/NoAttach/NoEmbed", TypeTextHTML, EncodingQP, false, false, false}, - {"HTML/B64/NoAlt/NoAttach/NoEmbed", TypeTextHTML, EncodingB64, false, false, false}, - {"HTML/No/NoAlt/NoAttach/NoEmbed", TypeTextHTML, NoEncoding, false, false, false}, - {"Plain/QP/HTML/NoAttach/NoEmbed", TypeTextPlain, EncodingQP, true, false, false}, - {"Plain/B64/HTML/NoAttach/NoEmbed", TypeTextPlain, EncodingB64, true, false, false}, - {"Plain/No/HTML/NoAttach/NoEmbed", TypeTextPlain, NoEncoding, true, false, false}, - {"Plain/QP/NoAlt/Attach/NoEmbed", TypeTextPlain, EncodingQP, false, true, false}, - {"Plain/B64/NoAlt/Attach/NoEmbed", TypeTextPlain, EncodingB64, false, true, false}, - {"Plain/No/NoAlt/Attach/NoEmbed", TypeTextPlain, NoEncoding, false, true, false}, - {"Plain/QP/NoAlt/NoAttach/Embed", TypeTextPlain, EncodingQP, false, false, true}, - {"Plain/B64/NoAlt/NoAttach/Embed", TypeTextPlain, EncodingB64, false, false, true}, - {"Plain/No/NoAlt/NoAttach/Embed", TypeTextPlain, NoEncoding, false, false, true}, - {"Plain/QP/HTML/Attach/Embed", TypeTextPlain, EncodingQP, true, true, true}, - {"Plain/B64/HTML/Attach/Embed", TypeTextPlain, EncodingB64, true, true, true}, - {"Plain/No/HTML/Attach/Embed", TypeTextPlain, NoEncoding, true, true, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m := NewMsg(WithEncoding(tt.en)) - m.SetBodyString(tt.ct, tt.name) - if tt.alt { - m.AddAlternativeString(TypeTextHTML, fmt.Sprintf("

%s

", tt.name)) - } - if tt.wa { - m.AttachFile("README.md") - } - if tt.we { - m.EmbedFile("README.md") - } - wbuf := bytes.Buffer{} - n, err := m.WriteTo(&wbuf) - if err != nil { - t.Errorf("WriteTo() failed: %s", err) - return - } - if n != int64(wbuf.Len()) { - t.Errorf("WriteTo() failed: expected written byte length: %d, got: %d", n, wbuf.Len()) - } - wbuf.Reset() - }) - } - } - // TestMsg_appendFile tests the appendFile() method of the Msg func TestMsg_appendFile(t *testing.T) { From d39953c837bdf9c22b0964a6fcd1ee6760419cfc Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 16:32:24 +0100 Subject: [PATCH 171/188] 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. --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 798b03e..f4f40d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,8 +31,8 @@ jobs: go: ['1.23'] env: PERFORM_ONLINE_TEST: ${{ vars.PERFORM_ONLINE_TEST }} - PERFORM_UNIX_OPEN_WRITE_TESTS: "true" - TEST_SENDMAIL: ${{ vars.TEST_SENDMAIL }} + 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 }} From a59173fae00cb0e1b683012054f9c070546605a1 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 17:04:44 +0100 Subject: [PATCH 172/188] 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. --- msg.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/msg.go b/msg.go index 4c42058..0b06a22 100644 --- a/msg.go +++ b/msg.go @@ -2371,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) } From 4ecc6f2b0ce28fc7def34f1238bce353b278e749 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 17:14:06 +0100 Subject: [PATCH 173/188] 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. --- msg_test.go | 700 +++++++++++++++-------------------------------- msg_unix_test.go | 66 ++--- 2 files changed, 242 insertions(+), 524 deletions(-) diff --git a/msg_test.go b/msg_test.go index 0ccde30..356014c 100644 --- a/msg_test.go +++ b/msg_test.go @@ -5472,6 +5472,90 @@ func TestMsg_WriteTo(t *testing.T) { t.Errorf("expected written bytes: %d, got: %d", n, messageBuffer.Len()) } }) + t.Run("WriteTo with multiple writes", func(t *testing.T) { + message := testMessage(t) + buffer := bytes.NewBuffer(nil) + messageBuf := bytes.NewBuffer(nil) + for i := 0; i < 10; i++ { + t.Run(fmt.Sprintf("write %d", i), func(t *testing.T) { + if _, err := message.WriteTo(buffer); err != nil { + t.Fatalf("failed to write message to buffer: %s", err) + } + parsed, err := EMLToMsgFromReader(buffer) + if err != nil { + t.Fatalf("failed to parse message in buffer: %s", err) + } + checkAddrHeader(t, parsed, HeaderFrom, "WriteTo", 0, 1, TestSenderValid, "") + checkAddrHeader(t, parsed, HeaderTo, "WriteTo", 0, 1, TestRcptValid, "") + checkGenHeader(t, parsed, HeaderSubject, "WriteTo", 0, 1, "Testmail") + parts := parsed.GetParts() + if len(parts) != 1 { + t.Fatalf("expected 1 parts, got: %d", len(parts)) + } + if parts[0].contentType != TypeTextPlain { + t.Errorf("expected contentType to be %s, got: %s", TypeTextPlain, parts[0].contentType) + } + if parts[0].encoding != EncodingQP { + t.Errorf("expected encoding to be %s, got: %s", EncodingQP, parts[0].encoding) + } + _, err = parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writer func failed: %s", err) + } + got := strings.TrimSpace(messageBuf.String()) + if !strings.HasSuffix(got, "Testmail") { + t.Errorf("expected message buffer to contain Testmail, got: %s", got) + } + buffer.Reset() + }) + } + }) +} + +func TestMsg_WriteToFile(t *testing.T) { + t.Run("WriteToFile with normal mail parts", func(t *testing.T) { + tempfile, err := os.CreateTemp("", "testmail.*.eml") + if err != nil { + t.Fatalf("failed to create temp file: %s", err) + } + 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.Fatalf("failed to write message to tempfile %q: %s", tempfile.Name(), err) + } + parsed, err := EMLToMsgFromFile(tempfile.Name()) + if err != nil { + t.Fatalf("failed to parse message in buffer: %s", err) + } + checkAddrHeader(t, parsed, HeaderFrom, "WriteTo", 0, 1, TestSenderValid, "") + checkAddrHeader(t, parsed, HeaderTo, "WriteTo", 0, 1, TestRcptValid, "") + checkGenHeader(t, parsed, HeaderSubject, "WriteTo", 0, 1, "Testmail") + parts := parsed.GetParts() + if len(parts) != 1 { + t.Fatalf("expected 1 parts, got: %d", len(parts)) + } + if parts[0].contentType != TypeTextPlain { + t.Errorf("expected contentType to be %s, got: %s", TypeTextPlain, parts[0].contentType) + } + if parts[0].encoding != EncodingQP { + t.Errorf("expected encoding to be %s, got: %s", EncodingQP, parts[0].encoding) + } + messageBuf := bytes.NewBuffer(nil) + _, err = parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writer func failed: %s", err) + } + got := strings.TrimSpace(messageBuf.String()) + if !strings.HasSuffix(got, "Testmail") { + t.Errorf("expected message buffer to contain Testmail, got: %s", got) + } + }) } func TestMsg_Write(t *testing.T) { @@ -5530,9 +5614,134 @@ func TestMsg_WriteToSkipMiddleware(t *testing.T) { }) } -/* -// TestApplyMiddlewares tests the applyMiddlewares for the Msg object +// TestMsg_WriteToSendmailWithContext tests the WriteToSendmailWithContext() method of the Msg +func TestMsg_WriteToSendmailWithContext(t *testing.T) { + if os.Getenv("PERFORM_SENDMAIL_TESTS") != "true" { + t.Skipf("PERFORM_SENDMAIL_TESTS variable is not set to true, skipping sendmail test") + } + if !hasSendmail() { + t.Skipf("sendmail binary not found, skipping test") + } + tests := []struct { + sendmailPath string + shouldFail bool + }{ + {"/dev/null", true}, + {"/bin/cat", true}, + {"/is/invalid", true}, + {SendmailPath, false}, + } + t.Run("WriteToSendmailWithContext on different paths", func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.sendmailPath, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) + defer cancel() + + message := testMessage(t) + err := message.WriteToSendmailWithContext(ctx, tt.sendmailPath) + if err != nil && !tt.shouldFail { + t.Errorf("failed to write message to sendmail: %s", err) + } + if err == nil && tt.shouldFail { + t.Error("expected error, got nil") + } + }) + } + }) + t.Run("WriteToSendmailWithContext on canceled context", func(t *testing.T) { + if !hasSendmail() { + t.Skipf("sendmail binary not found, skipping test") + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) + cancel() + + message := testMessage(t) + if err := message.WriteToSendmailWithContext(ctx, SendmailPath); err == nil { + t.Fatalf("expected error on canceled context, got nil") + } + }) + t.Run("WriteToSendmailWithContext via WriteToSendmail", func(t *testing.T) { + if !hasSendmail() { + t.Skipf("sendmail binary not found, skipping test") + } + message := testMessage(t) + if err := message.WriteToSendmail(); err != nil { + t.Fatalf("failed to write message to sendmail: %s", err) + } + }) + t.Run("WriteToSendmailWithContext via WriteToSendmailWithCommand", func(t *testing.T) { + if !hasSendmail() { + t.Skipf("sendmail binary not found, skipping test") + } + message := testMessage(t) + if err := message.WriteToSendmailWithCommand(SendmailPath); err != nil { + t.Fatalf("failed to write message to sendmail: %s", err) + } + }) +} + +func TestMsg_NewReader(t *testing.T) { + t.Run("NewReader succeeds", func(t *testing.T) { + message := testMessage(t) + reader := message.NewReader() + if reader == nil { + t.Fatalf("failed to create message reader") + } + if reader.Error() != nil { + t.Errorf("failed to create message reader: %s", reader.Error()) + } + parsed, err := EMLToMsgFromReader(reader) + if err != nil { + t.Fatalf("failed to parse message in buffer: %s", err) + } + checkAddrHeader(t, parsed, HeaderFrom, "WriteTo", 0, 1, TestSenderValid, "") + checkAddrHeader(t, parsed, HeaderTo, "WriteTo", 0, 1, TestRcptValid, "") + checkGenHeader(t, parsed, HeaderSubject, "WriteTo", 0, 1, "Testmail") + parts := parsed.GetParts() + if len(parts) != 1 { + t.Fatalf("expected 1 parts, got: %d", len(parts)) + } + if parts[0].contentType != TypeTextPlain { + t.Errorf("expected contentType to be %s, got: %s", TypeTextPlain, parts[0].contentType) + } + if parts[0].encoding != EncodingQP { + t.Errorf("expected encoding to be %s, got: %s", EncodingQP, parts[0].encoding) + } + messageBuf := bytes.NewBuffer(nil) + _, err = parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writer func failed: %s", err) + } + got := strings.TrimSpace(messageBuf.String()) + if !strings.HasSuffix(got, "Testmail") { + t.Errorf("expected message buffer to contain Testmail, got: %s", got) + } + }) + t.Run("NewReader should fail on write", func(t *testing.T) { + message := testMessage(t) + if len(message.parts) != 1 { + t.Fatalf("expected 1 parts, got: %d", len(message.parts)) + } + message.parts[0].writeFunc = func(io.Writer) (int64, error) { + return 0, errors.New("intentional write error") + } + reader := message.NewReader() + if reader == nil { + t.Fatalf("failed to create message reader") + } + if reader.Error() == nil { + t.Fatalf("expected error on write, got nil") + } + if !strings.EqualFold(reader.Error().Error(), `failed to write Msg to Reader buffer: bodyWriter function: `+ + `intentional write error`) { + t.Errorf("expected error to be %s, got: %s", `failed to write Msg to Reader buffer: bodyWriter function: `+ + `intentional write error`, reader.Error().Error()) + } + }) +} + +/* // TestMsg_hasAlt tests the hasAlt() method of the Msg func TestMsg_hasAlt(t *testing.T) { @@ -5566,52 +5775,6 @@ func TestMsg_WriteToSkipMiddleware(t *testing.T) { } } -// TestMsg_appendFile tests the appendFile() method of the Msg - - func TestMsg_appendFile(t *testing.T) { - m := NewMsg() - var fl []*File - f := &File{ - Name: "file.txt", - } - fl = m.appendFile(fl, f, nil) - if len(fl) != 1 { - t.Errorf("appendFile() failed. Expected length: %d, got: %d", 1, len(fl)) - } - fl = m.appendFile(fl, f, nil) - if len(fl) != 2 { - t.Errorf("appendFile() failed. Expected length: %d, got: %d", 2, len(fl)) - } - } - -// TestMsg_multipleWrites tests multiple executions of WriteTo on the Msg - - func TestMsg_multipleWrites(t *testing.T) { - ts := "XXX_UNIQUE_STRING_XXX" - wbuf := bytes.Buffer{} - m := NewMsg() - m.SetBodyString(TypeTextPlain, ts) - - // First WriteTo() - _, err := m.WriteTo(&wbuf) - if err != nil { - t.Errorf("failed to write body to buffer: %s", err) - } - if !strings.Contains(wbuf.String(), ts) { - t.Errorf("first WriteTo() body does not contain unique string: %s", ts) - } - - // Second WriteTo() - wbuf.Reset() - _, err = m.WriteTo(&wbuf) - if err != nil { - t.Errorf("failed to write body to buffer: %s", err) - } - if !strings.Contains(wbuf.String(), ts) { - t.Errorf("second WriteTo() body does not contain unique string: %s", ts) - } - } - // TestMsg_NewReader tests the Msg.NewReader method func TestMsg_NewReader(t *testing.T) { @@ -5689,436 +5852,6 @@ func TestMsg_WriteToSkipMiddleware(t *testing.T) { } } -// TestMsg_SetBodyTextTemplate tests the Msg.SetBodyTextTemplate method - - func TestMsg_SetBodyTextTemplate(t *testing.T) { - tests := []struct { - name string - tpl string - ph string - sf bool - }{ - {"normal text", "This is a {{.Placeholder}}", "TemplateTest", false}, - {"invalid tpl", "This is a {{ foo .Placeholder}}", "TemplateTest", true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data := struct { - Placeholder string - }{Placeholder: tt.ph} - tpl, err := ttpl.New("test").Parse(tt.tpl) - if err != nil && !tt.sf { - t.Errorf("failed to render template: %s", err) - return - } - m := NewMsg() - if err := m.SetBodyTextTemplate(tpl, data); err != nil && !tt.sf { - t.Errorf("failed to set template as body: %s", err) - } - - wbuf := bytes.Buffer{} - _, err = m.WriteTo(&wbuf) - if err != nil { - t.Errorf("failed to write body to buffer: %s", err) - } - if !strings.Contains(wbuf.String(), tt.ph) && !tt.sf { - t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) - } - m.Reset() - }) - } - } - -// TestMsg_SetBodyHTMLTemplate tests the Msg.SetBodyHTMLTemplate method - - func TestMsg_SetBodyHTMLTemplate(t *testing.T) { - tests := []struct { - name string - tpl string - ph string - ex string - sf bool - }{ - {"normal HTML", "

This is a {{.Placeholder}}

", "TemplateTest", "TemplateTest", false}, - { - "HTML with HTML", "

This is a {{.Placeholder}}

", "", - "<script>alert(1)</script>", false, - }, - {"invalid tpl", "

This is a {{ foo .Placeholder}}

", "TemplateTest", "", true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data := struct { - Placeholder string - }{Placeholder: tt.ph} - tpl, err := htpl.New("test").Parse(tt.tpl) - if err != nil && !tt.sf { - t.Errorf("failed to render template: %s", err) - return - } - m := NewMsg() - if err := m.SetBodyHTMLTemplate(tpl, data); err != nil && !tt.sf { - t.Errorf("failed to set template as body: %s", err) - } - - wbuf := bytes.Buffer{} - _, err = m.WriteTo(&wbuf) - if err != nil { - t.Errorf("failed to write body to buffer: %s", err) - } - if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf { - t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) - } - m.Reset() - }) - } - } - -// TestMsg_AddAlternativeTextTemplate tests the Msg.AddAlternativeTextTemplate method - - func TestMsg_AddAlternativeTextTemplate(t *testing.T) { - tests := []struct { - name string - tpl string - ph string - sf bool - }{ - {"normal text", "This is a {{.Placeholder}}", "TemplateTest", false}, - {"invalid tpl", "This is a {{ foo .Placeholder}}", "TemplateTest", true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data := struct { - Placeholder string - }{Placeholder: tt.ph} - tpl, err := ttpl.New("test").Parse(tt.tpl) - if err != nil && !tt.sf { - t.Errorf("failed to render template: %s", err) - return - } - m := NewMsg() - m.SetBodyString(TypeTextHTML, "") - if err := m.AddAlternativeTextTemplate(tpl, data); err != nil && !tt.sf { - t.Errorf("failed to set template as alternative part: %s", err) - } - - wbuf := bytes.Buffer{} - _, err = m.WriteTo(&wbuf) - if err != nil { - t.Errorf("failed to write body to buffer: %s", err) - } - if !strings.Contains(wbuf.String(), tt.ph) && !tt.sf { - t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) - } - m.Reset() - }) - } - } - -// TestMsg_AddAlternativeHTMLTemplate tests the Msg.AddAlternativeHTMLTemplate method - - func TestMsg_AddAlternativeHTMLTemplate(t *testing.T) { - tests := []struct { - name string - tpl string - ph string - ex string - sf bool - }{ - {"normal HTML", "

This is a {{.Placeholder}}

", "TemplateTest", "TemplateTest", false}, - { - "HTML with HTML", "

This is a {{.Placeholder}}

", "", - "<script>alert(1)</script>", false, - }, - {"invalid tpl", "

This is a {{ foo .Placeholder}}

", "TemplateTest", "", true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data := struct { - Placeholder string - }{Placeholder: tt.ph} - tpl, err := htpl.New("test").Parse(tt.tpl) - if err != nil && !tt.sf { - t.Errorf("failed to render template: %s", err) - return - } - m := NewMsg() - m.SetBodyString(TypeTextPlain, "") - if err := m.AddAlternativeHTMLTemplate(tpl, data); err != nil && !tt.sf { - t.Errorf("failed to set template as alternative part: %s", err) - } - - wbuf := bytes.Buffer{} - _, err = m.WriteTo(&wbuf) - if err != nil { - t.Errorf("failed to write body to buffer: %s", err) - } - if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf { - t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) - } - m.Reset() - }) - } - } - -// TestMsg_AttachTextTemplate tests the Msg.AttachTextTemplate method - - func TestMsg_AttachTextTemplate(t *testing.T) { - tests := []struct { - name string - tpl string - ph string - ex string - ac int - sf bool - }{ - { - "normal text", "This is a {{.Placeholder}}", "TemplateTest", - "VGhpcyBpcyBhIFRlbXBsYXRlVGVzdA==", 1, false, - }, - {"invalid tpl", "This is a {{ foo .Placeholder}}", "TemplateTest", "", 0, true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data := struct { - Placeholder string - }{Placeholder: tt.ph} - tpl, err := ttpl.New("test").Parse(tt.tpl) - if err != nil && !tt.sf { - t.Errorf("failed to render template: %s", err) - return - } - m := NewMsg() - m.SetBodyString(TypeTextPlain, "This is the body") - if err := m.AttachTextTemplate("attachment.txt", tpl, data); err != nil && !tt.sf { - t.Errorf("failed to attach template: %s", err) - } - - wbuf := bytes.Buffer{} - _, err = m.WriteTo(&wbuf) - if err != nil { - t.Errorf("failed to write body to buffer: %s", err) - } - if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf { - t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) - } - if len(m.attachments) != tt.ac { - t.Errorf("wrong number of attachments. Expected: %d, got: %d", tt.ac, len(m.attachments)) - } - m.Reset() - }) - } - } - -// TestMsg_AttachHTMLTemplate tests the Msg.AttachHTMLTemplate method - - func TestMsg_AttachHTMLTemplate(t *testing.T) { - tests := []struct { - name string - tpl string - ph string - ex string - ac int - sf bool - }{ - { - "normal HTML", "

This is a {{.Placeholder}}

", "TemplateTest", - "PHA+VGhpcyBpcyBhIFRlbXBsYXRlVGVzdDwvcD4=", 1, false, - }, - { - "HTML with HTML", "

This is a {{.Placeholder}}

", "", - "PHA+VGhpcyBpcyBhICZsdDtzY3JpcHQmZ3Q7YWxlcnQoMSkmbHQ7L3NjcmlwdCZndDs8L3A+", 1, false, - }, - {"invalid tpl", "

This is a {{ foo .Placeholder}}

", "TemplateTest", "", 0, true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data := struct { - Placeholder string - }{Placeholder: tt.ph} - tpl, err := htpl.New("test").Parse(tt.tpl) - if err != nil && !tt.sf { - t.Errorf("failed to render template: %s", err) - return - } - m := NewMsg() - m.SetBodyString(TypeTextPlain, "") - if err := m.AttachHTMLTemplate("attachment.html", tpl, data); err != nil && !tt.sf { - t.Errorf("failed to set template as alternative part: %s", err) - } - - wbuf := bytes.Buffer{} - _, err = m.WriteTo(&wbuf) - if err != nil { - t.Errorf("failed to write body to buffer: %s", err) - } - if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf { - t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) - } - if len(m.attachments) != tt.ac { - t.Errorf("wrong number of attachments. Expected: %d, got: %d", tt.ac, len(m.attachments)) - } - m.Reset() - }) - } - } - -// TestMsg_EmbedTextTemplate tests the Msg.EmbedTextTemplate method - - func TestMsg_EmbedTextTemplate(t *testing.T) { - tests := []struct { - name string - tpl string - ph string - ex string - ec int - sf bool - }{ - { - "normal text", "This is a {{.Placeholder}}", "TemplateTest", - "VGhpcyBpcyBhIFRlbXBsYXRlVGVzdA==", 1, false, - }, - {"invalid tpl", "This is a {{ foo .Placeholder}}", "TemplateTest", "", 0, true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data := struct { - Placeholder string - }{Placeholder: tt.ph} - tpl, err := ttpl.New("test").Parse(tt.tpl) - if err != nil && !tt.sf { - t.Errorf("failed to render template: %s", err) - return - } - m := NewMsg() - m.SetBodyString(TypeTextPlain, "This is the body") - if err := m.EmbedTextTemplate("attachment.txt", tpl, data); err != nil && !tt.sf { - t.Errorf("failed to attach template: %s", err) - } - - wbuf := bytes.Buffer{} - _, err = m.WriteTo(&wbuf) - if err != nil { - t.Errorf("failed to write body to buffer: %s", err) - } - if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf { - t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) - } - if len(m.embeds) != tt.ec { - t.Errorf("wrong number of attachments. Expected: %d, got: %d", tt.ec, len(m.attachments)) - } - m.Reset() - }) - } - } - -// TestMsg_EmbedHTMLTemplate tests the Msg.EmbedHTMLTemplate method - - func TestMsg_EmbedHTMLTemplate(t *testing.T) { - tests := []struct { - name string - tpl string - ph string - ex string - ec int - sf bool - }{ - { - "normal HTML", "

This is a {{.Placeholder}}

", "TemplateTest", - "PHA+VGhpcyBpcyBhIFRlbXBsYXRlVGVzdDwvcD4=", 1, false, - }, - { - "HTML with HTML", "

This is a {{.Placeholder}}

", "", - "PHA+VGhpcyBpcyBhICZsdDtzY3JpcHQmZ3Q7YWxlcnQoMSkmbHQ7L3NjcmlwdCZndDs8L3A+", 1, false, - }, - {"invalid tpl", "

This is a {{ foo .Placeholder}}

", "TemplateTest", "", 0, true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data := struct { - Placeholder string - }{Placeholder: tt.ph} - tpl, err := htpl.New("test").Parse(tt.tpl) - if err != nil && !tt.sf { - t.Errorf("failed to render template: %s", err) - return - } - m := NewMsg() - m.SetBodyString(TypeTextPlain, "") - if err := m.EmbedHTMLTemplate("attachment.html", tpl, data); err != nil && !tt.sf { - t.Errorf("failed to set template as alternative part: %s", err) - } - - wbuf := bytes.Buffer{} - _, err = m.WriteTo(&wbuf) - if err != nil { - t.Errorf("failed to write body to buffer: %s", err) - } - if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf { - t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph) - } - if len(m.embeds) != tt.ec { - t.Errorf("wrong number of attachments. Expected: %d, got: %d", tt.ec, len(m.attachments)) - } - m.Reset() - }) - } - } - -// TestMsg_WriteToTempFile will test the output to temporary files - - func TestMsg_WriteToTempFile(t *testing.T) { - m := NewMsg() - _ = m.From("Toni Tester ") - _ = m.To("Ellenor Tester ") - m.SetBodyString(TypeTextPlain, "This is a test") - f, err := m.WriteToTempFile() - if err != nil { - t.Errorf("failed to write message to temporary output file: %s", err) - } - _ = os.Remove(f) - } - -// TestMsg_WriteToFile will test the output to a file - - func TestMsg_WriteToFile(t *testing.T) { - f, err := os.CreateTemp("", "go-mail-test_*.eml") - if err != nil { - t.Errorf("failed to create temporary output file: %s", err) - } - defer func() { - _ = f.Close() - _ = os.Remove(f.Name()) - }() - - m := NewMsg() - _ = m.From("Toni Tester ") - _ = m.To("Ellenor Tester ") - m.SetBodyString(TypeTextPlain, "This is a test") - if err := m.WriteToFile(f.Name()); err != nil { - t.Errorf("failed to write to output file: %s", err) - } - fi, err := os.Stat(f.Name()) - if err != nil { - t.Errorf("failed to stat output file: %s", err) - } - if fi == nil { - t.Errorf("received empty file handle") - return - } - if fi.Size() <= 0 { - t.Errorf("output file is expected to contain data but its size is zero") - } - } - // TestMsg_GetGenHeader will test the GetGenHeader method of the Msg func TestMsg_GetGenHeader(t *testing.T) { @@ -6892,3 +6625,14 @@ func checkGenHeader(t *testing.T, message *Msg, header Header, fn string, field, t.Errorf("failed to exec %s, genHeader value is %s, want: %s", fn, values[field], wantVal) } } + +// hasSendmail checks if the /usr/sbin/sendmail file exists and is executable. Returns true if conditions are met. +func hasSendmail() bool { + sm, err := os.Stat(SendmailPath) + if err == nil { + if sm.Mode()&0o111 != 0 { + return true + } + } + return false +} diff --git a/msg_unix_test.go b/msg_unix_test.go index 2f5e2d6..8651daf 100644 --- a/msg_unix_test.go +++ b/msg_unix_test.go @@ -9,11 +9,9 @@ package mail import ( "bytes" - "context" "errors" "os" "testing" - "time" ) func TestMsg_AttachFile_unixOnly(t *testing.T) { @@ -94,52 +92,28 @@ func TestMsg_EmbedFile_unixOnly(t *testing.T) { }) } -// TestMsg_WriteToSendmailWithContext tests the WriteToSendmailWithContext() method of the Msg -func TestMsg_WriteToSendmailWithContext(t *testing.T) { - if os.Getenv("TEST_SENDMAIL") != "true" { - t.Skipf("TEST_SENDMAIL variable is not 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) +func TestMsg_WriteToFile_unixOnly(t *testing.T) { + t.Run("WriteToFile fails on create", func(t *testing.T) { + 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) } - m.Reset() }) - } -} - -// TestMsg_WriteToSendmail will test the output to the local sendmail command -func TestMsg_WriteToSendmail(t *testing.T) { - if os.Getenv("TEST_SENDMAIL") != "true" { - t.Skipf("TEST_SENDMAIL variable is not 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 ") - _ = m.To(TestRcpt) - m.SetBodyString(TypeTextPlain, "This is a test") - if err := m.WriteToSendmail(); err != nil { - t.Errorf("WriteToSendmail failed: %s", err) - } + message := testMessage(t) + if err = message.WriteToFile(tempfile.Name()); err == nil { + t.Errorf("expected error, got nil") + } + }) } func TestMsg_WriteToTempFileFailed(t *testing.T) { From 2f3809f33e8d7ee56f1ff6ba7c0bc42bb76d1219 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 17:20:40 +0100 Subject: [PATCH 174/188] 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. --- msg_test.go | 120 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 75 insertions(+), 45 deletions(-) diff --git a/msg_test.go b/msg_test.go index 356014c..94659a3 100644 --- a/msg_test.go +++ b/msg_test.go @@ -5741,6 +5741,81 @@ func TestMsg_NewReader(t *testing.T) { }) } +func TestMsg_UpdateReader(t *testing.T) { + t.Run("UpdateReader succeeds", func(t *testing.T) { + message := testMessage(t) + reader := message.NewReader() + if reader == nil { + t.Fatalf("failed to create message reader") + } + if reader.Error() != nil { + t.Errorf("failed to create message reader: %s", reader.Error()) + } + message.Subject("This is the actual subject") + message.UpdateReader(reader) + if reader == nil { + t.Fatalf("failed to create message reader") + } + if reader.Error() != nil { + t.Errorf("failed to create message reader: %s", reader.Error()) + } + parsed, err := EMLToMsgFromReader(reader) + if err != nil { + t.Fatalf("failed to parse message in buffer: %s", err) + } + checkAddrHeader(t, parsed, HeaderFrom, "WriteTo", 0, 1, TestSenderValid, "") + checkAddrHeader(t, parsed, HeaderTo, "WriteTo", 0, 1, TestRcptValid, "") + checkGenHeader(t, parsed, HeaderSubject, "WriteTo", 0, 1, "This is the actual subject") + parts := parsed.GetParts() + if len(parts) != 1 { + t.Fatalf("expected 1 parts, got: %d", len(parts)) + } + if parts[0].contentType != TypeTextPlain { + t.Errorf("expected contentType to be %s, got: %s", TypeTextPlain, parts[0].contentType) + } + if parts[0].encoding != EncodingQP { + t.Errorf("expected encoding to be %s, got: %s", EncodingQP, parts[0].encoding) + } + messageBuf := bytes.NewBuffer(nil) + _, err = parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writer func failed: %s", err) + } + got := strings.TrimSpace(messageBuf.String()) + if !strings.HasSuffix(got, "Testmail") { + t.Errorf("expected message buffer to contain Testmail, got: %s", got) + } + }) + t.Run("UpdateReader should fail on write", func(t *testing.T) { + message := testMessage(t) + reader := message.NewReader() + if reader == nil { + t.Fatalf("failed to create message reader") + } + if reader.Error() != nil { + t.Errorf("failed to create message reader: %s", reader.Error()) + } + message.Subject("This is the actual subject") + if len(message.parts) != 1 { + t.Fatalf("expected 1 parts, got: %d", len(message.parts)) + } + message.parts[0].writeFunc = func(io.Writer) (int64, error) { + return 0, errors.New("intentional write error") + } + message.UpdateReader(reader) + if reader == nil { + t.Fatalf("failed to create message reader") + } + if reader.Error() == nil { + t.Fatalf("expected error on write, got nil") + } + if !strings.EqualFold(reader.Error().Error(), `bodyWriter function: intentional write error`) { + t.Errorf("expected error to be %s, got: %s", `bodyWriter function: intentional write error`, + reader.Error().Error()) + } + }) +} + /* // TestMsg_hasAlt tests the hasAlt() method of the Msg @@ -5775,51 +5850,6 @@ func TestMsg_NewReader(t *testing.T) { } } -// TestMsg_NewReader tests the Msg.NewReader method - - func TestMsg_NewReader(t *testing.T) { - m := NewMsg() - m.SetBodyString(TypeTextPlain, "TEST123") - mr := m.NewReader() - if mr == nil { - t.Errorf("NewReader failed: Reader is nil") - } - if mr.Error() != nil { - t.Errorf("NewReader failed: %s", mr.Error()) - } - } - -// TestMsg_NewReader_ioCopy tests the Msg.NewReader method using io.Copy - - func TestMsg_NewReader_ioCopy(t *testing.T) { - wbuf1 := bytes.Buffer{} - wbuf2 := bytes.Buffer{} - m := NewMsg() - m.SetBodyString(TypeTextPlain, "TEST123") - mr := m.NewReader() - if mr == nil { - t.Errorf("NewReader failed: Reader is nil") - } - - // First we use WriteTo to have something to compare to - _, err := m.WriteTo(&wbuf1) - if err != nil { - t.Errorf("failed to write body to buffer: %s", err) - } - - // Then we write to wbuf2 via io.Copy - n, err := io.Copy(&wbuf2, mr) - if err != nil { - t.Errorf("failed to use io.Copy on Reader: %s", err) - } - if n != int64(wbuf1.Len()) { - t.Errorf("message length of WriteTo and io.Copy differ. Expected: %d, got: %d", wbuf1.Len(), n) - } - if wbuf1.String() != wbuf2.String() { - t.Errorf("message content of WriteTo and io.Copy differ") - } - } - // TestMsg_UpdateReader tests the Msg.UpdateReader method func TestMsg_UpdateReader(t *testing.T) { From 7d670a1f2457610a562b4dfcdfbbb1d50637aed0 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 17:31:11 +0100 Subject: [PATCH 175/188] 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. --- client_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client_test.go b/client_test.go index 46c7dcd..8408035 100644 --- a/client_test.go +++ b/client_test.go @@ -3559,6 +3559,7 @@ type serverProps struct { FailOnQuit bool FailOnReset bool FailOnSTARTTLS bool + FailTemp bool FeatureSet string ListenPort int SSLListener bool @@ -3720,6 +3721,10 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server writeLine("500 5.0.0 Error during DATA transmission") break } + if props.FailTemp { + writeLine("451 4.3.0 Error: fail on DATA close") + break + } writeLine("250 2.0.0 Ok: queued as 1234567890") break } From 1c6b9faf15a1bfcb3d822db3f8e8407e29ba692d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 17:34:35 +0100 Subject: [PATCH 176/188] 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. --- msg_test.go | 221 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) diff --git a/msg_test.go b/msg_test.go index 94659a3..3ce2b63 100644 --- a/msg_test.go +++ b/msg_test.go @@ -5816,6 +5816,227 @@ func TestMsg_UpdateReader(t *testing.T) { }) } +func TestMsg_HasSendError(t *testing.T) { + t.Run("HasSendError on unsent message", func(t *testing.T) { + message := testMessage(t) + if message.HasSendError() { + t.Error("HasSendError on unsent message should return false") + } + }) + t.Run("HasSendError on sent message", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-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) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + + message := testMessage(t) + if err = client.DialAndSend(message); 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 test server: %s", err) + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + + if message.HasSendError() { + t.Error("HasSendError on sent message should return false") + } + }) + t.Run("HasSendError on failed message delivery", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnDataClose: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + + message := testMessage(t) + if err = client.DialAndSend(message); err == nil { + t.Error("message delivery was supposed to fail on data close") + } + if !message.HasSendError() { + t.Error("HasSendError on failed message delivery should return true") + } + }) + t.Run("HasSendError on failed message with SendError", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnDataClose: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + + message := testMessage(t) + if err = client.DialAndSend(message); err == nil { + t.Error("message delivery was supposed to fail on data close") + } + if !message.HasSendError() { + t.Fatal("HasSendError on failed message delivery should return true") + } + if message.SendError() == nil { + t.Fatal("SendError on failed message delivery should return SendErr") + } + var sendErr *SendError + if !errors.As(message.SendError(), &sendErr) { + t.Fatal("expected SendError to return a SendError type") + } + }) + t.Run("HasSendError with SendErrorIsTemp", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnDataClose: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + + message := testMessage(t) + if err = client.DialAndSend(message); err == nil { + t.Error("message delivery was supposed to fail on data close") + } + if !message.HasSendError() { + t.Error("HasSendError on failed message delivery should return true") + } + if message.SendErrorIsTemp() { + t.Error("SendErrorIsTemp on hard failed message delivery should return false") + } + }) + t.Run("HasSendError with SendErrorIsTemp on temp error", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailTemp: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + + message := testMessage(t) + if err = client.DialAndSend(message); err == nil { + t.Error("message delivery was supposed to fail on data close") + } + if !message.HasSendError() { + t.Error("HasSendError on failed message delivery should return true") + } + if !message.SendErrorIsTemp() { + t.Error("SendErrorIsTemp on temp failed message delivery should return true") + } + }) + t.Run("HasSendError with not a SendErr", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailTemp: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + + message := testMessage(t) + if err = client.DialAndSend(message); err == nil { + t.Error("message delivery was supposed to fail on data close") + } + message.sendError = errors.New("not a SendErr") + if !message.HasSendError() { + t.Error("HasSendError with not a SendErr should still return true") + } + if message.SendErrorIsTemp() { + t.Error("SendErrorIsTemp on not a SendErr should return false") + } + }) +} + /* // TestMsg_hasAlt tests the hasAlt() method of the Msg From c28bd7e33193f896d6e88d5d58cb30683483a763 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 17:42:18 +0100 Subject: [PATCH 177/188] 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. --- msg_unix_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/msg_unix_test.go b/msg_unix_test.go index 8651daf..018e215 100644 --- a/msg_unix_test.go +++ b/msg_unix_test.go @@ -94,6 +94,10 @@ func TestMsg_EmbedFile_unixOnly(t *testing.T) { 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) @@ -116,6 +120,7 @@ func TestMsg_WriteToFile_unixOnly(t *testing.T) { }) } +/* func TestMsg_WriteToTempFileFailed(t *testing.T) { m := NewMsg() _ = m.From("Toni Tester ") @@ -137,3 +142,6 @@ func TestMsg_WriteToTempFileFailed(t *testing.T) { t.Errorf("WriteToTempFile() did not fail as expected") } } + + +*/ From d0280ea9ad47e59b69921715bebaa1e55f4e7b78 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 18:52:02 +0100 Subject: [PATCH 178/188] 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. --- msg_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ msg_unix_test.go | 43 +++++++++++++++++++++---------------------- 2 files changed, 61 insertions(+), 22 deletions(-) diff --git a/msg_test.go b/msg_test.go index 3ce2b63..62c74ae 100644 --- a/msg_test.go +++ b/msg_test.go @@ -6037,6 +6037,46 @@ func TestMsg_HasSendError(t *testing.T) { }) } +func TestMsg_WriteToTempFile(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 succeeds", func(t *testing.T) { + message := testMessage(t) + tempFile, err := message.WriteToTempFile() + if err != nil { + t.Fatalf("failed to write message to temp file: %s", err) + } + parsed, err := EMLToMsgFromFile(tempFile) + if err != nil { + t.Fatalf("failed to parse message in buffer: %s", err) + } + checkAddrHeader(t, parsed, HeaderFrom, "WriteTo", 0, 1, TestSenderValid, "") + checkAddrHeader(t, parsed, HeaderTo, "WriteTo", 0, 1, TestRcptValid, "") + checkGenHeader(t, parsed, HeaderSubject, "WriteTo", 0, 1, "Testmail") + parts := parsed.GetParts() + if len(parts) != 1 { + t.Fatalf("expected 1 parts, got: %d", len(parts)) + } + if parts[0].contentType != TypeTextPlain { + t.Errorf("expected contentType to be %s, got: %s", TypeTextPlain, parts[0].contentType) + } + if parts[0].encoding != EncodingQP { + t.Errorf("expected encoding to be %s, got: %s", EncodingQP, parts[0].encoding) + } + messageBuf := bytes.NewBuffer(nil) + _, err = parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writer func failed: %s", err) + } + got := strings.TrimSpace(messageBuf.String()) + if !strings.HasSuffix(got, "Testmail") { + t.Errorf("expected message buffer to contain Testmail, got: %s", got) + } + }) +} + /* // TestMsg_hasAlt tests the hasAlt() method of the Msg diff --git a/msg_unix_test.go b/msg_unix_test.go index 018e215..57e1547 100644 --- a/msg_unix_test.go +++ b/msg_unix_test.go @@ -120,28 +120,27 @@ func TestMsg_WriteToFile_unixOnly(t *testing.T) { }) } -/* -func TestMsg_WriteToTempFileFailed(t *testing.T) { - m := NewMsg() - _ = m.From("Toni Tester ") - _ = m.To("Ellenor Tester ") - m.SetBodyString(TypeTextPlain, "This is a test") +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") + } - curTmpDir := os.Getenv("TMPDIR") - defer func() { - if err := os.Setenv("TMPDIR", curTmpDir); err != nil { - t.Errorf("failed to set TMPDIR environment variable: %s", err) + 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) } - }() - - 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") - } + message := testMessage(t) + _, err := message.WriteToTempFile() + if err == nil { + t.Errorf("expected writing to invalid TMPDIR to fail, got: %s", err) + } + }) } - - -*/ From 7bbcee7d48e8c0f2763a949eff8740fc75ea25b5 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 19:47:00 +0100 Subject: [PATCH 179/188] 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. --- msg_test.go | 1114 ++++++++++++++++++--------------------------------- 1 file changed, 388 insertions(+), 726 deletions(-) diff --git a/msg_test.go b/msg_test.go index 62c74ae..117e8e7 100644 --- a/msg_test.go +++ b/msg_test.go @@ -3908,6 +3908,32 @@ func TestMsg_SetBodyWriter(t *testing.T) { t.Errorf("expected message body to be %s, got: %s", "test", messageBuf.String()) } }) + t.Run("SetBodyWriter with nil option", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + message.SetBodyWriter(TypeTextPlain, writerFunc, nil) + parts := message.GetParts() + if len(parts) != 1 { + t.Fatalf("expected 1 part, got: %d", len(parts)) + } + if parts[0] == nil { + t.Fatal("expected part to be not nil") + } + if parts[0].contentType != TypeTextPlain { + t.Errorf("expected contentType to be %s, got: %s", TypeTextPlain, + parts[0].contentType) + } + messageBuf := bytes.NewBuffer(nil) + _, err := parts[0].writeFunc(messageBuf) + if err != nil { + t.Errorf("writeFunc failed: %s", err) + } + if !strings.EqualFold(messageBuf.String(), "test") { + t.Errorf("expected message body to be %s, got: %s", "test", messageBuf.String()) + } + }) } func TestMsg_SetBodyHTMLTemplate(t *testing.T) { @@ -4622,6 +4648,39 @@ func TestMsg_AttachReader(t *testing.T) { t.Error("writer func expected to fail, but didn't") } }) + // Tests the Msg.AttachReader methods with consecutive calls to Msg.WriteTo to make sure + // the attachments are not lost. + // https://github.com/wneessen/go-mail/issues/110 + t.Run("AttachReader with consecutive writes", func(t *testing.T) { + teststring := "This is a test string" + message := testMessage(t) + if err := message.AttachReader("attachment.txt", bytes.NewBufferString(teststring)); err != nil { + t.Fatalf("failed to attach teststring buffer: %s", err) + } + messageBuffer := bytes.NewBuffer(nil) + altBuffer := bytes.NewBuffer(nil) + // First write + n1, err := message.WriteTo(messageBuffer) + if err != nil { + t.Fatalf("failed to write message to buffer: %s", err) + } + // Second write + n2, err := message.WriteTo(altBuffer) + if err != nil { + t.Fatalf("failed to write message to alternative buffer: %s", err) + } + if n1 != n2 { + t.Errorf("number of written bytes between consecutive writes differ, got: %d and %d", n1, n2) + } + if !strings.Contains(messageBuffer.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { + t.Errorf("expected string not found in message buffer, want: %s, got: %s", + "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n", messageBuffer.String()) + } + if !strings.Contains(altBuffer.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { + t.Errorf("expected string not found in alternative message buffer, want: %s, got: %s", + "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n", altBuffer.String()) + } + }) } func TestMsg_AttachReadSeeker(t *testing.T) { @@ -4684,6 +4743,37 @@ func TestMsg_AttachReadSeeker(t *testing.T) { t.Error("writer func expected to fail, but didn't") } }) + // Tests the Msg.AttachReadSeeker methods with consecutive calls to Msg.WriteTo to make sure + // the attachments are not lost. + // https://github.com/wneessen/go-mail/issues/110 + t.Run("AttachReadSeeker with consecutive writes", func(t *testing.T) { + teststring := []byte("This is a test string") + message := testMessage(t) + message.AttachReadSeeker("attachment.txt", bytes.NewReader(teststring)) + messageBuffer := bytes.NewBuffer(nil) + altBuffer := bytes.NewBuffer(nil) + // First write + n1, err := message.WriteTo(messageBuffer) + if err != nil { + t.Fatalf("failed to write message to buffer: %s", err) + } + // Second write + n2, err := message.WriteTo(altBuffer) + if err != nil { + t.Fatalf("failed to write message to alternative buffer: %s", err) + } + if n1 != n2 { + t.Errorf("number of written bytes between consecutive writes differ, got: %d and %d", n1, n2) + } + if !strings.Contains(messageBuffer.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { + t.Errorf("expected string not found in message buffer, want: %s, got: %s", + "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n", messageBuffer.String()) + } + if !strings.Contains(altBuffer.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { + t.Errorf("expected string not found in alternative message buffer, want: %s, got: %s", + "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n", altBuffer.String()) + } + }) } func TestMsg_AttachHTMLTemplate(t *testing.T) { @@ -5023,6 +5113,39 @@ func TestMsg_EmbedReader(t *testing.T) { t.Fatalf("expected error, got nil") } }) + // Tests the Msg.EmbedReader methods with consecutive calls to Msg.WriteTo to make sure + // the attachments are not lost. + // https://github.com/wneessen/go-mail/issues/110 + t.Run("EmbedReader with consecutive writes", func(t *testing.T) { + teststring := "This is a test string" + message := testMessage(t) + if err := message.EmbedReader("embed.txt", bytes.NewBufferString(teststring)); err != nil { + t.Fatalf("failed to embed teststring buffer: %s", err) + } + messageBuffer := bytes.NewBuffer(nil) + altBuffer := bytes.NewBuffer(nil) + // First write + n1, err := message.WriteTo(messageBuffer) + if err != nil { + t.Fatalf("failed to write message to buffer: %s", err) + } + // Second write + n2, err := message.WriteTo(altBuffer) + if err != nil { + t.Fatalf("failed to write message to alternative buffer: %s", err) + } + if n1 != n2 { + t.Errorf("number of written bytes between consecutive writes differ, got: %d and %d", n1, n2) + } + if !strings.Contains(messageBuffer.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { + t.Errorf("expected string not found in message buffer, want: %s, got: %s", + "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n", messageBuffer.String()) + } + if !strings.Contains(altBuffer.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { + t.Errorf("expected string not found in alternative message buffer, want: %s, got: %s", + "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n", altBuffer.String()) + } + }) } func TestMsg_EmbedReadSeeker(t *testing.T) { @@ -5085,6 +5208,37 @@ func TestMsg_EmbedReadSeeker(t *testing.T) { t.Error("writer func expected to fail, but didn't") } }) + // Tests the Msg.EmbedReadSeeker methods with consecutive calls to Msg.WriteTo to make sure + // the attachments are not lost. + // https://github.com/wneessen/go-mail/issues/110 + t.Run("EmbedReadSeeker with consecutive writes", func(t *testing.T) { + teststring := []byte("This is a test string") + message := testMessage(t) + message.EmbedReadSeeker("embed.txt", bytes.NewReader(teststring)) + messageBuffer := bytes.NewBuffer(nil) + altBuffer := bytes.NewBuffer(nil) + // First write + n1, err := message.WriteTo(messageBuffer) + if err != nil { + t.Fatalf("failed to write message to buffer: %s", err) + } + // Second write + n2, err := message.WriteTo(altBuffer) + if err != nil { + t.Fatalf("failed to write message to alternative buffer: %s", err) + } + if n1 != n2 { + t.Errorf("number of written bytes between consecutive writes differ, got: %d and %d", n1, n2) + } + if !strings.Contains(messageBuffer.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { + t.Errorf("expected string not found in message buffer, want: %s, got: %s", + "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n", messageBuffer.String()) + } + if !strings.Contains(altBuffer.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { + t.Errorf("expected string not found in alternative message buffer, want: %s, got: %s", + "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n", altBuffer.String()) + } + }) } func TestMsg_EmbedHTMLTemplate(t *testing.T) { @@ -6077,743 +6231,219 @@ func TestMsg_WriteToTempFile(t *testing.T) { }) } -/* -// TestMsg_hasAlt tests the hasAlt() method of the Msg +func TestMsg_hasAlt(t *testing.T) { + t.Run("message has no alt", func(t *testing.T) { + message := testMessage(t) + if message.hasAlt() { + t.Error("message has no alt, but hasAlt returned true") + } + }) + t.Run("message has alt", func(t *testing.T) { + message := testMessage(t) + message.AddAlternativeString(TypeTextHTML, "this is the alt") + if !message.hasAlt() { + t.Error("message has alt, but hasAlt returned false") + } + }) + t.Run("message has no alt due to deleted part", func(t *testing.T) { + message := testMessage(t) + message.AddAlternativeString(TypeTextHTML, "this is the alt") + if len(message.GetParts()) != 2 { + t.Errorf("expected message to have 2 parts, got: %d", len(message.GetParts())) + } + message.parts[1].isDeleted = true + if message.hasAlt() { + t.Error("message has no alt, but hasAlt returned true") + } + }) + t.Run("message has alt and deleted parts", func(t *testing.T) { + message := testMessage(t) + message.AddAlternativeString(TypeTextHTML, "this is the alt") + message.AddAlternativeString(TypeTextPlain, "this is also a alt") + if len(message.GetParts()) != 3 { + t.Errorf("expected message to have 3 parts, got: %d", len(message.GetParts())) + } + message.parts[1].isDeleted = true + if !message.hasAlt() { + t.Error("message has alt, but hasAlt returned false") + } + }) + t.Run("message has alt but it is pgptype", func(t *testing.T) { + message := testMessage(t) + message.SetPGPType(PGPSignature) + if message.hasAlt() { + t.Error("message has alt but it is a pgpType, hence hasAlt should return false") + } + }) +} - func TestMsg_hasAlt(t *testing.T) { - m := NewMsg() - m.SetBodyString(TypeTextPlain, "Plain") - m.AddAlternativeString(TypeTextHTML, "HTML") - if !m.hasAlt() { - t.Errorf("mail has alternative parts but hasAlt() returned true") +func TestMsg_hasMixed(t *testing.T) { + t.Run("message has no mixed", func(t *testing.T) { + message := testMessage(t) + if message.hasMixed() { + t.Error("message has no mixed, but hasMixed returned true") } - } + }) + t.Run("message with alt has no mixed", func(t *testing.T) { + message := testMessage(t) + message.AddAlternativeString(TypeTextHTML, "this is the alt") + if message.hasMixed() { + t.Error("message has no mixed, but hasMixed returned true") + } + }) + t.Run("message with attachment is mixed", func(t *testing.T) { + message := testMessage(t) + message.AttachFile("testdata/attachment.txt") + if !message.hasMixed() { + t.Error("message with attachment is supposed to be mixed") + } + }) + t.Run("message with embed is not mixed", func(t *testing.T) { + message := testMessage(t) + message.EmbedFile("testdata/embed.txt") + if message.hasMixed() { + t.Error("message with embed is not supposed to be mixed") + } + }) + t.Run("message with attachment and embed is mixed", func(t *testing.T) { + message := testMessage(t) + message.AttachFile("testdata/attachment.txt") + message.EmbedFile("testdata/embed.txt") + if !message.hasMixed() { + t.Error("message with attachment and embed is supposed to be mixed") + } + }) +} -// TestMsg_hasRelated tests the hasRelated() method of the Msg +func TestMsg_hasRelated(t *testing.T) { + t.Run("message has no related", func(t *testing.T) { + message := testMessage(t) + if message.hasRelated() { + t.Error("message has no related, but hasRelated returned true") + } + }) + t.Run("message with alt has no related", func(t *testing.T) { + message := testMessage(t) + message.AddAlternativeString(TypeTextHTML, "this is the alt") + if message.hasRelated() { + t.Error("message has no related, but hasRelated returned true") + } + }) + t.Run("message with attachment is not related", func(t *testing.T) { + message := testMessage(t) + message.AttachFile("testdata/attachment.txt") + if message.hasRelated() { + t.Error("message with attachment is not supposed to be related") + } + }) + t.Run("message with embed is related", func(t *testing.T) { + message := testMessage(t) + message.EmbedFile("testdata/embed.txt") + if !message.hasRelated() { + t.Error("message with embed is supposed to be related") + } + }) + t.Run("message with attachment and embed is related", func(t *testing.T) { + message := testMessage(t) + message.AttachFile("testdata/attachment.txt") + message.EmbedFile("testdata/embed.txt") + if !message.hasRelated() { + t.Error("message with attachment and embed is supposed to be related") + } + }) +} - func TestMsg_hasRelated(t *testing.T) { - m := NewMsg() - m.SetBodyString(TypeTextPlain, "Plain") - m.EmbedFile("README.md") - if !m.hasRelated() { - t.Errorf("mail has related parts but hasRelated() returned true") +func TestMsg_hasPGPType(t *testing.T) { + t.Run("message has no pgpType", func(t *testing.T) { + message := testMessage(t) + if message.hasPGPType() { + t.Error("message has no PGPType, but hasPGPType returned true") } - } + }) + t.Run("message has signature", func(t *testing.T) { + message := testMessage(t) + message.SetPGPType(PGPSignature) + if !message.hasPGPType() { + t.Error("message has signature, but hasPGPType returned false") + } + }) + t.Run("message has encryption", func(t *testing.T) { + message := testMessage(t) + message.SetPGPType(PGPEncrypt) + if !message.hasPGPType() { + t.Error("message has encryption, but hasPGPType returned false") + } + }) + t.Run("message has encryption and signature", func(t *testing.T) { + message := testMessage(t) + message.SetPGPType(PGPEncrypt | PGPSignature) + if !message.hasPGPType() { + t.Error("message has encryption and signature, but hasPGPType returned false") + } + }) + t.Run("message has NoPGP", func(t *testing.T) { + message := testMessage(t) + message.SetPGPType(NoPGP) + if message.hasPGPType() { + t.Error("message has NoPGP, but hasPGPType returned true") + } + }) +} -// TestMsg_hasMixed tests the hasMixed() method of the Msg +func TestMsg_checkUserAgent(t *testing.T) { + t.Run("default user agent should be set", func(t *testing.T) { + message := testMessage(t) + message.checkUserAgent() + checkGenHeader(t, message, HeaderUserAgent, "checkUserAgent", 0, 1, + fmt.Sprintf("go-mail v%s // https://github.com/wneessen/go-mail", VERSION)) + checkGenHeader(t, message, HeaderXMailer, "checkUserAgent", 0, 1, + fmt.Sprintf("go-mail v%s // https://github.com/wneessen/go-mail", VERSION)) + }) + t.Run("noDefaultUserAgent should return empty string", func(t *testing.T) { + message := testMessage(t) + message.noDefaultUserAgent = true + message.checkUserAgent() + if len(message.genHeader[HeaderUserAgent]) != 0 { + t.Error("user agent should be empty") + } + if len(message.genHeader[HeaderXMailer]) != 0 { + t.Error("x-mailer should be empty") + } + }) +} - func TestMsg_hasMixed(t *testing.T) { - m := NewMsg() - m.SetBodyString(TypeTextPlain, "Plain") - m.AttachFile("README.md") - if !m.hasMixed() { - t.Errorf("mail has mixed parts but hasMixed() returned true") +func TestMsg_addDefaultHeader(t *testing.T) { + t.Run("empty message should add defaults", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("failed to create new message") } - } + if _, ok := message.genHeader[HeaderDate]; ok { + t.Error("empty message should not have date header") + } + if _, ok := message.genHeader[HeaderMessageID]; ok { + t.Error("empty message should not have message-id header") + } + if _, ok := message.genHeader[HeaderMIMEVersion]; ok { + t.Error("empty message should not have mime version header") + } + message.addDefaultHeader() + if _, ok := message.genHeader[HeaderDate]; !ok { + t.Error("message should now have date header") + } + if _, ok := message.genHeader[HeaderMessageID]; !ok { + t.Error("message should now have message-id header") + } + if _, ok := message.genHeader[HeaderMIMEVersion]; !ok { + t.Error("message should now have mime version header") + } + }) +} -// TestMsg_UpdateReader tests the Msg.UpdateReader method - - func TestMsg_UpdateReader(t *testing.T) { - m := NewMsg() - m.Subject("Subject-Run 1") - mr := m.NewReader() - if mr == nil { - t.Errorf("NewReader failed: Reader is nil") - } - wbuf1 := bytes.Buffer{} - _, err := io.Copy(&wbuf1, mr) - if err != nil { - t.Errorf("io.Copy on Reader failed: %s", err) - } - if !strings.Contains(wbuf1.String(), "Subject: Subject-Run 1") { - t.Errorf("io.Copy on Reader failed. Expected to find %q but string in Subject was not found", - "Subject-Run 1") - } - - m.Subject("Subject-Run 2") - m.UpdateReader(mr) - wbuf2 := bytes.Buffer{} - _, err = io.Copy(&wbuf2, mr) - if err != nil { - t.Errorf("2nd io.Copy on Reader failed: %s", err) - } - if !strings.Contains(wbuf2.String(), "Subject: Subject-Run 2") { - t.Errorf("io.Copy on Reader failed. Expected to find %q but string in Subject was not found", - "Subject-Run 2") - } - } - -// TestMsg_GetGenHeader will test the GetGenHeader method of the Msg - - func TestMsg_GetGenHeader(t *testing.T) { - m := NewMsg() - m.Subject("this is a test") - sa := m.GetGenHeader(HeaderSubject) - if len(sa) <= 0 { - t.Errorf("GetGenHeader on subject failed. Got empty slice") - return - } - if sa[0] == "" { - t.Errorf("GetGenHeader on subject failed. Got empty value") - } - if sa[0] != "this is a test" { - t.Errorf("GetGenHeader on subject failed. Expected: %q, got: %q", "this is a test", sa[0]) - } - } - -// TestMsg_GetAddrHeader will test the Msg.GetAddrHeader method - - func TestMsg_GetAddrHeader(t *testing.T) { - m := NewMsg() - if err := m.FromFormat("Toni Sender", "sender@example.com"); err != nil { - t.Errorf("failed to set FROM address: %s", err) - } - if err := m.AddToFormat("Toni To", "to@example.com"); err != nil { - t.Errorf("failed to set TO address: %s", err) - } - if err := m.AddCcFormat("Toni Cc", "cc@example.com"); err != nil { - t.Errorf("failed to set CC address: %s", err) - } - if err := m.AddBccFormat("Toni Bcc", "bcc@example.com"); err != nil { - t.Errorf("failed to set BCC address: %s", err) - } - fh := m.GetAddrHeader(HeaderFrom) - if len(fh) <= 0 { - t.Errorf("GetAddrHeader on FROM failed. Got empty slice") - return - } - if fh[0].String() == "" { - t.Errorf("GetAddrHeader on FROM failed. Got empty value") - } - if fh[0].String() != `"Toni Sender" ` { - t.Errorf("GetAddrHeader on FROM failed. Expected: %q, got: %q", - `"Toni Sender" "`, fh[0].String()) - } - th := m.GetAddrHeader(HeaderTo) - if len(th) <= 0 { - t.Errorf("GetAddrHeader on TO failed. Got empty slice") - return - } - if th[0].String() == "" { - t.Errorf("GetAddrHeader on TO failed. Got empty value") - } - if th[0].String() != `"Toni To" ` { - t.Errorf("GetAddrHeader on TO failed. Expected: %q, got: %q", - `"Toni To" "`, th[0].String()) - } - ch := m.GetAddrHeader(HeaderCc) - if len(ch) <= 0 { - t.Errorf("GetAddrHeader on CC failed. Got empty slice") - return - } - if ch[0].String() == "" { - t.Errorf("GetAddrHeader on CC failed. Got empty value") - } - if ch[0].String() != `"Toni Cc" ` { - t.Errorf("GetAddrHeader on CC failed. Expected: %q, got: %q", - `"Toni Cc" "`, ch[0].String()) - } - bh := m.GetAddrHeader(HeaderBcc) - if len(bh) <= 0 { - t.Errorf("GetAddrHeader on BCC failed. Got empty slice") - return - } - if bh[0].String() == "" { - t.Errorf("GetAddrHeader on BCC failed. Got empty value") - } - if bh[0].String() != `"Toni Bcc" ` { - t.Errorf("GetAddrHeader on BCC failed. Expected: %q, got: %q", - `"Toni Bcc" "`, bh[0].String()) - } - } - -// TestMsg_GetFrom will test the Msg.GetFrom method - - func TestMsg_GetFrom(t *testing.T) { - m := NewMsg() - if err := m.FromFormat("Toni Sender", "sender@example.com"); err != nil { - t.Errorf("failed to set FROM address: %s", err) - } - fh := m.GetFrom() - if len(fh) <= 0 { - t.Errorf("GetFrom failed. Got empty slice") - return - } - if fh[0].String() == "" { - t.Errorf("GetFrom failed. Got empty value") - } - if fh[0].String() != `"Toni Sender" ` { - t.Errorf("GetFrom failed. Expected: %q, got: %q", - `"Toni Sender" "`, fh[0].String()) - } - } - -// TestMsg_GetFromString will test the Msg.GetFromString method - - func TestMsg_GetFromString(t *testing.T) { - m := NewMsg() - if err := m.FromFormat("Toni Sender", "sender@example.com"); err != nil { - t.Errorf("failed to set FROM address: %s", err) - } - fh := m.GetFromString() - if len(fh) <= 0 { - t.Errorf("GetFromString failed. Got empty slice") - return - } - if fh[0] == "" { - t.Errorf("GetFromString failed. Got empty value") - } - if fh[0] != `"Toni Sender" ` { - t.Errorf("GetFromString failed. Expected: %q, got: %q", - `"Toni Sender" "`, fh[0]) - } - } - -// TestMsg_GetTo will test the Msg.GetTo method - - func TestMsg_GetTo(t *testing.T) { - m := NewMsg() - if err := m.AddToFormat("Toni To", "to@example.com"); err != nil { - t.Errorf("failed to set TO address: %s", err) - } - fh := m.GetTo() - if len(fh) <= 0 { - t.Errorf("GetTo failed. Got empty slice") - return - } - if fh[0].String() == "" { - t.Errorf("GetTo failed. Got empty value") - } - if fh[0].String() != `"Toni To" ` { - t.Errorf("GetTo failed. Expected: %q, got: %q", - `"Toni To" "`, fh[0].String()) - } - } - -// TestMsg_GetToString will test the Msg.GetToString method - - func TestMsg_GetToString(t *testing.T) { - m := NewMsg() - if err := m.AddToFormat("Toni To", "to@example.com"); err != nil { - t.Errorf("failed to set TO address: %s", err) - } - fh := m.GetToString() - if len(fh) <= 0 { - t.Errorf("GetToString failed. Got empty slice") - return - } - if fh[0] == "" { - t.Errorf("GetToString failed. Got empty value") - } - if fh[0] != `"Toni To" ` { - t.Errorf("GetToString failed. Expected: %q, got: %q", - `"Toni To" "`, fh[0]) - } - } - -// TestMsg_GetCc will test the Msg.GetCc method - - func TestMsg_GetCc(t *testing.T) { - m := NewMsg() - if err := m.AddCcFormat("Toni Cc", "cc@example.com"); err != nil { - t.Errorf("failed to set TO address: %s", err) - } - fh := m.GetCc() - if len(fh) <= 0 { - t.Errorf("GetCc failed. Got empty slice") - return - } - if fh[0].String() == "" { - t.Errorf("GetCc failed. Got empty value") - } - if fh[0].String() != `"Toni Cc" ` { - t.Errorf("GetCc failed. Expected: %q, got: %q", - `"Toni Cc" "`, fh[0].String()) - } - } - -// TestMsg_GetCcString will test the Msg.GetCcString method - - func TestMsg_GetCcString(t *testing.T) { - m := NewMsg() - if err := m.AddCcFormat("Toni Cc", "cc@example.com"); err != nil { - t.Errorf("failed to set TO address: %s", err) - } - fh := m.GetCcString() - if len(fh) <= 0 { - t.Errorf("GetCcString failed. Got empty slice") - return - } - if fh[0] == "" { - t.Errorf("GetCcString failed. Got empty value") - } - if fh[0] != `"Toni Cc" ` { - t.Errorf("GetCcString failed. Expected: %q, got: %q", - `"Toni Cc" "`, fh[0]) - } - } - -// TestMsg_GetBcc will test the Msg.GetBcc method - - func TestMsg_GetBcc(t *testing.T) { - m := NewMsg() - if err := m.AddBccFormat("Toni Bcc", "bcc@example.com"); err != nil { - t.Errorf("failed to set TO address: %s", err) - } - fh := m.GetBcc() - if len(fh) <= 0 { - t.Errorf("GetBcc failed. Got empty slice") - return - } - if fh[0].String() == "" { - t.Errorf("GetBcc failed. Got empty value") - } - if fh[0].String() != `"Toni Bcc" ` { - t.Errorf("GetBcc failed. Expected: %q, got: %q", - `"Toni Cc" "`, fh[0].String()) - } - } - -// TestMsg_GetBccString will test the Msg.GetBccString method - - func TestMsg_GetBccString(t *testing.T) { - m := NewMsg() - if err := m.AddBccFormat("Toni Bcc", "bcc@example.com"); err != nil { - t.Errorf("failed to set TO address: %s", err) - } - fh := m.GetBccString() - if len(fh) <= 0 { - t.Errorf("GetBccString failed. Got empty slice") - return - } - if fh[0] == "" { - t.Errorf("GetBccString failed. Got empty value") - } - if fh[0] != `"Toni Bcc" ` { - t.Errorf("GetBccString failed. Expected: %q, got: %q", - `"Toni Cc" "`, fh[0]) - } - } - -// TestMsg_GetBoundary will test the Msg.GetBoundary method - - func TestMsg_GetBoundary(t *testing.T) { - b := "random_boundary_string" - m := NewMsg() - if boundary := m.GetBoundary(); boundary != "" { - t.Errorf("GetBoundary failed. Expected empty string, but got: %s", boundary) - } - m = NewMsg(WithBoundary(b)) - if boundary := m.GetBoundary(); boundary != b { - t.Errorf("GetBoundary failed. Expected boundary: %s, got: %s", b, boundary) - } - } - -// TestMsg_AttachEmbedReader_consecutive tests the Msg.AttachReader and Msg.EmbedReader -// methods with consecutive calls to Msg.WriteTo to make sure the attachments are not -// lost (see Github issue #110) - - func TestMsg_AttachEmbedReader_consecutive(t *testing.T) { - ts1 := "This is a test string" - ts2 := "Another test string" - m := NewMsg() - if err := m.AttachReader("attachment.txt", bytes.NewBufferString(ts1)); err != nil { - t.Errorf("AttachReader() failed. Expected no error, got: %s", err.Error()) - return - } - if err := m.EmbedReader("embedded.txt", bytes.NewBufferString(ts2)); err != nil { - t.Errorf("EmbedReader() failed. Expected no error, got: %s", err.Error()) - return - } - obuf1 := &bytes.Buffer{} - obuf2 := &bytes.Buffer{} - _, err := m.WriteTo(obuf1) - if err != nil { - t.Errorf("WriteTo to first output buffer failed: %s", err) - } - _, err = m.WriteTo(obuf2) - if err != nil { - t.Errorf("WriteTo to second output buffer failed: %s", err) - } - if !strings.Contains(obuf1.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { - t.Errorf("Expected file attachment string not found in first output buffer") - } - if !strings.Contains(obuf2.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { - t.Errorf("Expected file attachment string not found in second output buffer") - } - if !strings.Contains(obuf1.String(), "QW5vdGhlciB0ZXN0IHN0cmluZw==") { - t.Errorf("Expected embedded file string not found in first output buffer") - } - if !strings.Contains(obuf2.String(), "QW5vdGhlciB0ZXN0IHN0cmluZw==") { - t.Errorf("Expected embded file string not found in second output buffer") - } - } - -// TestMsg_AttachEmbedReadSeeker_consecutive tests the Msg.AttachReadSeeker and -// Msg.EmbedReadSeeker methods with consecutive calls to Msg.WriteTo to make -// sure the attachments are not lost (see Github issue #110) - - func TestMsg_AttachEmbedReadSeeker_consecutive(t *testing.T) { - ts1 := []byte("This is a test string") - ts2 := []byte("Another test string") - m := NewMsg() - m.AttachReadSeeker("attachment.txt", bytes.NewReader(ts1)) - m.EmbedReadSeeker("embedded.txt", bytes.NewReader(ts2)) - obuf1 := &bytes.Buffer{} - obuf2 := &bytes.Buffer{} - _, err := m.WriteTo(obuf1) - if err != nil { - t.Errorf("WriteTo to first output buffer failed: %s", err) - } - _, err = m.WriteTo(obuf2) - if err != nil { - t.Errorf("WriteTo to second output buffer failed: %s", err) - } - if !strings.Contains(obuf1.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { - t.Errorf("Expected file attachment string not found in first output buffer") - } - if !strings.Contains(obuf2.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { - t.Errorf("Expected file attachment string not found in second output buffer") - } - if !strings.Contains(obuf1.String(), "QW5vdGhlciB0ZXN0IHN0cmluZw==") { - t.Errorf("Expected embedded file string not found in first output buffer") - } - if !strings.Contains(obuf2.String(), "QW5vdGhlciB0ZXN0IHN0cmluZw==") { - t.Errorf("Expected embded file string not found in second output buffer") - } - } - -// TestMsg_AttachReadSeeker tests the Msg.AttachReadSeeker method - - func TestMsg_AttachReadSeeker(t *testing.T) { - m := NewMsg() - ts := []byte("This is a test string") - r := bytes.NewReader(ts) - m.AttachReadSeeker("testfile.txt", r) - if len(m.attachments) != 1 { - t.Errorf("AttachReadSeeker() failed. Number of attachments expected: %d, got: %d", 1, - len(m.attachments)) - return - } - file := m.attachments[0] - if file == nil { - t.Errorf("AttachReadSeeker() failed. Attachment file pointer is nil") - return - } - if file.Name != "testfile.txt" { - t.Errorf("AttachReadSeeker() failed. Expected file name: %s, got: %s", "testfile.txt", - file.Name) - } - wbuf := bytes.Buffer{} - if _, err := file.Writer(&wbuf); err != nil { - t.Errorf("execute WriterFunc failed: %s", err) - } - if wbuf.String() != string(ts) { - t.Errorf("AttachReadSeeker() failed. Expected string: %q, got: %q", ts, wbuf.String()) - } - } - -// TestMsg_EmbedReadSeeker tests the Msg.EmbedReadSeeker method - - func TestMsg_EmbedReadSeeker(t *testing.T) { - m := NewMsg() - ts := []byte("This is a test string") - r := bytes.NewReader(ts) - m.EmbedReadSeeker("testfile.txt", r) - if len(m.embeds) != 1 { - t.Errorf("EmbedReadSeeker() failed. Number of attachments expected: %d, got: %d", 1, - len(m.embeds)) - return - } - file := m.embeds[0] - if file == nil { - t.Errorf("EmbedReadSeeker() failed. Embedded file pointer is nil") - return - } - if file.Name != "testfile.txt" { - t.Errorf("EmbedReadSeeker() failed. Expected file name: %s, got: %s", "testfile.txt", - file.Name) - } - wbuf := bytes.Buffer{} - if _, err := file.Writer(&wbuf); err != nil { - t.Errorf("execute WriterFunc failed: %s", err) - } - if wbuf.String() != string(ts) { - t.Errorf("EmbedReadSeeker() failed. Expected string: %q, got: %q", ts, wbuf.String()) - } - } - -// TestMsg_ToFromString tests Msg.ToFromString in different scenarios - - func TestMsg_ToFromString(t *testing.T) { - tests := []struct { - n string - v string - w []*mail.Address - sf bool - }{ - {"valid single address", "test@test.com", []*mail.Address{ - {Name: "", Address: "test@test.com"}, - }, false}, - { - "valid multiple addresses", "test@test.com,test2@example.com", - []*mail.Address{ - {Name: "", Address: "test@test.com"}, - {Name: "", Address: "test2@example.com"}, - }, - false, - }, - { - "valid multiple addresses with space and name", - `test@test.com, "Toni Tester" `, - []*mail.Address{ - {Name: "", Address: "test@test.com"}, - {Name: "Toni Tester", Address: "test2@example.com"}, - }, - false, - }, - { - "invalid and valid multiple addresses", "test@test.com,test2#example.com", nil, - true, - }, - } - - for _, tt := range tests { - t.Run(tt.n, func(t *testing.T) { - m := NewMsg() - if err := m.ToFromString(tt.v); err != nil && !tt.sf { - t.Errorf("Msg.ToFromString failed: %s", err) - return - } - mto := m.GetTo() - if len(mto) != len(tt.w) { - t.Errorf("Msg.ToFromString failed, expected len: %d, got: %d", len(tt.w), - len(mto)) - return - } - for i := range mto { - w := tt.w[i] - g := mto[i] - if w.String() != g.String() { - t.Errorf("Msg.ToFromString failed, expected address: %s, got: %s", - w.String(), g.String()) - } - } - }) - } - } - -// TestMsg_CcFromString tests Msg.CcFromString in different scenarios - - func TestMsg_CcFromString(t *testing.T) { - tests := []struct { - n string - v string - w []*mail.Address - sf bool - }{ - {"valid single address", "test@test.com", []*mail.Address{ - {Name: "", Address: "test@test.com"}, - }, false}, - { - "valid multiple addresses", "test@test.com,test2@example.com", - []*mail.Address{ - {Name: "", Address: "test@test.com"}, - {Name: "", Address: "test2@example.com"}, - }, - false, - }, - { - "valid multiple addresses with space and name", - `test@test.com, "Toni Tester" `, - []*mail.Address{ - {Name: "", Address: "test@test.com"}, - {Name: "Toni Tester", Address: "test2@example.com"}, - }, - false, - }, - { - "invalid and valid multiple addresses", "test@test.com,test2#example.com", nil, - true, - }, - } - - for _, tt := range tests { - t.Run(tt.n, func(t *testing.T) { - m := NewMsg() - if err := m.CcFromString(tt.v); err != nil && !tt.sf { - t.Errorf("Msg.CcFromString failed: %s", err) - return - } - mto := m.GetCc() - if len(mto) != len(tt.w) { - t.Errorf("Msg.CcFromString failed, expected len: %d, got: %d", len(tt.w), - len(mto)) - return - } - for i := range mto { - w := tt.w[i] - g := mto[i] - if w.String() != g.String() { - t.Errorf("Msg.CcFromString failed, expected address: %s, got: %s", - w.String(), g.String()) - } - } - }) - } - } - -// TestMsg_BccFromString tests Msg.BccFromString in different scenarios - - func TestMsg_BccFromString(t *testing.T) { - tests := []struct { - n string - v string - w []*mail.Address - sf bool - }{ - {"valid single address", "test@test.com", []*mail.Address{ - {Name: "", Address: "test@test.com"}, - }, false}, - { - "valid multiple addresses", "test@test.com,test2@example.com", - []*mail.Address{ - {Name: "", Address: "test@test.com"}, - {Name: "", Address: "test2@example.com"}, - }, - false, - }, - { - "valid multiple addresses with space and name", - `test@test.com, "Toni Tester" `, - []*mail.Address{ - {Name: "", Address: "test@test.com"}, - {Name: "Toni Tester", Address: "test2@example.com"}, - }, - false, - }, - { - "invalid and valid multiple addresses", "test@test.com,test2#example.com", nil, - true, - }, - } - - for _, tt := range tests { - t.Run(tt.n, func(t *testing.T) { - m := NewMsg() - if err := m.BccFromString(tt.v); err != nil && !tt.sf { - t.Errorf("Msg.BccFromString failed: %s", err) - return - } - mto := m.GetBcc() - if len(mto) != len(tt.w) { - t.Errorf("Msg.BccFromString failed, expected len: %d, got: %d", len(tt.w), - len(mto)) - return - } - for i := range mto { - w := tt.w[i] - g := mto[i] - if w.String() != g.String() { - t.Errorf("Msg.BccFromString failed, expected address: %s, got: %s", - w.String(), g.String()) - } - } - }) - } - } - -// TestMsg_checkUserAgent tests the checkUserAgent method of the Msg - - func TestMsg_checkUserAgent(t *testing.T) { - tests := []struct { - name string - noDefaultUserAgent bool - genHeader map[Header][]string - wantUserAgent string - sf bool - }{ - { - name: "check default user agent", - noDefaultUserAgent: false, - wantUserAgent: fmt.Sprintf("go-mail v%s // https://github.com/wneessen/go-mail", VERSION), - sf: false, - }, - { - name: "check no default user agent", - noDefaultUserAgent: true, - wantUserAgent: "", - sf: true, - }, - { - name: "check if ua and xm is already set", - noDefaultUserAgent: false, - genHeader: map[Header][]string{ - HeaderUserAgent: {"custom UA"}, - HeaderXMailer: {"custom XM"}, - }, - wantUserAgent: "custom UA", - sf: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - msg := &Msg{ - noDefaultUserAgent: tt.noDefaultUserAgent, - genHeader: tt.genHeader, - } - msg.checkUserAgent() - gotUserAgent := "" - if val, ok := msg.genHeader[HeaderUserAgent]; ok { - gotUserAgent = val[0] // Assuming the first one is the needed value - } - if gotUserAgent != tt.wantUserAgent && !tt.sf { - t.Errorf("UserAgent got = %v, want = %v", gotUserAgent, tt.wantUserAgent) - } - }) - } - } - -// TestNewMsgWithMIMEVersion tests WithMIMEVersion and Msg.SetMIMEVersion - - func TestNewMsgWithNoDefaultUserAgent(t *testing.T) { - m := NewMsg(WithNoDefaultUserAgent()) - if m.noDefaultUserAgent != true { - t.Errorf("WithNoDefaultUserAgent() failed. Expected: %t, got: %t", true, false) - } - } - -// Fuzzing tests - - func FuzzMsg_Subject(f *testing.F) { - f.Add("Testsubject") - f.Add("") - f.Add("This is a longer test subject.") - f.Add("Let's add some umlauts: üäöß") - f.Add("Or even emojis: ☝️💪👍") - f.Fuzz(func(t *testing.T, data string) { - m := NewMsg() - m.Subject(data) - m.Reset() - }) - } - - func FuzzMsg_From(f *testing.F) { - f.Add("Toni Tester ") - f.Add("") - f.Add("mail@server.com") - f.Fuzz(func(t *testing.T, data string) { - m := NewMsg() - if err := m.From(data); err != nil && - !strings.Contains(err.Error(), "failed to parse mail address") { - t.Errorf("failed set set FROM address: %s", err) - } - m.Reset() - }) - } -*/ +// uppercaseMiddleware is a middleware type that transforms the subject to uppercase. type uppercaseMiddleware struct{} +// Handle satisfies the Middleware interface for the uppercaseMiddlware func (mw uppercaseMiddleware) Handle(m *Msg) *Msg { s, ok := m.genHeader[HeaderSubject] if !ok { @@ -6826,12 +6456,15 @@ func (mw uppercaseMiddleware) Handle(m *Msg) *Msg { return m } +// Type satisfies the Middleware interface for the uppercaseMiddlware func (mw uppercaseMiddleware) Type() MiddlewareType { return "uppercase" } +// encodeMiddleware is a middleware type that transforms an "a" in the subject to an "@" type encodeMiddleware struct{} +// Handle satisfies the Middleware interface for the encodeMiddleware func (mw encodeMiddleware) Handle(m *Msg) *Msg { s, ok := m.genHeader[HeaderSubject] if !ok { @@ -6844,6 +6477,7 @@ func (mw encodeMiddleware) Handle(m *Msg) *Msg { return m } +// Type satisfies the Middleware interface for the encodeMiddleware func (mw encodeMiddleware) Type() MiddlewareType { return "encode" } @@ -6927,3 +6561,31 @@ func hasSendmail() bool { } return false } + +// Fuzzing tests +func FuzzMsg_Subject(f *testing.F) { + f.Add("Testsubject") + f.Add("") + f.Add("This is a longer test subject.") + f.Add("Let's add some umlauts: üäöß") + f.Add("Or even emojis: ☝️💪👍") + f.Fuzz(func(t *testing.T, data string) { + m := NewMsg() + m.Subject(data) + m.Reset() + }) +} + +func FuzzMsg_From(f *testing.F) { + f.Add("Toni Tester ") + f.Add("") + f.Add("mail@server.com") + f.Fuzz(func(t *testing.T, data string) { + m := NewMsg() + if err := m.From(data); err != nil && + !strings.Contains(err.Error(), "failed to parse mail address") { + t.Errorf("failed set set FROM address: %s", err) + } + m.Reset() + }) +} From 61244a541e5a7affb7f030a4e9bc74abab0cc6c8 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 20:52:38 +0100 Subject: [PATCH 180/188] 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. --- client_test.go | 4 +- msgwriter_test.go | 410 +++++++++++++++++++++++++++++++--------------- 2 files changed, 278 insertions(+), 136 deletions(-) diff --git a/client_test.go b/client_test.go index 8408035..1ba0fc9 100644 --- a/client_test.go +++ b/client_test.go @@ -3528,9 +3528,9 @@ func parseJSONLog(t *testing.T, buf *bytes.Buffer) logData { } // testMessage configures and returns a new email message for testing, initializing it with valid sender and recipient. -func testMessage(t *testing.T) *Msg { +func testMessage(t *testing.T, opts ...MsgOption) *Msg { t.Helper() - message := NewMsg() + message := NewMsg(opts...) if message == nil { t.Fatal("failed to create new message") } diff --git a/msgwriter_test.go b/msgwriter_test.go index a41e5d3..babb52a 100644 --- a/msgwriter_test.go +++ b/msgwriter_test.go @@ -6,151 +6,293 @@ package mail import ( "bytes" + "errors" "fmt" - "io" "mime" "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" `) - _ = m.To(`"Toni Receiver" `) - 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: `) { - 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" `) { - ea = append(ea, "From") - } - if !strings.Contains(ms, `To: "Toni Receiver" `) { - 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" `) - _ = m.To(`"Toni Receiver" `) - 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" `) - _ = m.To(`"Toni Receiver" `) - 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: \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, "

Testmail

") + 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()) + } + }) } From 5c8d07407f18052dbaf23a1975cad869abdfc6de Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 21:13:36 +0100 Subject: [PATCH 181/188] 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. --- msgwriter_test.go | 134 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/msgwriter_test.go b/msgwriter_test.go index babb52a..e0bae95 100644 --- a/msgwriter_test.go +++ b/msgwriter_test.go @@ -296,3 +296,137 @@ func TestMsgWriter_writePreformattedGenHeader(t *testing.T) { } }) } + +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) + } + 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: 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) + } + 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) + } + 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()) + } + if !strings.Contains(buffer.String(), `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) + } + if !strings.Contains(buffer.String(), "\r\n\r\nVGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo=") { + 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(), `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) + } + if !strings.Contains(buffer.String(), "\r\n\r\nVGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo=") { + 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(), `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()) + } + }) +} From 8e11fabbaf5528f9783e13d5db5e06410d2e1e30 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 21:14:05 +0100 Subject: [PATCH 182/188] 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. --- client_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/client_test.go b/client_test.go index 1ba0fc9..664f0fd 100644 --- a/client_test.go +++ b/client_test.go @@ -30,8 +30,6 @@ import ( const ( // DefaultHost is used as default hostname for the Client DefaultHost = "127.0.0.1" - // TestRcpt is a trash mail address to send test mails to - TestRcpt = "couttifaddebro-1473@yopmail.com" // TestServerProto is the protocol used for the simple SMTP test server TestServerProto = "tcp" // TestServerAddr is the address the simple SMTP test server listens on From dedb0e36c863257d1109f565111d6eb0b5485204 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 21:25:22 +0100 Subject: [PATCH 183/188] 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. --- msgwriter_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/msgwriter_test.go b/msgwriter_test.go index e0bae95..6b9a9d7 100644 --- a/msgwriter_test.go +++ b/msgwriter_test.go @@ -430,3 +430,41 @@ func TestMsgWriter_addFiles(t *testing.T) { } }) } + +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()) + } + }) +} From b7a87fb15b50d2f5365f3c084616b2d4e2f1fa34 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 21:30:50 +0100 Subject: [PATCH 184/188] 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. --- msgwriter_test.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/msgwriter_test.go b/msgwriter_test.go index 6b9a9d7..10650e5 100644 --- a/msgwriter_test.go +++ b/msgwriter_test.go @@ -468,3 +468,34 @@ func TestMsgWriter_writePart(t *testing.T) { } }) } + +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()) + } + }) +} From a30215ebcee20eeb106b6e57ce8a5ff4ab79ac4c Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 22:02:43 +0100 Subject: [PATCH 185/188] 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. --- msgwriter_test.go | 113 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/msgwriter_test.go b/msgwriter_test.go index 10650e5..111023f 100644 --- a/msgwriter_test.go +++ b/msgwriter_test.go @@ -8,6 +8,7 @@ import ( "bytes" "errors" "fmt" + "io" "mime" "strings" "testing" @@ -499,3 +500,115 @@ func TestMsgWriter_writeString(t *testing.T) { } }) } + +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) + } + }) +} From 3a046d728e7f399aa388d82140a6a89573e39ac1 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 22:08:35 +0100 Subject: [PATCH 186/188] 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. --- testdata/attachment | 1 + testdata/attachment.license | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 testdata/attachment create mode 100644 testdata/attachment.license diff --git a/testdata/attachment b/testdata/attachment new file mode 100644 index 0000000..fc21731 --- /dev/null +++ b/testdata/attachment @@ -0,0 +1 @@ +This is a test attachment diff --git a/testdata/attachment.license b/testdata/attachment.license new file mode 100644 index 0000000..615ea2b --- /dev/null +++ b/testdata/attachment.license @@ -0,0 +1,3 @@ +// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors +// +// SPDX-License-Identifier: MIT From b63a3dab9ad39b2d2011ffc50f34328137c2596b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 22:20:09 +0100 Subject: [PATCH 187/188] 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. --- msgwriter_test.go | 100 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 18 deletions(-) diff --git a/msgwriter_test.go b/msgwriter_test.go index 111023f..05de1e7 100644 --- a/msgwriter_test.go +++ b/msgwriter_test.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "mime" + "runtime" "strings" "testing" "time" @@ -312,14 +313,28 @@ func TestMsgWriter_addFiles(t *testing.T) { if msgwriter.err != nil { t.Errorf("msgWriter failed to write: %s", msgwriter.err) } - if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo=") { - t.Errorf("attachment not found in mail message. Mail: %s", buffer.String()) + switch runtime.GOOS { + case "windows": + if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo") { + 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: text/plain; charset=utf-8; name="attachment.txt"`) { - t.Errorf("Content-Type 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) { @@ -331,8 +346,15 @@ func TestMsgWriter_addFiles(t *testing.T) { if msgwriter.err != nil { t.Errorf("msgWriter failed to write: %s", msgwriter.err) } - if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo=") { - t.Errorf("attachment not found in mail message. Mail: %s", buffer.String()) + switch runtime.GOOS { + case "windows": + if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo") { + 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()) @@ -350,8 +372,15 @@ func TestMsgWriter_addFiles(t *testing.T) { if msgwriter.err != nil { t.Errorf("msgWriter failed to write: %s", msgwriter.err) } - if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo=") { - t.Errorf("attachment not found in mail message. Mail: %s", buffer.String()) + switch runtime.GOOS { + case "windows": + if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo") { + 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()) @@ -375,8 +404,15 @@ func TestMsgWriter_addFiles(t *testing.T) { 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(), `text/plain; charset=utf-8; name="attachment.txt"`) { - t.Errorf("Content-Type 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()) @@ -391,14 +427,28 @@ func TestMsgWriter_addFiles(t *testing.T) { if msgwriter.err != nil { t.Errorf("msgWriter failed to write: %s", msgwriter.err) } - if !strings.Contains(buffer.String(), "\r\n\r\nVGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo=") { - t.Errorf("attachment not found in mail message. Mail: %s", buffer.String()) + switch runtime.GOOS { + case "windows": + if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo") { + 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(), `text/plain; charset=utf-8; name="attachment.txt"`) { - t.Errorf("Content-Type 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()) @@ -417,14 +467,28 @@ func TestMsgWriter_addFiles(t *testing.T) { if msgwriter.err != nil { t.Errorf("msgWriter failed to write: %s", msgwriter.err) } - if !strings.Contains(buffer.String(), "\r\n\r\nVGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo=") { - t.Errorf("attachment not found in mail message. Mail: %s", buffer.String()) + switch runtime.GOOS { + case "windows": + if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo") { + 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(), `text/plain; charset=utf-8; name="attachment.txt"`) { - t.Errorf("Content-Type 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()) From c9f8a2acddfb35c9a8277a87bab1f73ff13ea95a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 28 Oct 2024 22:28:23 +0100 Subject: [PATCH 188/188] 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. --- msgwriter_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/msgwriter_test.go b/msgwriter_test.go index 05de1e7..330507b 100644 --- a/msgwriter_test.go +++ b/msgwriter_test.go @@ -315,7 +315,7 @@ func TestMsgWriter_addFiles(t *testing.T) { } switch runtime.GOOS { case "windows": - if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo") { + if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudA0K") { t.Errorf("attachment not found in mail message. Mail: %s", buffer.String()) } default: @@ -348,7 +348,7 @@ func TestMsgWriter_addFiles(t *testing.T) { } switch runtime.GOOS { case "windows": - if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo") { + if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudA0K") { t.Errorf("attachment not found in mail message. Mail: %s", buffer.String()) } default: @@ -374,7 +374,7 @@ func TestMsgWriter_addFiles(t *testing.T) { } switch runtime.GOOS { case "windows": - if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo") { + if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudA0K") { t.Errorf("attachment not found in mail message. Mail: %s", buffer.String()) } default: @@ -429,7 +429,7 @@ func TestMsgWriter_addFiles(t *testing.T) { } switch runtime.GOOS { case "windows": - if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo") { + if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudA0K") { t.Errorf("attachment not found in mail message. Mail: %s", buffer.String()) } default: @@ -469,7 +469,7 @@ func TestMsgWriter_addFiles(t *testing.T) { } switch runtime.GOOS { case "windows": - if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudAo") { + if !strings.Contains(buffer.String(), "VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudA0K") { t.Errorf("attachment not found in mail message. Mail: %s", buffer.String()) } default: