mirror of
https://github.com/nabbar/golib.git
synced 2025-12-24 11:51:02 +08:00
Improvements, test & documentatons (2025-11 #2)
[root] - UPDATE documentation: enhanced README and TESTING guidelines - UPDATE dependencies: bump dependencies [config/components] - UPDATE mail component: apply update following changes in related package - UPDATE smtp component: apply update following changes in related package [mail] - MAJOR REFACTORING - REFACTOR package structure: reorganized into 4 specialized subpackages (queuer, render, sender, smtp) - ADD mail/queuer: mail queue management with counter, monitoring, and comprehensive tests - ADD mail/render: email template rendering with themes and direction handling (moved from mailer package) - ADD mail/sender: email composition and sending with attachments, priorities, and encoding - ADD mail/smtp: SMTP protocol handling with TLS modes and DSN support - ADD documentation: comprehensive README and TESTING for all subpackages - ADD tests: complete test suites with benchmarks, concurrency, and edge cases for all subpackages [mailer] - DEPRECATED - DELETE package: entire package merged into mail/render [mailPooler] - DEPRECATED - DELETE package: entire package merged into mail/queuer [smtp] - DEPRECATED - DELETE root package: entire package moved to mail/smtp - REFACTOR tlsmode: enhanced with encoding, formatting, and viper support (moved to mail/smtp/tlsmode) [size] - ADD documentation: comprehensive README - UPDATE interface: improved Size type methods - UPDATE encoding: enhanced marshaling support - UPDATE formatting: better unit handling and display - UPDATE parsing: improved error handling and validation [socket/server/unix] - ADD platform support: macOS-specific permission handling (perm_darwin.go) - ADD platform support: Linux-specific permission handling (perm_linux.go) - UPDATE listener: improved Unix socket and datagram listeners - UPDATE error handling: enhanced error messages for Unix sockets [socket/server/unixgram] - ADD platform support: macOS-specific permission handling (perm_darwin.go) - ADD platform support: Linux-specific permission handling (perm_linux.go) - UPDATE listener: improved Unix datagram listener - UPDATE error handling: enhanced error messages [socket/server/tcp] - UPDATE listener: improved TCP listener implementation
This commit is contained in:
1034
mail/render/README.md
Normal file
1034
mail/render/README.md
Normal file
File diff suppressed because it is too large
Load Diff
1012
mail/render/TESTING.md
Normal file
1012
mail/render/TESTING.md
Normal file
File diff suppressed because it is too large
Load Diff
485
mail/render/benchmark_test.go
Normal file
485
mail/render/benchmark_test.go
Normal file
@@ -0,0 +1,485 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 Nicolas JUHEL
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
package render_test
|
||||
|
||||
import (
|
||||
"github.com/go-hermes/hermes/v2"
|
||||
"github.com/nabbar/golib/mail/render"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gmeasure"
|
||||
)
|
||||
|
||||
var _ = Describe("Benchmarks", func() {
|
||||
Describe("Creation Benchmarks", func() {
|
||||
Context("mailer creation performance", func() {
|
||||
It("should benchmark New() creation", func() {
|
||||
experiment := gmeasure.NewExperiment("Mailer Creation")
|
||||
AddReportEntry(experiment.Name, experiment)
|
||||
|
||||
experiment.Sample(func(idx int) {
|
||||
experiment.MeasureDuration("new", func() {
|
||||
mailer := render.New()
|
||||
Expect(mailer).ToNot(BeNil())
|
||||
})
|
||||
}, gmeasure.SamplingConfig{N: 1000, Duration: 0})
|
||||
|
||||
stats := experiment.GetStats("new")
|
||||
AddReportEntry("Mean creation time", stats.DurationFor(gmeasure.StatMean))
|
||||
})
|
||||
|
||||
It("should benchmark Config.NewMailer() creation", func() {
|
||||
experiment := gmeasure.NewExperiment("Config Creation")
|
||||
AddReportEntry(experiment.Name, experiment)
|
||||
|
||||
config := render.Config{
|
||||
Theme: "default",
|
||||
Direction: "ltr",
|
||||
Name: "Test",
|
||||
Link: "https://example.com",
|
||||
Logo: "https://example.com/logo.png",
|
||||
Copyright: "© 2024",
|
||||
TroubleText: "Help",
|
||||
Body: hermes.Body{Name: "User"},
|
||||
}
|
||||
|
||||
experiment.Sample(func(idx int) {
|
||||
experiment.MeasureDuration("config-new", func() {
|
||||
mailer := config.NewMailer()
|
||||
Expect(mailer).ToNot(BeNil())
|
||||
})
|
||||
}, gmeasure.SamplingConfig{N: 1000})
|
||||
|
||||
stats := experiment.GetStats("config-new")
|
||||
AddReportEntry("Mean config creation time", stats.DurationFor(gmeasure.StatMean))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Clone Benchmarks", func() {
|
||||
Context("cloning performance", func() {
|
||||
It("should benchmark simple clone", func() {
|
||||
experiment := gmeasure.NewExperiment("Simple Clone")
|
||||
AddReportEntry(experiment.Name, experiment)
|
||||
|
||||
mailer := render.New()
|
||||
mailer.SetName("Test")
|
||||
body := &hermes.Body{Name: "User"}
|
||||
mailer.SetBody(body)
|
||||
|
||||
experiment.Sample(func(idx int) {
|
||||
experiment.MeasureDuration("clone", func() {
|
||||
clone := mailer.Clone()
|
||||
Expect(clone).ToNot(BeNil())
|
||||
})
|
||||
}, gmeasure.SamplingConfig{N: 1000})
|
||||
|
||||
stats := experiment.GetStats("clone")
|
||||
AddReportEntry("Mean simple clone time", stats.DurationFor(gmeasure.StatMean))
|
||||
})
|
||||
|
||||
It("should benchmark complex clone", func() {
|
||||
experiment := gmeasure.NewExperiment("Complex Clone")
|
||||
AddReportEntry(experiment.Name, experiment)
|
||||
|
||||
mailer := render.New()
|
||||
body := &hermes.Body{
|
||||
Name: "User",
|
||||
Intros: []string{"Intro 1", "Intro 2", "Intro 3"},
|
||||
Outros: []string{"Outro 1", "Outro 2"},
|
||||
Dictionary: []hermes.Entry{
|
||||
{Key: "K1", Value: "V1"},
|
||||
{Key: "K2", Value: "V2"},
|
||||
},
|
||||
Tables: []hermes.Table{{
|
||||
Data: [][]hermes.Entry{
|
||||
{{Key: "C1", Value: "V1"}, {Key: "C2", Value: "V2"}},
|
||||
{{Key: "C1", Value: "V3"}, {Key: "C2", Value: "V4"}},
|
||||
},
|
||||
}},
|
||||
Actions: []hermes.Action{
|
||||
{
|
||||
Instructions: "Test",
|
||||
Button: hermes.Button{Text: "Click", Link: "https://example.com"},
|
||||
},
|
||||
},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
|
||||
experiment.Sample(func(idx int) {
|
||||
experiment.MeasureDuration("clone-complex", func() {
|
||||
clone := mailer.Clone()
|
||||
Expect(clone).ToNot(BeNil())
|
||||
})
|
||||
}, gmeasure.SamplingConfig{N: 500})
|
||||
|
||||
stats := experiment.GetStats("clone-complex")
|
||||
AddReportEntry("Mean complex clone time", stats.DurationFor(gmeasure.StatMean))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Generation Benchmarks", func() {
|
||||
var mailer render.Mailer
|
||||
|
||||
BeforeEach(func() {
|
||||
mailer = render.New()
|
||||
mailer.SetName("Test Company")
|
||||
mailer.SetLink("https://example.com")
|
||||
mailer.SetLogo("https://example.com/logo.png")
|
||||
mailer.SetCopyright("© 2024")
|
||||
mailer.SetTroubleText("Contact us")
|
||||
})
|
||||
|
||||
Context("HTML generation performance", func() {
|
||||
It("should benchmark simple HTML generation", func() {
|
||||
experiment := gmeasure.NewExperiment("Simple HTML")
|
||||
AddReportEntry(experiment.Name, experiment)
|
||||
|
||||
body := &hermes.Body{
|
||||
Name: "User",
|
||||
Intros: []string{"Welcome!"},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
|
||||
experiment.Sample(func(idx int) {
|
||||
experiment.MeasureDuration("html-simple", func() {
|
||||
buf, err := mailer.GenerateHTML()
|
||||
Expect(err).To(BeNil())
|
||||
Expect(buf).ToNot(BeNil())
|
||||
})
|
||||
}, gmeasure.SamplingConfig{N: 100})
|
||||
|
||||
stats := experiment.GetStats("html-simple")
|
||||
AddReportEntry("Mean simple HTML time", stats.DurationFor(gmeasure.StatMean))
|
||||
AddReportEntry("Max simple HTML time", stats.DurationFor(gmeasure.StatMax))
|
||||
})
|
||||
|
||||
It("should benchmark complex HTML generation", func() {
|
||||
experiment := gmeasure.NewExperiment("Complex HTML")
|
||||
AddReportEntry(experiment.Name, experiment)
|
||||
|
||||
body := &hermes.Body{
|
||||
Name: "User",
|
||||
Intros: []string{"Welcome!", "Here's your report"},
|
||||
Dictionary: []hermes.Entry{
|
||||
{Key: "ID", Value: "123"},
|
||||
{Key: "Date", Value: "2024-01-01"},
|
||||
},
|
||||
Tables: []hermes.Table{{
|
||||
Data: [][]hermes.Entry{
|
||||
{{Key: "Item", Value: "A"}, {Key: "Price", Value: "$10"}},
|
||||
{{Key: "Item", Value: "B"}, {Key: "Price", Value: "$20"}},
|
||||
{{Key: "Item", Value: "C"}, {Key: "Price", Value: "$30"}},
|
||||
},
|
||||
}},
|
||||
Actions: []hermes.Action{
|
||||
{
|
||||
Instructions: "View details",
|
||||
Button: hermes.Button{Text: "View", Link: "https://example.com"},
|
||||
},
|
||||
},
|
||||
Outros: []string{"Thank you", "Best regards"},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
|
||||
experiment.Sample(func(idx int) {
|
||||
experiment.MeasureDuration("html-complex", func() {
|
||||
buf, err := mailer.GenerateHTML()
|
||||
Expect(err).To(BeNil())
|
||||
Expect(buf).ToNot(BeNil())
|
||||
})
|
||||
}, gmeasure.SamplingConfig{N: 100})
|
||||
|
||||
stats := experiment.GetStats("html-complex")
|
||||
AddReportEntry("Mean complex HTML time", stats.DurationFor(gmeasure.StatMean))
|
||||
AddReportEntry("Max complex HTML time", stats.DurationFor(gmeasure.StatMax))
|
||||
})
|
||||
})
|
||||
|
||||
Context("plain text generation performance", func() {
|
||||
It("should benchmark plain text generation", func() {
|
||||
experiment := gmeasure.NewExperiment("Plain Text")
|
||||
AddReportEntry(experiment.Name, experiment)
|
||||
|
||||
body := &hermes.Body{
|
||||
Name: "User",
|
||||
Intros: []string{"Welcome!"},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
|
||||
experiment.Sample(func(idx int) {
|
||||
experiment.MeasureDuration("text-simple", func() {
|
||||
buf, err := mailer.GeneratePlainText()
|
||||
Expect(err).To(BeNil())
|
||||
Expect(buf).ToNot(BeNil())
|
||||
})
|
||||
}, gmeasure.SamplingConfig{N: 100})
|
||||
|
||||
stats := experiment.GetStats("text-simple")
|
||||
AddReportEntry("Mean plain text time", stats.DurationFor(gmeasure.StatMean))
|
||||
})
|
||||
})
|
||||
|
||||
Context("theme comparison", func() {
|
||||
It("should compare generation time between themes", func() {
|
||||
experiment := gmeasure.NewExperiment("Theme Comparison")
|
||||
AddReportEntry(experiment.Name, experiment)
|
||||
|
||||
body := &hermes.Body{
|
||||
Name: "User",
|
||||
Intros: []string{"Test email"},
|
||||
Actions: []hermes.Action{
|
||||
{Button: hermes.Button{Text: "Click", Link: "https://example.com"}},
|
||||
},
|
||||
}
|
||||
|
||||
experiment.Sample(func(idx int) {
|
||||
mailer.SetTheme(render.ThemeDefault)
|
||||
mailer.SetBody(body)
|
||||
experiment.MeasureDuration("theme-default", func() {
|
||||
buf, err := mailer.GenerateHTML()
|
||||
Expect(err).To(BeNil())
|
||||
Expect(buf).ToNot(BeNil())
|
||||
})
|
||||
}, gmeasure.SamplingConfig{N: 100})
|
||||
|
||||
experiment.Sample(func(idx int) {
|
||||
mailer.SetTheme(render.ThemeFlat)
|
||||
mailer.SetBody(body)
|
||||
experiment.MeasureDuration("theme-flat", func() {
|
||||
buf, err := mailer.GenerateHTML()
|
||||
Expect(err).To(BeNil())
|
||||
Expect(buf).ToNot(BeNil())
|
||||
})
|
||||
}, gmeasure.SamplingConfig{N: 100})
|
||||
|
||||
defaultStats := experiment.GetStats("theme-default")
|
||||
flatStats := experiment.GetStats("theme-flat")
|
||||
AddReportEntry("Mean default theme time", defaultStats.DurationFor(gmeasure.StatMean))
|
||||
AddReportEntry("Mean flat theme time", flatStats.DurationFor(gmeasure.StatMean))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("ParseData Benchmarks", func() {
|
||||
Context("template parsing performance", func() {
|
||||
It("should benchmark simple ParseData", func() {
|
||||
experiment := gmeasure.NewExperiment("Simple ParseData")
|
||||
AddReportEntry(experiment.Name, experiment)
|
||||
|
||||
data := map[string]string{
|
||||
"{{user}}": "John",
|
||||
"{{company}}": "Test Inc",
|
||||
}
|
||||
|
||||
experiment.Sample(func(idx int) {
|
||||
mailer := render.New()
|
||||
body := &hermes.Body{
|
||||
Name: "{{user}}",
|
||||
Intros: []string{"Hello {{user}}"},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
mailer.SetName("{{company}}")
|
||||
|
||||
experiment.MeasureDuration("parse-simple", func() {
|
||||
mailer.ParseData(data)
|
||||
})
|
||||
}, gmeasure.SamplingConfig{N: 500})
|
||||
|
||||
stats := experiment.GetStats("parse-simple")
|
||||
AddReportEntry("Mean simple parse time", stats.DurationFor(gmeasure.StatMean))
|
||||
})
|
||||
|
||||
It("should benchmark complex ParseData", func() {
|
||||
experiment := gmeasure.NewExperiment("Complex ParseData")
|
||||
AddReportEntry(experiment.Name, experiment)
|
||||
|
||||
data := map[string]string{
|
||||
"{{user}}": "John",
|
||||
"{{company}}": "Test Inc",
|
||||
"{{code}}": "123456",
|
||||
"{{link}}": "https://example.com",
|
||||
"{{email}}": "user@example.com",
|
||||
}
|
||||
|
||||
experiment.Sample(func(idx int) {
|
||||
mailer := render.New()
|
||||
body := &hermes.Body{
|
||||
Name: "{{user}}",
|
||||
Intros: []string{"Hello {{user}}", "Your code: {{code}}"},
|
||||
Dictionary: []hermes.Entry{
|
||||
{Key: "Company", Value: "{{company}}"},
|
||||
{Key: "Email", Value: "{{email}}"},
|
||||
},
|
||||
Tables: []hermes.Table{{
|
||||
Data: [][]hermes.Entry{
|
||||
{{Key: "Link", Value: "{{link}}"}},
|
||||
},
|
||||
}},
|
||||
Actions: []hermes.Action{
|
||||
{
|
||||
Instructions: "Visit {{link}}",
|
||||
Button: hermes.Button{Text: "Visit", Link: "{{link}}"},
|
||||
},
|
||||
},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
mailer.SetName("{{company}}")
|
||||
|
||||
experiment.MeasureDuration("parse-complex", func() {
|
||||
mailer.ParseData(data)
|
||||
})
|
||||
}, gmeasure.SamplingConfig{N: 500})
|
||||
|
||||
stats := experiment.GetStats("parse-complex")
|
||||
AddReportEntry("Mean complex parse time", stats.DurationFor(gmeasure.StatMean))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Complete Workflow Benchmark", func() {
|
||||
Context("end-to-end performance", func() {
|
||||
It("should benchmark complete email generation workflow", func() {
|
||||
experiment := gmeasure.NewExperiment("Complete Workflow")
|
||||
AddReportEntry(experiment.Name, experiment)
|
||||
|
||||
config := render.Config{
|
||||
Theme: "default",
|
||||
Direction: "ltr",
|
||||
Name: "{{company}}",
|
||||
Link: "https://example.com",
|
||||
Logo: "https://example.com/logo.png",
|
||||
Copyright: "© 2024 {{company}}",
|
||||
TroubleText: "Contact us",
|
||||
Body: hermes.Body{
|
||||
Name: "{{user}}",
|
||||
Intros: []string{"Welcome {{user}}!"},
|
||||
Actions: []hermes.Action{
|
||||
{
|
||||
Instructions: "Verify your account:",
|
||||
Button: hermes.Button{Text: "Verify", Link: "{{link}}"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
data := map[string]string{
|
||||
"{{company}}": "Test Inc",
|
||||
"{{user}}": "John Doe",
|
||||
"{{link}}": "https://example.com/verify",
|
||||
}
|
||||
|
||||
experiment.Sample(func(idx int) {
|
||||
experiment.MeasureDuration("workflow", func() {
|
||||
mailer := config.NewMailer()
|
||||
mailer.ParseData(data)
|
||||
htmlBuf, htmlErr := mailer.GenerateHTML()
|
||||
textBuf, textErr := mailer.GeneratePlainText()
|
||||
|
||||
Expect(htmlErr).To(BeNil())
|
||||
Expect(textErr).To(BeNil())
|
||||
Expect(htmlBuf).ToNot(BeNil())
|
||||
Expect(textBuf).ToNot(BeNil())
|
||||
})
|
||||
}, gmeasure.SamplingConfig{N: 100})
|
||||
|
||||
stats := experiment.GetStats("workflow")
|
||||
AddReportEntry("Mean workflow time", stats.DurationFor(gmeasure.StatMean))
|
||||
AddReportEntry("Max workflow time", stats.DurationFor(gmeasure.StatMax))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Parsing Benchmarks", func() {
|
||||
Context("theme and direction parsing", func() {
|
||||
It("should benchmark ParseTheme", func() {
|
||||
experiment := gmeasure.NewExperiment("ParseTheme")
|
||||
AddReportEntry(experiment.Name, experiment)
|
||||
|
||||
themes := []string{"default", "flat", "Default", "FLAT", "unknown"}
|
||||
|
||||
experiment.Sample(func(idx int) {
|
||||
theme := themes[idx%len(themes)]
|
||||
experiment.MeasureDuration("parse-theme", func() {
|
||||
_ = render.ParseTheme(theme)
|
||||
})
|
||||
}, gmeasure.SamplingConfig{N: 1000})
|
||||
|
||||
stats := experiment.GetStats("parse-theme")
|
||||
AddReportEntry("Mean theme parse time", stats.DurationFor(gmeasure.StatMean))
|
||||
})
|
||||
|
||||
It("should benchmark ParseTextDirection", func() {
|
||||
experiment := gmeasure.NewExperiment("ParseTextDirection")
|
||||
AddReportEntry(experiment.Name, experiment)
|
||||
|
||||
directions := []string{"ltr", "rtl", "left-to-right", "right-to-left", "LTR"}
|
||||
|
||||
experiment.Sample(func(idx int) {
|
||||
dir := directions[idx%len(directions)]
|
||||
experiment.MeasureDuration("parse-direction", func() {
|
||||
_ = render.ParseTextDirection(dir)
|
||||
})
|
||||
}, gmeasure.SamplingConfig{N: 1000})
|
||||
|
||||
stats := experiment.GetStats("parse-direction")
|
||||
AddReportEntry("Mean direction parse time", stats.DurationFor(gmeasure.StatMean))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Validation Benchmark", func() {
|
||||
Context("config validation performance", func() {
|
||||
It("should benchmark Config.Validate()", func() {
|
||||
experiment := gmeasure.NewExperiment("Config Validation")
|
||||
AddReportEntry(experiment.Name, experiment)
|
||||
|
||||
validConfig := render.Config{
|
||||
Theme: "default",
|
||||
Direction: "ltr",
|
||||
Name: "Test",
|
||||
Link: "https://example.com",
|
||||
Logo: "https://example.com/logo.png",
|
||||
Copyright: "© 2024",
|
||||
TroubleText: "Help",
|
||||
Body: hermes.Body{Name: "User"},
|
||||
}
|
||||
|
||||
experiment.Sample(func(idx int) {
|
||||
experiment.MeasureDuration("validate", func() {
|
||||
err := validConfig.Validate()
|
||||
Expect(err).To(BeNil())
|
||||
})
|
||||
}, gmeasure.SamplingConfig{N: 500})
|
||||
|
||||
stats := experiment.GetStats("validate")
|
||||
AddReportEntry("Mean validation time", stats.DurationFor(gmeasure.StatMean))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
441
mail/render/concurrency_test.go
Normal file
441
mail/render/concurrency_test.go
Normal file
@@ -0,0 +1,441 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 Nicolas JUHEL
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
package render_test
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/go-hermes/hermes/v2"
|
||||
"github.com/nabbar/golib/mail/render"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Concurrency", func() {
|
||||
Describe("Concurrent Clone Operations", func() {
|
||||
Context("when cloning from multiple goroutines", func() {
|
||||
It("should safely clone mailer concurrently", func() {
|
||||
mailer := render.New()
|
||||
mailer.SetName("Original")
|
||||
body := &hermes.Body{
|
||||
Name: "User",
|
||||
Intros: []string{"Intro 1", "Intro 2"},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
const numGoroutines = 100
|
||||
|
||||
results := make([]render.Mailer, numGoroutines)
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
wg.Add(1)
|
||||
go func(index int) {
|
||||
defer GinkgoRecover()
|
||||
defer wg.Done()
|
||||
|
||||
clone := mailer.Clone()
|
||||
results[index] = clone
|
||||
|
||||
// Verify clone has correct values
|
||||
Expect(clone.GetName()).To(Equal("Original"))
|
||||
Expect(clone.GetBody().Name).To(Equal("User"))
|
||||
Expect(clone.GetBody().Intros).To(HaveLen(2))
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Verify all clones were created
|
||||
for _, clone := range results {
|
||||
Expect(clone).ToNot(BeNil())
|
||||
}
|
||||
})
|
||||
|
||||
It("should create independent clones under concurrent load", func() {
|
||||
mailer := render.New()
|
||||
body := &hermes.Body{
|
||||
Intros: []string{"Original"},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
const numGoroutines = 50
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
wg.Add(1)
|
||||
go func(index int) {
|
||||
defer GinkgoRecover()
|
||||
defer wg.Done()
|
||||
|
||||
clone := mailer.Clone()
|
||||
|
||||
// Modify the clone
|
||||
cloneBody := clone.GetBody()
|
||||
cloneBody.Intros = append(cloneBody.Intros, "Modified")
|
||||
|
||||
// Clone should have 2 intros
|
||||
Expect(len(cloneBody.Intros)).To(BeNumerically(">=", 2))
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Original should still have 1 intro
|
||||
originalBody := mailer.GetBody()
|
||||
Expect(originalBody.Intros).To(HaveLen(1))
|
||||
Expect(originalBody.Intros[0]).To(Equal("Original"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Concurrent Read Operations", func() {
|
||||
Context("when reading from multiple goroutines", func() {
|
||||
It("should safely read mailer properties concurrently", func() {
|
||||
mailer := render.New()
|
||||
mailer.SetName("Test Company")
|
||||
mailer.SetLink("https://example.com")
|
||||
mailer.SetLogo("https://example.com/logo.png")
|
||||
mailer.SetCopyright("© 2024")
|
||||
mailer.SetTroubleText("Contact us")
|
||||
mailer.SetTheme(render.ThemeFlat)
|
||||
mailer.SetTextDirection(render.RightToLeft)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
const numGoroutines = 100
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer GinkgoRecover()
|
||||
defer wg.Done()
|
||||
|
||||
// Read all properties
|
||||
name := mailer.GetName()
|
||||
link := mailer.GetLink()
|
||||
logo := mailer.GetLogo()
|
||||
copyright := mailer.GetCopyright()
|
||||
troubleText := mailer.GetTroubleText()
|
||||
theme := mailer.GetTheme()
|
||||
direction := mailer.GetTextDirection()
|
||||
body := mailer.GetBody()
|
||||
|
||||
// Verify values
|
||||
Expect(name).To(Equal("Test Company"))
|
||||
Expect(link).To(Equal("https://example.com"))
|
||||
Expect(logo).To(Equal("https://example.com/logo.png"))
|
||||
Expect(copyright).To(Equal("© 2024"))
|
||||
Expect(troubleText).To(Equal("Contact us"))
|
||||
Expect(theme).To(Equal(render.ThemeFlat))
|
||||
Expect(direction).To(Equal(render.RightToLeft))
|
||||
Expect(body).ToNot(BeNil())
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Concurrent Generation", func() {
|
||||
Context("when generating HTML from multiple goroutines", func() {
|
||||
It("should safely generate HTML concurrently using independent clones", func() {
|
||||
mailer := render.New()
|
||||
mailer.SetName("Test Company")
|
||||
mailer.SetLink("https://example.com")
|
||||
mailer.SetLogo("https://example.com/logo.png")
|
||||
body := &hermes.Body{
|
||||
Name: "User",
|
||||
Intros: []string{"Welcome!"},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
const numGoroutines = 50
|
||||
|
||||
results := make(chan bool, numGoroutines)
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer GinkgoRecover()
|
||||
defer wg.Done()
|
||||
|
||||
// Clone to avoid concurrent modifications
|
||||
clone := mailer.Clone()
|
||||
buf, err := clone.GenerateHTML()
|
||||
|
||||
Expect(err).To(BeNil())
|
||||
Expect(buf).ToNot(BeNil())
|
||||
Expect(buf.Len()).To(BeNumerically(">", 0))
|
||||
|
||||
results <- true
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(results)
|
||||
|
||||
count := 0
|
||||
for range results {
|
||||
count++
|
||||
}
|
||||
|
||||
Expect(count).To(Equal(numGoroutines))
|
||||
})
|
||||
|
||||
It("should safely generate plain text concurrently using independent clones", func() {
|
||||
mailer := render.New()
|
||||
mailer.SetName("Test Company")
|
||||
body := &hermes.Body{
|
||||
Name: "User",
|
||||
Intros: []string{"Test"},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
const numGoroutines = 50
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer GinkgoRecover()
|
||||
defer wg.Done()
|
||||
|
||||
clone := mailer.Clone()
|
||||
buf, err := clone.GeneratePlainText()
|
||||
|
||||
Expect(err).To(BeNil())
|
||||
Expect(buf).ToNot(BeNil())
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Concurrent Configuration Updates", func() {
|
||||
Context("when multiple goroutines update different mailers", func() {
|
||||
It("should handle concurrent updates on independent mailers", func() {
|
||||
var wg sync.WaitGroup
|
||||
const numGoroutines = 50
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
wg.Add(1)
|
||||
go func(index int) {
|
||||
defer GinkgoRecover()
|
||||
defer wg.Done()
|
||||
|
||||
mailer := render.New()
|
||||
mailer.SetName("Mailer " + string(rune(index)))
|
||||
mailer.SetTheme(render.ThemeFlat)
|
||||
|
||||
body := &hermes.Body{
|
||||
Name: "User " + string(rune(index)),
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
|
||||
// Generate to ensure no race conditions
|
||||
_, err := mailer.GenerateHTML()
|
||||
Expect(err).To(BeNil())
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Concurrent ParseData", func() {
|
||||
Context("when parsing data in multiple goroutines", func() {
|
||||
It("should safely parse data on independent clones", func() {
|
||||
baseMailer := render.New()
|
||||
body := &hermes.Body{
|
||||
Name: "{{user}}",
|
||||
Intros: []string{"Hello {{user}}"},
|
||||
}
|
||||
baseMailer.SetBody(body)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
const numGoroutines = 50
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
wg.Add(1)
|
||||
go func(index int) {
|
||||
defer GinkgoRecover()
|
||||
defer wg.Done()
|
||||
|
||||
// Clone for independent operation
|
||||
mailer := baseMailer.Clone()
|
||||
|
||||
data := map[string]string{
|
||||
"{{user}}": "User " + string(rune(index)),
|
||||
}
|
||||
|
||||
mailer.ParseData(data)
|
||||
|
||||
body := mailer.GetBody()
|
||||
Expect(body.Name).To(ContainSubstring("User"))
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Original should still have template variables
|
||||
originalBody := baseMailer.GetBody()
|
||||
Expect(originalBody.Name).To(Equal("{{user}}"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Mixed Concurrent Operations", func() {
|
||||
Context("when performing various operations concurrently", func() {
|
||||
It("should handle mixed operations safely", func() {
|
||||
mailer := render.New()
|
||||
mailer.SetName("Base")
|
||||
body := &hermes.Body{
|
||||
Name: "{{user}}",
|
||||
Intros: []string{"Welcome"},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
const numGoroutines = 100
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
wg.Add(1)
|
||||
|
||||
// Distribute different operations
|
||||
switch i % 4 {
|
||||
case 0:
|
||||
// Clone operation
|
||||
go func() {
|
||||
defer GinkgoRecover()
|
||||
defer wg.Done()
|
||||
clone := mailer.Clone()
|
||||
Expect(clone).ToNot(BeNil())
|
||||
}()
|
||||
case 1:
|
||||
// Read operation
|
||||
go func() {
|
||||
defer GinkgoRecover()
|
||||
defer wg.Done()
|
||||
name := mailer.GetName()
|
||||
Expect(name).To(Equal("Base"))
|
||||
}()
|
||||
case 2:
|
||||
// Generate HTML
|
||||
go func() {
|
||||
defer GinkgoRecover()
|
||||
defer wg.Done()
|
||||
clone := mailer.Clone()
|
||||
_, err := clone.GenerateHTML()
|
||||
Expect(err).To(BeNil())
|
||||
}()
|
||||
case 3:
|
||||
// Parse data on clone
|
||||
go func() {
|
||||
defer GinkgoRecover()
|
||||
defer wg.Done()
|
||||
clone := mailer.Clone()
|
||||
clone.ParseData(map[string]string{
|
||||
"{{user}}": "Test",
|
||||
})
|
||||
Expect(clone.GetBody().Name).To(Equal("Test"))
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Original should remain unchanged
|
||||
Expect(mailer.GetName()).To(Equal("Base"))
|
||||
Expect(mailer.GetBody().Name).To(Equal("{{user}}"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("High Load Stress Test", func() {
|
||||
Context("under high concurrent load", func() {
|
||||
It("should remain stable with many concurrent operations", func() {
|
||||
config := render.Config{
|
||||
Theme: "default",
|
||||
Direction: "ltr",
|
||||
Name: "Stress Test",
|
||||
Link: "https://example.com",
|
||||
Logo: "https://example.com/logo.png",
|
||||
Copyright: "© 2024",
|
||||
TroubleText: "Help",
|
||||
Body: hermes.Body{
|
||||
Name: "{{user}}",
|
||||
Intros: []string{"Stress test email"},
|
||||
},
|
||||
}
|
||||
|
||||
baseMailer := config.NewMailer()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
const highLoad = 200
|
||||
|
||||
successCount := make(chan bool, highLoad)
|
||||
|
||||
for i := 0; i < highLoad; i++ {
|
||||
wg.Add(1)
|
||||
go func(index int) {
|
||||
defer GinkgoRecover()
|
||||
defer wg.Done()
|
||||
|
||||
clone := baseMailer.Clone()
|
||||
clone.ParseData(map[string]string{
|
||||
"{{user}}": "User" + string(rune(index)),
|
||||
})
|
||||
|
||||
htmlBuf, htmlErr := clone.GenerateHTML()
|
||||
textBuf, textErr := clone.GeneratePlainText()
|
||||
|
||||
if htmlErr == nil && textErr == nil &&
|
||||
htmlBuf != nil && textBuf != nil {
|
||||
successCount <- true
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(successCount)
|
||||
|
||||
count := 0
|
||||
for range successCount {
|
||||
count++
|
||||
}
|
||||
|
||||
// Should have high success rate
|
||||
Expect(count).To(BeNumerically(">=", highLoad*0.95))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
172
mail/render/config.go
Normal file
172
mail/render/config.go
Normal file
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2021 Nicolas JUHEL
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
package render
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
libhms "github.com/go-hermes/hermes/v2"
|
||||
libval "github.com/go-playground/validator/v10"
|
||||
liberr "github.com/nabbar/golib/errors"
|
||||
)
|
||||
|
||||
// Config represents the configuration structure for creating a Mailer instance.
|
||||
// It supports multiple serialization formats (JSON, YAML, TOML) and includes
|
||||
// validation tags for ensuring configuration correctness.
|
||||
//
|
||||
// All fields except DisableCSSInline are required and must pass validation.
|
||||
// The Link and Logo fields must be valid URLs.
|
||||
//
|
||||
// This struct is designed to be used with configuration files or environment
|
||||
// variables via libraries like Viper or similar configuration management tools.
|
||||
//
|
||||
// Example JSON configuration:
|
||||
//
|
||||
// {
|
||||
// "theme": "flat",
|
||||
// "direction": "ltr",
|
||||
// "name": "My Company",
|
||||
// "link": "https://example.com",
|
||||
// "logo": "https://example.com/logo.png",
|
||||
// "copyright": "© 2024 My Company",
|
||||
// "troubleText": "Need help? Contact support@example.com",
|
||||
// "disableCSSInline": false,
|
||||
// "body": {
|
||||
// "name": "John Doe",
|
||||
// "intros": ["Welcome to our service!"]
|
||||
// }
|
||||
// }
|
||||
type Config struct {
|
||||
// Theme specifies the email template theme ("default" or "flat")
|
||||
Theme string `json:"theme,omitempty" yaml:"theme,omitempty" toml:"theme,omitempty" mapstructure:"theme,omitempty" validate:"required"`
|
||||
// Direction specifies the text direction ("ltr" or "rtl")
|
||||
Direction string `json:"direction,omitempty" yaml:"direction,omitempty" toml:"direction,omitempty" mapstructure:"direction,omitempty" validate:"required"`
|
||||
// Name is the product/company name displayed in the email
|
||||
Name string `json:"name,omitempty" yaml:"name,omitempty" toml:"name,omitempty" mapstructure:"name,omitempty" validate:"required"`
|
||||
// Link is the primary product/company URL (must be a valid URL)
|
||||
Link string `json:"link,omitempty" yaml:"link,omitempty" toml:"link,omitempty" mapstructure:"link,omitempty" validate:"required,url"`
|
||||
// Logo is the URL of the logo image (must be a valid URL)
|
||||
Logo string `json:"logo,omitempty" yaml:"logo,omitempty" toml:"logo,omitempty" mapstructure:"logo,omitempty" validate:"required,url"`
|
||||
// Copyright is the copyright text displayed in the footer
|
||||
Copyright string `json:"copyright,omitempty" yaml:"copyright,omitempty" toml:"copyright,omitempty" mapstructure:"copyright,omitempty" validate:"required"`
|
||||
// TroubleText is the help/support text displayed for troubleshooting
|
||||
TroubleText string `json:"troubleText,omitempty" yaml:"troubleText,omitempty" toml:"troubleText,omitempty" mapstructure:"troubleText,omitempty" validate:"required"`
|
||||
// DisableCSSInline controls whether to disable CSS inlining in HTML output
|
||||
DisableCSSInline bool `json:"disableCSSInline,omitempty" yaml:"disableCSSInline,omitempty" toml:"disableCSSInline,omitempty" mapstructure:"disableCSSInline,omitempty"`
|
||||
// Body contains the email body content structure
|
||||
// See github.com/go-hermes/hermes/v2 Body for field details
|
||||
Body libhms.Body `json:"body" yaml:"body" toml:"body" mapstructure:"body" validate:"required"`
|
||||
}
|
||||
|
||||
// Validate validates the Config struct using validator tags.
|
||||
// It checks all required fields and validates URL formats for Link and Logo.
|
||||
//
|
||||
// Returns:
|
||||
// - nil if validation succeeds
|
||||
// - liberr.Error containing all validation errors if validation fails
|
||||
//
|
||||
// Validation rules:
|
||||
// - All fields except DisableCSSInline are required
|
||||
// - Link must be a valid URL
|
||||
// - Logo must be a valid URL
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// config := render.Config{
|
||||
// Theme: "flat",
|
||||
// Direction: "ltr",
|
||||
// Name: "Company",
|
||||
// Link: "https://example.com",
|
||||
// Logo: "https://example.com/logo.png",
|
||||
// Copyright: "© 2024",
|
||||
// TroubleText: "Help",
|
||||
// Body: hermes.Body{},
|
||||
// }
|
||||
// if err := config.Validate(); err != nil {
|
||||
// // Handle validation errors
|
||||
// }
|
||||
func (c Config) Validate() liberr.Error {
|
||||
err := ErrorMailerConfigInvalid.Error(nil)
|
||||
|
||||
if er := libval.New().Struct(c); er != nil {
|
||||
if e, ok := er.(*libval.InvalidValidationError); ok {
|
||||
err.Add(e)
|
||||
}
|
||||
|
||||
for _, e := range er.(libval.ValidationErrors) {
|
||||
//nolint goerr113
|
||||
err.Add(fmt.Errorf("config field '%s' is not validated by constraint '%s'", e.Namespace(), e.ActualTag()))
|
||||
}
|
||||
}
|
||||
|
||||
if err.HasParent() {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewMailer creates a new Mailer instance from the configuration.
|
||||
// It parses the theme and direction strings and initializes all fields.
|
||||
//
|
||||
// Note: This method does not validate the configuration. Call Validate()
|
||||
// before NewMailer() to ensure the configuration is valid.
|
||||
//
|
||||
// Returns:
|
||||
// - A configured Mailer instance ready to use
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// config := render.Config{
|
||||
// Theme: "flat",
|
||||
// Direction: "ltr",
|
||||
// Name: "My Company",
|
||||
// Link: "https://example.com",
|
||||
// Logo: "https://example.com/logo.png",
|
||||
// Copyright: "© 2024",
|
||||
// TroubleText: "Need help?",
|
||||
// Body: hermes.Body{Name: "User"},
|
||||
// }
|
||||
// if err := config.Validate(); err == nil {
|
||||
// mailer := config.NewMailer()
|
||||
// htmlBuf, _ := mailer.GenerateHTML()
|
||||
// }
|
||||
func (c Config) NewMailer() Mailer {
|
||||
return &email{
|
||||
t: ParseTheme(c.Theme),
|
||||
d: ParseTextDirection(c.Direction),
|
||||
p: libhms.Product{
|
||||
Name: c.Name,
|
||||
Link: c.Link,
|
||||
Logo: c.Logo,
|
||||
Copyright: c.Copyright,
|
||||
TroubleText: c.TroubleText,
|
||||
},
|
||||
b: &c.Body,
|
||||
c: c.DisableCSSInline,
|
||||
}
|
||||
}
|
||||
282
mail/render/config_test.go
Normal file
282
mail/render/config_test.go
Normal file
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 Nicolas JUHEL
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
package render_test
|
||||
|
||||
import (
|
||||
"github.com/go-hermes/hermes/v2"
|
||||
"github.com/nabbar/golib/mail/render"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Config", func() {
|
||||
var validConfig render.Config
|
||||
|
||||
BeforeEach(func() {
|
||||
validConfig = render.Config{
|
||||
Theme: "default",
|
||||
Direction: "ltr",
|
||||
Name: "Test Company",
|
||||
Link: "https://example.com",
|
||||
Logo: "https://example.com/logo.png",
|
||||
Copyright: "© 2024 Test Company",
|
||||
TroubleText: "Having trouble?",
|
||||
DisableCSSInline: false,
|
||||
Body: hermes.Body{
|
||||
Name: "John Doe",
|
||||
Intros: []string{"Welcome to our service!"},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
Describe("Validation", func() {
|
||||
Context("with valid configuration", func() {
|
||||
It("should validate successfully", func() {
|
||||
err := validConfig.Validate()
|
||||
Expect(err).To(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Context("with missing required fields", func() {
|
||||
It("should fail validation when theme is missing", func() {
|
||||
validConfig.Theme = ""
|
||||
err := validConfig.Validate()
|
||||
Expect(err).ToNot(BeNil())
|
||||
Expect(err.HasParent()).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should fail validation when direction is missing", func() {
|
||||
validConfig.Direction = ""
|
||||
err := validConfig.Validate()
|
||||
Expect(err).ToNot(BeNil())
|
||||
Expect(err.HasParent()).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should fail validation when name is missing", func() {
|
||||
validConfig.Name = ""
|
||||
err := validConfig.Validate()
|
||||
Expect(err).ToNot(BeNil())
|
||||
Expect(err.HasParent()).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should fail validation when link is missing", func() {
|
||||
validConfig.Link = ""
|
||||
err := validConfig.Validate()
|
||||
Expect(err).ToNot(BeNil())
|
||||
Expect(err.HasParent()).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should fail validation when logo is missing", func() {
|
||||
validConfig.Logo = ""
|
||||
err := validConfig.Validate()
|
||||
Expect(err).ToNot(BeNil())
|
||||
Expect(err.HasParent()).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should fail validation when copyright is missing", func() {
|
||||
validConfig.Copyright = ""
|
||||
err := validConfig.Validate()
|
||||
Expect(err).ToNot(BeNil())
|
||||
Expect(err.HasParent()).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should fail validation when trouble text is missing", func() {
|
||||
validConfig.TroubleText = ""
|
||||
err := validConfig.Validate()
|
||||
Expect(err).ToNot(BeNil())
|
||||
Expect(err.HasParent()).To(BeTrue())
|
||||
})
|
||||
})
|
||||
|
||||
Context("with invalid URL format", func() {
|
||||
It("should fail validation for invalid link URL", func() {
|
||||
validConfig.Link = "not-a-valid-url"
|
||||
err := validConfig.Validate()
|
||||
Expect(err).ToNot(BeNil())
|
||||
Expect(err.HasParent()).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should fail validation for invalid logo URL", func() {
|
||||
validConfig.Logo = "not-a-valid-url"
|
||||
err := validConfig.Validate()
|
||||
Expect(err).ToNot(BeNil())
|
||||
Expect(err.HasParent()).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should accept valid HTTP URLs", func() {
|
||||
validConfig.Link = "http://example.com"
|
||||
validConfig.Logo = "http://example.com/logo.png"
|
||||
err := validConfig.Validate()
|
||||
Expect(err).To(BeNil())
|
||||
})
|
||||
|
||||
It("should accept valid HTTPS URLs", func() {
|
||||
validConfig.Link = "https://example.com"
|
||||
validConfig.Logo = "https://example.com/logo.png"
|
||||
err := validConfig.Validate()
|
||||
Expect(err).To(BeNil())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("NewMailer", func() {
|
||||
Context("when creating mailer from config", func() {
|
||||
It("should create mailer with correct values", func() {
|
||||
mailer := validConfig.NewMailer()
|
||||
|
||||
Expect(mailer).ToNot(BeNil())
|
||||
Expect(mailer.GetName()).To(Equal(validConfig.Name))
|
||||
Expect(mailer.GetLink()).To(Equal(validConfig.Link))
|
||||
Expect(mailer.GetLogo()).To(Equal(validConfig.Logo))
|
||||
Expect(mailer.GetCopyright()).To(Equal(validConfig.Copyright))
|
||||
Expect(mailer.GetTroubleText()).To(Equal(validConfig.TroubleText))
|
||||
Expect(mailer.GetTheme()).To(Equal(render.ThemeDefault))
|
||||
Expect(mailer.GetTextDirection()).To(Equal(render.LeftToRight))
|
||||
})
|
||||
|
||||
It("should create mailer with flat theme", func() {
|
||||
validConfig.Theme = "flat"
|
||||
mailer := validConfig.NewMailer()
|
||||
|
||||
Expect(mailer.GetTheme()).To(Equal(render.ThemeFlat))
|
||||
})
|
||||
|
||||
It("should create mailer with right to left direction", func() {
|
||||
validConfig.Direction = "rtl"
|
||||
mailer := validConfig.NewMailer()
|
||||
|
||||
Expect(mailer.GetTextDirection()).To(Equal(render.RightToLeft))
|
||||
})
|
||||
|
||||
It("should preserve body content", func() {
|
||||
mailer := validConfig.NewMailer()
|
||||
body := mailer.GetBody()
|
||||
|
||||
Expect(body).ToNot(BeNil())
|
||||
Expect(body.Name).To(Equal("John Doe"))
|
||||
Expect(body.Intros).To(HaveLen(1))
|
||||
Expect(body.Intros[0]).To(Equal("Welcome to our service!"))
|
||||
})
|
||||
|
||||
It("should handle CSS inline disable flag", func() {
|
||||
validConfig.DisableCSSInline = true
|
||||
mailer := validConfig.NewMailer()
|
||||
|
||||
// Should not panic
|
||||
Expect(mailer).ToNot(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Context("with different configurations", func() {
|
||||
It("should handle minimal valid configuration", func() {
|
||||
minConfig := render.Config{
|
||||
Theme: "default",
|
||||
Direction: "ltr",
|
||||
Name: "Company",
|
||||
Link: "https://company.com",
|
||||
Logo: "https://company.com/logo.png",
|
||||
Copyright: "©",
|
||||
TroubleText: "Help",
|
||||
Body: hermes.Body{},
|
||||
}
|
||||
|
||||
err := minConfig.Validate()
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
mailer := minConfig.NewMailer()
|
||||
Expect(mailer).ToNot(BeNil())
|
||||
})
|
||||
|
||||
It("should handle configuration with complex body", func() {
|
||||
validConfig.Body = hermes.Body{
|
||||
Name: "User",
|
||||
Intros: []string{"Line 1", "Line 2"},
|
||||
Outros: []string{"Goodbye"},
|
||||
Actions: []hermes.Action{
|
||||
{
|
||||
Instructions: "Click below",
|
||||
Button: hermes.Button{
|
||||
Text: "Action",
|
||||
Link: "https://example.com/action",
|
||||
},
|
||||
},
|
||||
},
|
||||
Dictionary: []hermes.Entry{
|
||||
{Key: "Key", Value: "Value"},
|
||||
},
|
||||
Tables: []hermes.Table{{
|
||||
Data: [][]hermes.Entry{
|
||||
{{Key: "Col1", Value: "Val1"}},
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
mailer := validConfig.NewMailer()
|
||||
body := mailer.GetBody()
|
||||
|
||||
Expect(body.Name).To(Equal("User"))
|
||||
Expect(body.Intros).To(HaveLen(2))
|
||||
Expect(body.Outros).To(HaveLen(1))
|
||||
Expect(body.Actions).To(HaveLen(1))
|
||||
Expect(body.Dictionary).To(HaveLen(1))
|
||||
Expect(body.Tables[0].Data).To(HaveLen(1))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Configuration Edge Cases", func() {
|
||||
Context("when handling special characters", func() {
|
||||
It("should handle special characters in text fields", func() {
|
||||
validConfig.Name = "Company™ & Co. <>"
|
||||
validConfig.Copyright = "© 2024 • All Rights Reserved"
|
||||
validConfig.TroubleText = "Questions? →"
|
||||
|
||||
err := validConfig.Validate()
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
mailer := validConfig.NewMailer()
|
||||
Expect(mailer.GetName()).To(ContainSubstring("™"))
|
||||
Expect(mailer.GetCopyright()).To(ContainSubstring("•"))
|
||||
Expect(mailer.GetTroubleText()).To(ContainSubstring("→"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when handling long strings", func() {
|
||||
It("should handle very long text values", func() {
|
||||
longString := string(make([]byte, 1000))
|
||||
for i := range longString {
|
||||
longString = longString[:i] + "a" + longString[i+1:]
|
||||
}
|
||||
|
||||
validConfig.Copyright = longString
|
||||
mailer := validConfig.NewMailer()
|
||||
|
||||
Expect(len(mailer.GetCopyright())).To(Equal(1000))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
127
mail/render/direction.go
Normal file
127
mail/render/direction.go
Normal file
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020 Nicolas JUHEL
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
package render
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-hermes/hermes/v2"
|
||||
)
|
||||
|
||||
// TextDirection represents the text reading direction for email content.
|
||||
// This affects how the email content is laid out and displayed in email clients.
|
||||
//
|
||||
// Common use cases:
|
||||
// - LeftToRight: For languages like English, French, Spanish, etc.
|
||||
// - RightToLeft: For languages like Arabic, Hebrew, Persian, etc.
|
||||
type TextDirection uint8
|
||||
|
||||
const (
|
||||
// LeftToRight indicates left-to-right text direction.
|
||||
// Used for most Western languages (English, French, Spanish, German, etc.).
|
||||
LeftToRight TextDirection = iota
|
||||
|
||||
// RightToLeft indicates right-to-left text direction.
|
||||
// Used for RTL languages (Arabic, Hebrew, Persian, Urdu, etc.).
|
||||
RightToLeft
|
||||
)
|
||||
|
||||
// getDirection converts the TextDirection enum to the corresponding hermes.TextDirection.
|
||||
// This is an internal method used by the email generation process.
|
||||
func (d TextDirection) getDirection() hermes.TextDirection {
|
||||
switch d {
|
||||
case LeftToRight:
|
||||
return hermes.TDLeftToRight
|
||||
case RightToLeft:
|
||||
return hermes.TDRightToLeft
|
||||
}
|
||||
|
||||
return LeftToRight.getDirection()
|
||||
}
|
||||
|
||||
// String returns the string representation of the text direction.
|
||||
//
|
||||
// Returns:
|
||||
// - "Left->Right" for LeftToRight
|
||||
// - "Right->Left" for RightToLeft
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// dir := render.RightToLeft
|
||||
// fmt.Println(dir.String()) // Output: "Right->Left"
|
||||
func (d TextDirection) String() string {
|
||||
switch d {
|
||||
case LeftToRight:
|
||||
return "Left->Right"
|
||||
case RightToLeft:
|
||||
return "Right->Left"
|
||||
}
|
||||
|
||||
return LeftToRight.String()
|
||||
}
|
||||
|
||||
// ParseTextDirection parses a text direction string and returns the corresponding TextDirection enum.
|
||||
// The parsing is case-insensitive and supports multiple formats.
|
||||
//
|
||||
// Supported formats:
|
||||
// - "ltr", "LTR" -> LeftToRight
|
||||
// - "rtl", "RTL" -> RightToLeft
|
||||
// - "left", "left-to-right", "Left->Right" -> LeftToRight
|
||||
// - "right", "right-to-left", "Right->Left" -> RightToLeft
|
||||
//
|
||||
// If the direction string is not recognized or empty, LeftToRight is returned as the default.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// dir := render.ParseTextDirection("rtl")
|
||||
// dir = render.ParseTextDirection("right-to-left")
|
||||
// dir = render.ParseTextDirection("RTL")
|
||||
// dir = render.ParseTextDirection("unknown") // Returns LeftToRight
|
||||
func ParseTextDirection(direction string) TextDirection {
|
||||
d := strings.ToLower(direction)
|
||||
|
||||
// Check for common abbreviations first
|
||||
if strings.Contains(d, "rtl") {
|
||||
return RightToLeft
|
||||
}
|
||||
if strings.Contains(d, "ltr") {
|
||||
return LeftToRight
|
||||
}
|
||||
|
||||
l := strings.Index(d, "left")
|
||||
r := strings.Index(d, "right")
|
||||
|
||||
// If both "left" and "right" are found, check which comes first
|
||||
// "right->left" or "right-to-left" means RightToLeft
|
||||
if l >= 0 && r >= 0 && r < l {
|
||||
return RightToLeft
|
||||
} else if r >= 0 && l < 0 {
|
||||
// Only "right" found without "left" - assume RightToLeft
|
||||
return RightToLeft
|
||||
}
|
||||
|
||||
return LeftToRight
|
||||
}
|
||||
299
mail/render/doc.go
Normal file
299
mail/render/doc.go
Normal file
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020 Nicolas JUHEL
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
// Package render provides email template rendering functionality using the Hermes library.
|
||||
// It offers a high-level API for generating both HTML and plain text email content with
|
||||
// support for themes, text direction, and template variable replacement.
|
||||
//
|
||||
// # Overview
|
||||
//
|
||||
// The render package wraps the github.com/go-hermes/hermes/v2 library to provide an
|
||||
// easy-to-use interface for creating professional-looking transactional emails.
|
||||
// It supports multiple themes, bidirectional text, and rich content including tables,
|
||||
// actions, and custom formatting.
|
||||
//
|
||||
// # Key Features
|
||||
//
|
||||
// - Multiple visual themes (Default, Flat)
|
||||
// - Bidirectional text support (LTR/RTL)
|
||||
// - Template variable replacement with {{variable}} syntax
|
||||
// - Rich content support (tables, actions, buttons, dictionary entries)
|
||||
// - HTML and plain text generation
|
||||
// - Configuration via struct with validation
|
||||
// - Deep cloning for concurrent operations
|
||||
// - Integration with github.com/nabbar/golib/errors for structured error handling
|
||||
//
|
||||
// # Basic Usage
|
||||
//
|
||||
// Creating a simple email:
|
||||
//
|
||||
// mailer := render.New()
|
||||
// mailer.SetName("My Company")
|
||||
// mailer.SetLink("https://example.com")
|
||||
// mailer.SetLogo("https://example.com/logo.png")
|
||||
// mailer.SetTheme(render.ThemeFlat)
|
||||
//
|
||||
// body := &hermes.Body{
|
||||
// Name: "John Doe",
|
||||
// Intros: []string{"Welcome to our service!"},
|
||||
// Outros: []string{"Thank you for signing up."},
|
||||
// }
|
||||
// mailer.SetBody(body)
|
||||
//
|
||||
// htmlBuf, err := mailer.GenerateHTML()
|
||||
// textBuf, err := mailer.GeneratePlainText()
|
||||
//
|
||||
// # Configuration-Based Usage
|
||||
//
|
||||
// Using a configuration struct (suitable for JSON/YAML/TOML):
|
||||
//
|
||||
// config := render.Config{
|
||||
// Theme: "flat",
|
||||
// Direction: "ltr",
|
||||
// Name: "My Company",
|
||||
// Link: "https://example.com",
|
||||
// Logo: "https://example.com/logo.png",
|
||||
// Copyright: "© 2024 My Company",
|
||||
// TroubleText: "Need help? Contact support@example.com",
|
||||
// Body: hermes.Body{
|
||||
// Name: "John Doe",
|
||||
// Intros: []string{"Welcome!"},
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// if err := config.Validate(); err != nil {
|
||||
// // Handle validation error
|
||||
// }
|
||||
//
|
||||
// mailer := config.NewMailer()
|
||||
// htmlBuf, err := mailer.GenerateHTML()
|
||||
//
|
||||
// # Template Variable Replacement
|
||||
//
|
||||
// The package supports template variable replacement with the {{variable}} syntax:
|
||||
//
|
||||
// mailer := render.New()
|
||||
// mailer.SetName("{{company}}")
|
||||
//
|
||||
// body := &hermes.Body{
|
||||
// Name: "{{user}}",
|
||||
// Intros: []string{"Your verification code is {{code}}"},
|
||||
// Actions: []hermes.Action{
|
||||
// {
|
||||
// Instructions: "Click below to verify:",
|
||||
// Button: hermes.Button{
|
||||
// Text: "Verify Email",
|
||||
// Link: "{{verification_link}}",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
// mailer.SetBody(body)
|
||||
//
|
||||
// mailer.ParseData(map[string]string{
|
||||
// "{{company}}": "Acme Inc",
|
||||
// "{{user}}": "John Doe",
|
||||
// "{{code}}": "123456",
|
||||
// "{{verification_link}}": "https://example.com/verify?token=abc123",
|
||||
// })
|
||||
//
|
||||
// htmlBuf, err := mailer.GenerateHTML()
|
||||
//
|
||||
// # Advanced Content
|
||||
//
|
||||
// Creating emails with tables and actions:
|
||||
//
|
||||
// body := &hermes.Body{
|
||||
// Name: "John Doe",
|
||||
// Intros: []string{"Here is your monthly report:"},
|
||||
// Dictionary: []hermes.Entry{
|
||||
// {Key: "Transaction ID", Value: "TXN-123456"},
|
||||
// {Key: "Date", Value: "2024-01-15"},
|
||||
// },
|
||||
// Tables: []hermes.Table{{
|
||||
// Data: [][]hermes.Entry{
|
||||
// {
|
||||
// {Key: "Item", Value: "Product A"},
|
||||
// {Key: "Quantity", Value: "2"},
|
||||
// {Key: "Price", Value: "$50.00"},
|
||||
// },
|
||||
// {
|
||||
// {Key: "Item", Value: "Product B"},
|
||||
// {Key: "Quantity", Value: "1"},
|
||||
// {Key: "Price", Value: "$30.00"},
|
||||
// },
|
||||
// },
|
||||
// Columns: hermes.Columns{
|
||||
// CustomWidth: map[string]string{
|
||||
// "Item": "50%",
|
||||
// "Quantity": "20%",
|
||||
// "Price": "30%",
|
||||
// },
|
||||
// },
|
||||
// }},
|
||||
// Actions: []hermes.Action{
|
||||
// {
|
||||
// Instructions: "View full details:",
|
||||
// Button: hermes.Button{
|
||||
// Text: "View Invoice",
|
||||
// Link: "https://example.com/invoice/123456",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// Outros: []string{"Thank you for your business!"},
|
||||
// }
|
||||
//
|
||||
// mailer.SetBody(body)
|
||||
// htmlBuf, err := mailer.GenerateHTML()
|
||||
//
|
||||
// # Themes
|
||||
//
|
||||
// The package supports multiple themes:
|
||||
//
|
||||
// - ThemeDefault: Classic, centered email design
|
||||
// - ThemeFlat: Modern, minimalist email design
|
||||
//
|
||||
// Theme selection:
|
||||
//
|
||||
// mailer.SetTheme(render.ThemeFlat)
|
||||
// // or
|
||||
// theme := render.ParseTheme("flat")
|
||||
// mailer.SetTheme(theme)
|
||||
//
|
||||
// # Text Direction
|
||||
//
|
||||
// Support for both LTR and RTL languages:
|
||||
//
|
||||
// // For LTR languages (English, French, Spanish, etc.)
|
||||
// mailer.SetTextDirection(render.LeftToRight)
|
||||
//
|
||||
// // For RTL languages (Arabic, Hebrew, Persian, etc.)
|
||||
// mailer.SetTextDirection(render.RightToLeft)
|
||||
//
|
||||
// // Or parse from string
|
||||
// direction := render.ParseTextDirection("rtl")
|
||||
// mailer.SetTextDirection(direction)
|
||||
//
|
||||
// # Thread Safety
|
||||
//
|
||||
// Mailer instances are not thread-safe. For concurrent operations, use Clone():
|
||||
//
|
||||
// baseMailer := render.New()
|
||||
// baseMailer.SetName("My Company")
|
||||
// baseMailer.SetTheme(render.ThemeFlat)
|
||||
//
|
||||
// // Create independent copies for concurrent use
|
||||
// var wg sync.WaitGroup
|
||||
// for i := 0; i < 10; i++ {
|
||||
// wg.Add(1)
|
||||
// go func(index int) {
|
||||
// defer wg.Done()
|
||||
// mailer := baseMailer.Clone()
|
||||
// body := &hermes.Body{
|
||||
// Name: fmt.Sprintf("User %d", index),
|
||||
// }
|
||||
// mailer.SetBody(body)
|
||||
// htmlBuf, _ := mailer.GenerateHTML()
|
||||
// // Send email...
|
||||
// }(i)
|
||||
// }
|
||||
// wg.Wait()
|
||||
//
|
||||
// # Error Handling
|
||||
//
|
||||
// The package uses github.com/nabbar/golib/errors for structured error handling:
|
||||
//
|
||||
// htmlBuf, err := mailer.GenerateHTML()
|
||||
// if err != nil {
|
||||
// if err.Code() == render.ErrorMailerHtml {
|
||||
// // Handle HTML generation error
|
||||
// }
|
||||
// log.Printf("Error: %v", err)
|
||||
// }
|
||||
//
|
||||
// Error codes:
|
||||
// - ErrorParamEmpty: Required parameters are missing
|
||||
// - ErrorMailerConfigInvalid: Configuration validation failed
|
||||
// - ErrorMailerHtml: HTML generation failed
|
||||
// - ErrorMailerText: Plain text generation failed
|
||||
//
|
||||
// # Integration with Email Sending
|
||||
//
|
||||
// The generated content can be used with various email sending packages:
|
||||
//
|
||||
// mailer := render.New()
|
||||
// // ... configure mailer ...
|
||||
//
|
||||
// htmlBuf, err := mailer.GenerateHTML()
|
||||
// textBuf, err := mailer.GeneratePlainText()
|
||||
//
|
||||
// // Using standard net/smtp
|
||||
// msg := "From: sender@example.com\r\n" +
|
||||
// "To: recipient@example.com\r\n" +
|
||||
// "Subject: Welcome\r\n" +
|
||||
// "MIME-Version: 1.0\r\n" +
|
||||
// "Content-Type: text/html; charset=UTF-8\r\n\r\n" +
|
||||
// htmlBuf.String()
|
||||
//
|
||||
// // Or use github.com/nabbar/golib/mail/smtp for full SMTP support
|
||||
//
|
||||
// # Dependencies
|
||||
//
|
||||
// This package depends on:
|
||||
// - github.com/go-hermes/hermes/v2: Email template rendering engine
|
||||
// - github.com/nabbar/golib/errors: Structured error handling
|
||||
// - github.com/go-playground/validator/v10: Configuration validation
|
||||
//
|
||||
// # Performance Considerations
|
||||
//
|
||||
// - Email generation typically takes 1-5ms depending on content complexity
|
||||
// - Clone operations are fast (~1µs for simple, ~10µs for complex content)
|
||||
// - Template variable replacement is performed in-place with O(n*m) complexity
|
||||
// - CSS inlining (default) adds processing time but improves email client compatibility
|
||||
//
|
||||
// # Best Practices
|
||||
//
|
||||
// - Always validate configuration before creating a mailer from Config
|
||||
// - Use Clone() for concurrent operations to avoid race conditions
|
||||
// - Generate both HTML and plain text versions for better compatibility
|
||||
// - Test emails in multiple email clients (Gmail, Outlook, Apple Mail, etc.)
|
||||
// - Keep logo images under 200KB for faster loading
|
||||
// - Use HTTPS URLs for all links and images
|
||||
// - Include clear call-to-action buttons with descriptive text
|
||||
// - Provide a plain text alternative for accessibility
|
||||
//
|
||||
// # Related Packages
|
||||
//
|
||||
// For complete email workflow:
|
||||
// - github.com/nabbar/golib/mail/smtp: SMTP client for sending emails
|
||||
// - github.com/nabbar/golib/mail/sender: High-level email sending with attachments
|
||||
// - github.com/nabbar/golib/errors: Error handling and logging
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// For more information on email body structure and available fields:
|
||||
// - Hermes documentation: https://github.com/go-hermes/hermes
|
||||
// - Email best practices: https://www.campaignmonitor.com/best-practices/
|
||||
package render
|
||||
113
mail/render/email.go
Normal file
113
mail/render/email.go
Normal file
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020 Nicolas JUHEL
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
package render
|
||||
|
||||
import "github.com/go-hermes/hermes/v2"
|
||||
|
||||
// email is the internal implementation of the Mailer interface.
|
||||
// It encapsulates all configuration and content needed to generate emails.
|
||||
//
|
||||
// Fields:
|
||||
// - p: Product information (name, logo, link, copyright, trouble text)
|
||||
// - b: Email body content (intros, outros, tables, actions, etc.)
|
||||
// - t: Visual theme for the email template
|
||||
// - d: Text direction (LTR or RTL)
|
||||
// - c: CSS inlining control (true = disable inlining)
|
||||
type email struct {
|
||||
p hermes.Product // Product/company information
|
||||
b *hermes.Body // Email body content and structure
|
||||
t Themes // Visual theme selection
|
||||
d TextDirection // Text direction (LTR/RTL)
|
||||
c bool // CSS inlining disabled flag
|
||||
}
|
||||
|
||||
func (e *email) SetTheme(t Themes) {
|
||||
e.t = t
|
||||
}
|
||||
|
||||
func (e *email) GetTheme() Themes {
|
||||
return e.t
|
||||
}
|
||||
|
||||
func (e *email) SetTextDirection(d TextDirection) {
|
||||
e.d = d
|
||||
}
|
||||
|
||||
func (e *email) GetTextDirection() TextDirection {
|
||||
return e.d
|
||||
}
|
||||
|
||||
func (e *email) SetBody(b *hermes.Body) {
|
||||
e.b = b
|
||||
}
|
||||
|
||||
func (e *email) GetBody() *hermes.Body {
|
||||
return e.b
|
||||
}
|
||||
|
||||
func (e *email) SetCSSInline(disable bool) {
|
||||
e.c = disable
|
||||
}
|
||||
|
||||
func (e *email) SetName(name string) {
|
||||
e.p.Name = name
|
||||
}
|
||||
|
||||
func (e *email) GetName() string {
|
||||
return e.p.Name
|
||||
}
|
||||
|
||||
func (e *email) SetCopyright(copy string) {
|
||||
e.p.Copyright = copy
|
||||
}
|
||||
|
||||
func (e *email) GetCopyright() string {
|
||||
return e.p.Copyright
|
||||
}
|
||||
|
||||
func (e *email) SetLink(link string) {
|
||||
e.p.Link = link
|
||||
}
|
||||
|
||||
func (e *email) GetLink() string {
|
||||
return e.p.Link
|
||||
}
|
||||
|
||||
func (e *email) SetLogo(logoUrl string) {
|
||||
e.p.Logo = logoUrl
|
||||
}
|
||||
|
||||
func (e *email) GetLogo() string {
|
||||
return e.p.Logo
|
||||
}
|
||||
|
||||
func (e *email) SetTroubleText(text string) {
|
||||
e.p.TroubleText = text
|
||||
}
|
||||
|
||||
func (e *email) GetTroubleText() string {
|
||||
return e.p.TroubleText
|
||||
}
|
||||
90
mail/render/error.go
Normal file
90
mail/render/error.go
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020 Nicolas JUHEL
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
package render
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
liberr "github.com/nabbar/golib/errors"
|
||||
)
|
||||
|
||||
// Error codes for the render package.
|
||||
// These codes are used with the github.com/nabbar/golib/errors package
|
||||
// to provide structured error handling.
|
||||
//
|
||||
// All error codes start from liberr.MinPkgMailer to avoid conflicts
|
||||
// with other packages in the golib ecosystem.
|
||||
const (
|
||||
// ErrorParamEmpty indicates that required parameters are missing or empty.
|
||||
// This error is returned when mandatory configuration or data is not provided.
|
||||
ErrorParamEmpty liberr.CodeError = iota + liberr.MinPkgMailer
|
||||
|
||||
// ErrorMailerConfigInvalid indicates that the mailer configuration is invalid.
|
||||
// This error is returned by Config.Validate() when validation fails.
|
||||
ErrorMailerConfigInvalid
|
||||
|
||||
// ErrorMailerHtml indicates a failure during HTML email generation.
|
||||
// This typically occurs when the Hermes library encounters an error
|
||||
// while processing the email template.
|
||||
ErrorMailerHtml
|
||||
|
||||
// ErrorMailerText indicates a failure during plain text email generation.
|
||||
// This typically occurs when the Hermes library encounters an error
|
||||
// while converting the email to plain text format.
|
||||
ErrorMailerText
|
||||
)
|
||||
|
||||
func init() {
|
||||
if liberr.ExistInMapMessage(ErrorParamEmpty) {
|
||||
panic(fmt.Errorf("error code collision with package golib/mailer"))
|
||||
}
|
||||
liberr.RegisterIdFctMessage(ErrorParamEmpty, getMessage)
|
||||
}
|
||||
|
||||
// getMessage returns the human-readable error message for a given error code.
|
||||
// This function is registered with the error system during package initialization
|
||||
// and is called automatically when formatting errors.
|
||||
//
|
||||
// Parameters:
|
||||
// - code: The error code to get the message for
|
||||
//
|
||||
// Returns:
|
||||
// - The human-readable error message, or liberr.NullMessage if the code is unknown
|
||||
func getMessage(code liberr.CodeError) (message string) {
|
||||
switch code {
|
||||
case ErrorParamEmpty:
|
||||
return "given parameters is empty"
|
||||
case ErrorMailerConfigInvalid:
|
||||
return "config of mailer is invalid"
|
||||
case ErrorMailerHtml:
|
||||
return "cannot generate html content"
|
||||
case ErrorMailerText:
|
||||
return "cannot generate plain text content"
|
||||
}
|
||||
|
||||
return liberr.NullMessage
|
||||
}
|
||||
260
mail/render/interface.go
Normal file
260
mail/render/interface.go
Normal file
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020 Nicolas JUHEL
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/go-hermes/hermes/v2"
|
||||
liberr "github.com/nabbar/golib/errors"
|
||||
)
|
||||
|
||||
// Mailer defines the interface for generating and managing email templates.
|
||||
// It provides methods to configure email properties, manage template data,
|
||||
// and generate both HTML and plain text versions of emails.
|
||||
//
|
||||
// The Mailer uses the github.com/go-hermes/hermes/v2 library for template
|
||||
// rendering and supports features like:
|
||||
// - Theme selection (Default, Flat)
|
||||
// - Text direction (LTR, RTL)
|
||||
// - Template variable replacement
|
||||
// - Product information (name, logo, link, copyright)
|
||||
// - Rich body content (intros, outros, tables, actions)
|
||||
//
|
||||
// Thread Safety:
|
||||
// Mailer instances are not thread-safe. Use Clone() to create independent
|
||||
// copies for concurrent operations.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// mailer := render.New()
|
||||
// mailer.SetName("My Company")
|
||||
// mailer.SetTheme(render.ThemeFlat)
|
||||
// body := &hermes.Body{
|
||||
// Name: "John Doe",
|
||||
// Intros: []string{"Welcome to our service!"},
|
||||
// }
|
||||
// mailer.SetBody(body)
|
||||
// htmlBuf, err := mailer.GenerateHTML()
|
||||
type Mailer interface {
|
||||
// Clone creates a deep copy of the Mailer instance.
|
||||
// All slices and nested structures are copied independently to avoid
|
||||
// shared references, making the clone safe for concurrent use.
|
||||
Clone() Mailer
|
||||
|
||||
// SetTheme sets the visual theme for the email template.
|
||||
// Available themes: ThemeDefault, ThemeFlat
|
||||
SetTheme(t Themes)
|
||||
// GetTheme returns the currently configured theme.
|
||||
GetTheme() Themes
|
||||
|
||||
// SetTextDirection sets the text direction for the email content.
|
||||
// Use LeftToRight for LTR languages, RightToLeft for RTL languages.
|
||||
SetTextDirection(d TextDirection)
|
||||
// GetTextDirection returns the currently configured text direction.
|
||||
GetTextDirection() TextDirection
|
||||
|
||||
// SetBody sets the email body content.
|
||||
// See github.com/go-hermes/hermes/v2 Body for structure details.
|
||||
SetBody(b *hermes.Body)
|
||||
// GetBody returns the current email body content.
|
||||
GetBody() *hermes.Body
|
||||
|
||||
// SetCSSInline controls CSS inlining in HTML output.
|
||||
// Set to true to disable CSS inlining (useful for some email clients).
|
||||
SetCSSInline(disable bool)
|
||||
|
||||
// SetName sets the product/company name displayed in the email.
|
||||
SetName(name string)
|
||||
// GetName returns the configured product/company name.
|
||||
GetName() string
|
||||
|
||||
// SetCopyright sets the copyright text displayed in the email footer.
|
||||
SetCopyright(copy string)
|
||||
// GetCopyright returns the configured copyright text.
|
||||
GetCopyright() string
|
||||
|
||||
// SetLink sets the primary product/company link (usually a URL).
|
||||
SetLink(link string)
|
||||
// GetLink returns the configured product/company link.
|
||||
GetLink() string
|
||||
|
||||
// SetLogo sets the URL of the logo image displayed in the email header.
|
||||
SetLogo(logoUrl string)
|
||||
// GetLogo returns the configured logo URL.
|
||||
GetLogo() string
|
||||
|
||||
// SetTroubleText sets the help/support text displayed when actions fail.
|
||||
// Example: "Having trouble? Contact support@example.com"
|
||||
SetTroubleText(text string)
|
||||
// GetTroubleText returns the configured trouble text.
|
||||
GetTroubleText() string
|
||||
|
||||
// ParseData replaces template variables in all email content.
|
||||
// Variables in the format "{{key}}" are replaced with corresponding values.
|
||||
// This affects all text fields including product info, body content, tables, and actions.
|
||||
//
|
||||
// Example:
|
||||
// data := map[string]string{
|
||||
// "{{user}}": "John Doe",
|
||||
// "{{code}}": "123456",
|
||||
// }
|
||||
// mailer.ParseData(data)
|
||||
ParseData(data map[string]string)
|
||||
|
||||
// GenerateHTML generates the HTML version of the email.
|
||||
// Returns a buffer containing the complete HTML document.
|
||||
GenerateHTML() (*bytes.Buffer, liberr.Error)
|
||||
// GeneratePlainText generates the plain text version of the email.
|
||||
// Returns a buffer containing formatted plain text content.
|
||||
GeneratePlainText() (*bytes.Buffer, liberr.Error)
|
||||
}
|
||||
|
||||
// New creates a new Mailer instance with default configuration.
|
||||
// The default configuration includes:
|
||||
// - Theme: ThemeDefault
|
||||
// - Text Direction: LeftToRight
|
||||
// - CSS Inlining: Enabled
|
||||
// - Empty product information and body content
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// mailer := render.New()
|
||||
// mailer.SetName("My Company")
|
||||
// mailer.SetLink("https://example.com")
|
||||
func New() Mailer {
|
||||
return &email{
|
||||
t: ThemeDefault,
|
||||
d: LeftToRight,
|
||||
p: hermes.Product{
|
||||
Name: "",
|
||||
Link: "",
|
||||
Logo: "",
|
||||
Copyright: "",
|
||||
TroubleText: "",
|
||||
},
|
||||
b: &hermes.Body{},
|
||||
c: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Clone creates a deep copy of the email instance.
|
||||
// This method performs a complete deep copy of all fields, including:
|
||||
// - All string slices (Intros, Outros)
|
||||
// - Dictionary entries
|
||||
// - Table data and column configurations
|
||||
// - Actions with buttons
|
||||
//
|
||||
// The cloned instance is completely independent and safe for concurrent use.
|
||||
// Modifications to the clone will not affect the original instance.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// original := render.New()
|
||||
// original.SetName("Company")
|
||||
// clone := original.Clone()
|
||||
// clone.SetName("Other Company") // Does not affect original
|
||||
func (e *email) Clone() Mailer {
|
||||
// Deep copy of slices to avoid shared references
|
||||
intros := make([]string, len(e.b.Intros))
|
||||
copy(intros, e.b.Intros)
|
||||
|
||||
outros := make([]string, len(e.b.Outros))
|
||||
copy(outros, e.b.Outros)
|
||||
|
||||
dictionary := make([]hermes.Entry, len(e.b.Dictionary))
|
||||
copy(dictionary, e.b.Dictionary)
|
||||
|
||||
actions := make([]hermes.Action, len(e.b.Actions))
|
||||
copy(actions, e.b.Actions)
|
||||
|
||||
// Deep copy tables
|
||||
var tables []hermes.Table
|
||||
if e.b.Tables != nil {
|
||||
tables = make([]hermes.Table, len(e.b.Tables))
|
||||
for i, table := range e.b.Tables {
|
||||
// Deep copy table data
|
||||
var tableData [][]hermes.Entry
|
||||
if table.Data != nil {
|
||||
tableData = make([][]hermes.Entry, len(table.Data))
|
||||
for j, row := range table.Data {
|
||||
tableData[j] = make([]hermes.Entry, len(row))
|
||||
copy(tableData[j], row)
|
||||
}
|
||||
}
|
||||
|
||||
// Deep copy table columns
|
||||
var tableColumns hermes.Columns
|
||||
if table.Columns.CustomWidth != nil {
|
||||
tableColumns.CustomWidth = make(map[string]string, len(table.Columns.CustomWidth))
|
||||
for k, v := range table.Columns.CustomWidth {
|
||||
tableColumns.CustomWidth[k] = v
|
||||
}
|
||||
}
|
||||
if table.Columns.CustomAlignment != nil {
|
||||
tableColumns.CustomAlignment = make(map[string]string, len(table.Columns.CustomAlignment))
|
||||
for k, v := range table.Columns.CustomAlignment {
|
||||
tableColumns.CustomAlignment[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
tables[i] = hermes.Table{
|
||||
Title: table.Title,
|
||||
Data: tableData,
|
||||
Columns: tableColumns,
|
||||
Class: table.Class,
|
||||
TitleUnsafe: table.TitleUnsafe,
|
||||
Footer: table.Footer,
|
||||
FooterUnsafe: table.FooterUnsafe,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &email{
|
||||
p: hermes.Product{
|
||||
Name: e.p.Name,
|
||||
Link: e.p.Link,
|
||||
Logo: e.p.Logo,
|
||||
Copyright: e.p.Copyright,
|
||||
TroubleText: e.p.TroubleText,
|
||||
},
|
||||
b: &hermes.Body{
|
||||
Name: e.b.Name,
|
||||
Intros: intros,
|
||||
Dictionary: dictionary,
|
||||
Tables: tables,
|
||||
Actions: actions,
|
||||
Outros: outros,
|
||||
Greeting: e.b.Greeting,
|
||||
Signature: e.b.Signature,
|
||||
Title: e.b.Title,
|
||||
FreeMarkdown: e.b.FreeMarkdown,
|
||||
},
|
||||
t: e.t,
|
||||
d: e.d,
|
||||
c: e.c,
|
||||
}
|
||||
}
|
||||
274
mail/render/interface_test.go
Normal file
274
mail/render/interface_test.go
Normal file
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 Nicolas JUHEL
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
package render_test
|
||||
|
||||
import (
|
||||
"github.com/go-hermes/hermes/v2"
|
||||
"github.com/nabbar/golib/mail/render"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Mailer Interface", func() {
|
||||
var mailer render.Mailer
|
||||
|
||||
BeforeEach(func() {
|
||||
mailer = render.New()
|
||||
})
|
||||
|
||||
Describe("Creation", func() {
|
||||
Context("when creating a new mailer", func() {
|
||||
It("should create with default values", func() {
|
||||
Expect(mailer).ToNot(BeNil())
|
||||
Expect(mailer.GetTheme()).To(Equal(render.ThemeDefault))
|
||||
Expect(mailer.GetTextDirection()).To(Equal(render.LeftToRight))
|
||||
Expect(mailer.GetName()).To(BeEmpty())
|
||||
Expect(mailer.GetLink()).To(BeEmpty())
|
||||
Expect(mailer.GetLogo()).To(BeEmpty())
|
||||
Expect(mailer.GetCopyright()).To(BeEmpty())
|
||||
Expect(mailer.GetTroubleText()).To(BeEmpty())
|
||||
Expect(mailer.GetBody()).ToNot(BeNil())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Theme Management", func() {
|
||||
Context("when setting and getting theme", func() {
|
||||
It("should set and get default theme", func() {
|
||||
mailer.SetTheme(render.ThemeDefault)
|
||||
Expect(mailer.GetTheme()).To(Equal(render.ThemeDefault))
|
||||
})
|
||||
|
||||
It("should set and get flat theme", func() {
|
||||
mailer.SetTheme(render.ThemeFlat)
|
||||
Expect(mailer.GetTheme()).To(Equal(render.ThemeFlat))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Text Direction Management", func() {
|
||||
Context("when setting and getting text direction", func() {
|
||||
It("should set and get left to right", func() {
|
||||
mailer.SetTextDirection(render.LeftToRight)
|
||||
Expect(mailer.GetTextDirection()).To(Equal(render.LeftToRight))
|
||||
})
|
||||
|
||||
It("should set and get right to left", func() {
|
||||
mailer.SetTextDirection(render.RightToLeft)
|
||||
Expect(mailer.GetTextDirection()).To(Equal(render.RightToLeft))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Product Information", func() {
|
||||
Context("when setting product details", func() {
|
||||
It("should set and get name", func() {
|
||||
mailer.SetName("Test Company")
|
||||
Expect(mailer.GetName()).To(Equal("Test Company"))
|
||||
})
|
||||
|
||||
It("should set and get link", func() {
|
||||
mailer.SetLink("https://example.com")
|
||||
Expect(mailer.GetLink()).To(Equal("https://example.com"))
|
||||
})
|
||||
|
||||
It("should set and get logo", func() {
|
||||
mailer.SetLogo("https://example.com/logo.png")
|
||||
Expect(mailer.GetLogo()).To(Equal("https://example.com/logo.png"))
|
||||
})
|
||||
|
||||
It("should set and get copyright", func() {
|
||||
mailer.SetCopyright("© 2024 Test Company")
|
||||
Expect(mailer.GetCopyright()).To(Equal("© 2024 Test Company"))
|
||||
})
|
||||
|
||||
It("should set and get trouble text", func() {
|
||||
mailer.SetTroubleText("Having trouble? Contact support")
|
||||
Expect(mailer.GetTroubleText()).To(Equal("Having trouble? Contact support"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when setting empty values", func() {
|
||||
It("should handle empty strings", func() {
|
||||
mailer.SetName("")
|
||||
mailer.SetLink("")
|
||||
mailer.SetLogo("")
|
||||
mailer.SetCopyright("")
|
||||
mailer.SetTroubleText("")
|
||||
|
||||
Expect(mailer.GetName()).To(BeEmpty())
|
||||
Expect(mailer.GetLink()).To(BeEmpty())
|
||||
Expect(mailer.GetLogo()).To(BeEmpty())
|
||||
Expect(mailer.GetCopyright()).To(BeEmpty())
|
||||
Expect(mailer.GetTroubleText()).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Body Management", func() {
|
||||
Context("when setting and getting body", func() {
|
||||
It("should set and get body", func() {
|
||||
body := &hermes.Body{
|
||||
Name: "John Doe",
|
||||
Intros: []string{"Welcome!"},
|
||||
Greeting: "Hello",
|
||||
Signature: "Best regards",
|
||||
Title: "Test Email",
|
||||
}
|
||||
|
||||
mailer.SetBody(body)
|
||||
retrievedBody := mailer.GetBody()
|
||||
|
||||
Expect(retrievedBody).ToNot(BeNil())
|
||||
Expect(retrievedBody.Name).To(Equal("John Doe"))
|
||||
Expect(retrievedBody.Intros).To(HaveLen(1))
|
||||
Expect(retrievedBody.Intros[0]).To(Equal("Welcome!"))
|
||||
Expect(retrievedBody.Greeting).To(Equal("Hello"))
|
||||
Expect(retrievedBody.Signature).To(Equal("Best regards"))
|
||||
Expect(retrievedBody.Title).To(Equal("Test Email"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("CSS Inline", func() {
|
||||
Context("when setting CSS inline option", func() {
|
||||
It("should set disable CSS inline to true", func() {
|
||||
mailer.SetCSSInline(true)
|
||||
// No getter for this, but it should not panic
|
||||
})
|
||||
|
||||
It("should set disable CSS inline to false", func() {
|
||||
mailer.SetCSSInline(false)
|
||||
// No getter for this, but it should not panic
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Clone", func() {
|
||||
Context("when cloning a mailer", func() {
|
||||
BeforeEach(func() {
|
||||
mailer.SetName("Original Name")
|
||||
mailer.SetLink("https://original.com")
|
||||
mailer.SetLogo("https://original.com/logo.png")
|
||||
mailer.SetCopyright("© Original")
|
||||
mailer.SetTroubleText("Original trouble")
|
||||
mailer.SetTheme(render.ThemeFlat)
|
||||
mailer.SetTextDirection(render.RightToLeft)
|
||||
|
||||
body := &hermes.Body{
|
||||
Name: "Original Body",
|
||||
Intros: []string{"Intro 1", "Intro 2"},
|
||||
Outros: []string{"Outro 1"},
|
||||
Dictionary: []hermes.Entry{
|
||||
{Key: "Key1", Value: "Value1"},
|
||||
},
|
||||
Actions: []hermes.Action{
|
||||
{
|
||||
Instructions: "Click the button",
|
||||
Button: hermes.Button{
|
||||
Text: "Click me",
|
||||
Link: "https://example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
Tables: []hermes.Table{{
|
||||
Data: [][]hermes.Entry{
|
||||
{{Key: "Col1", Value: "Val1"}},
|
||||
},
|
||||
Columns: hermes.Columns{
|
||||
CustomWidth: map[string]string{"col1": "100px"},
|
||||
},
|
||||
}},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
})
|
||||
|
||||
It("should create an independent copy", func() {
|
||||
clone := mailer.Clone()
|
||||
|
||||
Expect(clone).ToNot(BeNil())
|
||||
Expect(clone.GetName()).To(Equal(mailer.GetName()))
|
||||
Expect(clone.GetLink()).To(Equal(mailer.GetLink()))
|
||||
Expect(clone.GetLogo()).To(Equal(mailer.GetLogo()))
|
||||
Expect(clone.GetCopyright()).To(Equal(mailer.GetCopyright()))
|
||||
Expect(clone.GetTroubleText()).To(Equal(mailer.GetTroubleText()))
|
||||
Expect(clone.GetTheme()).To(Equal(mailer.GetTheme()))
|
||||
Expect(clone.GetTextDirection()).To(Equal(mailer.GetTextDirection()))
|
||||
})
|
||||
|
||||
It("should have independent body slices", func() {
|
||||
clone := mailer.Clone()
|
||||
|
||||
// Modify clone's intros
|
||||
cloneBody := clone.GetBody()
|
||||
cloneBody.Intros = append(cloneBody.Intros, "New Intro")
|
||||
|
||||
// Original should not be affected
|
||||
originalBody := mailer.GetBody()
|
||||
Expect(originalBody.Intros).To(HaveLen(2))
|
||||
Expect(cloneBody.Intros).To(HaveLen(3))
|
||||
})
|
||||
|
||||
It("should have independent dictionary entries", func() {
|
||||
clone := mailer.Clone()
|
||||
|
||||
// Modify clone's dictionary
|
||||
cloneBody := clone.GetBody()
|
||||
cloneBody.Dictionary[0].Value = "Modified Value"
|
||||
|
||||
// Original should not be affected
|
||||
originalBody := mailer.GetBody()
|
||||
Expect(originalBody.Dictionary[0].Value).To(Equal("Value1"))
|
||||
Expect(cloneBody.Dictionary[0].Value).To(Equal("Modified Value"))
|
||||
})
|
||||
|
||||
It("should have independent table data", func() {
|
||||
clone := mailer.Clone()
|
||||
|
||||
// Modify clone's table data
|
||||
cloneBody := clone.GetBody()
|
||||
cloneBody.Tables[0].Data[0][0].Value = "Modified"
|
||||
|
||||
// Original should not be affected
|
||||
originalBody := mailer.GetBody()
|
||||
Expect(originalBody.Tables[0].Data[0][0].Value).To(Equal("Val1"))
|
||||
Expect(cloneBody.Tables[0].Data[0][0].Value).To(Equal("Modified"))
|
||||
})
|
||||
|
||||
It("should handle nil slices in body", func() {
|
||||
emptyMailer := render.New()
|
||||
emptyMailer.SetBody(&hermes.Body{
|
||||
Name: "Test",
|
||||
})
|
||||
|
||||
clone := emptyMailer.Clone()
|
||||
Expect(clone).ToNot(BeNil())
|
||||
Expect(clone.GetBody()).ToNot(BeNil())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
201
mail/render/render.go
Normal file
201
mail/render/render.go
Normal file
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020 Nicolas JUHEL
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
|
||||
"github.com/go-hermes/hermes/v2"
|
||||
liberr "github.com/nabbar/golib/errors"
|
||||
)
|
||||
|
||||
// ParseData replaces all template variables in the email content with their corresponding values.
|
||||
// Template variables should be in the format "{{key}}" and will be replaced with the
|
||||
// corresponding value from the data map.
|
||||
//
|
||||
// This method affects all textual content including:
|
||||
// - Product information (name, link, logo, copyright, trouble text)
|
||||
// - Body text fields (name, greeting, signature, title, free markdown)
|
||||
// - Intro and outro texts
|
||||
// - Dictionary entries (keys and values)
|
||||
// - Table data (keys and values)
|
||||
// - Actions (instructions, invite codes, button text, links, and colors)
|
||||
//
|
||||
// The replacement is performed in-place, modifying the email instance.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// mailer := render.New()
|
||||
// mailer.SetName("{{company}}")
|
||||
// body := &hermes.Body{
|
||||
// Name: "{{user}}",
|
||||
// Intros: []string{"Your code is {{code}}"},
|
||||
// }
|
||||
// mailer.SetBody(body)
|
||||
// mailer.ParseData(map[string]string{
|
||||
// "{{company}}": "Acme Inc",
|
||||
// "{{user}}": "John Doe",
|
||||
// "{{code}}": "123456",
|
||||
// })
|
||||
func (e *email) ParseData(data map[string]string) {
|
||||
for k, v := range data {
|
||||
e.p.Copyright = strings.ReplaceAll(e.p.Copyright, k, v)
|
||||
e.p.Link = strings.ReplaceAll(e.p.Link, k, v)
|
||||
e.p.Logo = strings.ReplaceAll(e.p.Logo, k, v)
|
||||
e.p.Name = strings.ReplaceAll(e.p.Name, k, v)
|
||||
e.p.TroubleText = strings.ReplaceAll(e.p.TroubleText, k, v)
|
||||
|
||||
e.b.Greeting = strings.ReplaceAll(e.b.Greeting, k, v)
|
||||
e.b.Name = strings.ReplaceAll(e.b.Name, k, v)
|
||||
e.b.Signature = strings.ReplaceAll(e.b.Signature, k, v)
|
||||
e.b.Title = strings.ReplaceAll(e.b.Title, k, v)
|
||||
|
||||
e.b.FreeMarkdown = hermes.Markdown(strings.ReplaceAll(string(e.b.FreeMarkdown), k, v))
|
||||
|
||||
for i := range e.b.Intros {
|
||||
e.b.Intros[i] = strings.ReplaceAll(e.b.Intros[i], k, v)
|
||||
}
|
||||
|
||||
for i := range e.b.Outros {
|
||||
e.b.Outros[i] = strings.ReplaceAll(e.b.Outros[i], k, v)
|
||||
}
|
||||
|
||||
for i := range e.b.Dictionary {
|
||||
e.b.Dictionary[i].Key = strings.ReplaceAll(e.b.Dictionary[i].Key, k, v)
|
||||
e.b.Dictionary[i].Value = strings.ReplaceAll(e.b.Dictionary[i].Value, k, v)
|
||||
}
|
||||
|
||||
for t := range e.b.Tables {
|
||||
for i := range e.b.Tables[t].Data {
|
||||
for j := range e.b.Tables[t].Data[i] {
|
||||
e.b.Tables[t].Data[i][j].Key = strings.ReplaceAll(e.b.Tables[t].Data[i][j].Key, k, v)
|
||||
e.b.Tables[t].Data[i][j].Value = strings.ReplaceAll(e.b.Tables[t].Data[i][j].Value, k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := range e.b.Actions {
|
||||
e.b.Actions[i].Instructions = strings.ReplaceAll(e.b.Actions[i].Instructions, k, v)
|
||||
e.b.Actions[i].InviteCode = strings.ReplaceAll(e.b.Actions[i].InviteCode, k, v)
|
||||
e.b.Actions[i].Button.Link = strings.ReplaceAll(e.b.Actions[i].Button.Link, k, v)
|
||||
e.b.Actions[i].Button.Text = strings.ReplaceAll(e.b.Actions[i].Button.Text, k, v)
|
||||
e.b.Actions[i].Button.Color = strings.ReplaceAll(e.b.Actions[i].Button.Color, k, v)
|
||||
e.b.Actions[i].Button.TextColor = strings.ReplaceAll(e.b.Actions[i].Button.TextColor, k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateHTML generates the HTML version of the email.
|
||||
// It uses the configured theme, text direction, and all content to create
|
||||
// a complete HTML email document ready to be sent.
|
||||
//
|
||||
// Returns:
|
||||
// - A buffer containing the complete HTML document
|
||||
// - An error if the HTML generation fails
|
||||
//
|
||||
// The generated HTML includes inline CSS (unless disabled via SetCSSInline)
|
||||
// and is optimized for email client compatibility.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// mailer := render.New()
|
||||
// mailer.SetName("My Company")
|
||||
// body := &hermes.Body{Name: "User", Intros: []string{"Welcome!"}}
|
||||
// mailer.SetBody(body)
|
||||
// htmlBuf, err := mailer.GenerateHTML()
|
||||
// if err == nil {
|
||||
// // Send htmlBuf.String() via email
|
||||
// }
|
||||
func (e *email) GenerateHTML() (*bytes.Buffer, liberr.Error) {
|
||||
return e.generated(e.genHtml)
|
||||
}
|
||||
|
||||
// GeneratePlainText generates the plain text version of the email.
|
||||
// It creates a formatted text version of the email content without any HTML.
|
||||
//
|
||||
// Returns:
|
||||
// - A buffer containing the formatted plain text
|
||||
// - An error if the text generation fails
|
||||
//
|
||||
// The plain text version includes all content formatted in a readable way
|
||||
// suitable for email clients that don't support HTML or for multipart emails.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// mailer := render.New()
|
||||
// body := &hermes.Body{Name: "User", Intros: []string{"Welcome!"}}
|
||||
// mailer.SetBody(body)
|
||||
// textBuf, err := mailer.GeneratePlainText()
|
||||
// if err == nil {
|
||||
// // Send textBuf.String() as plain text
|
||||
// }
|
||||
func (e *email) GeneratePlainText() (*bytes.Buffer, liberr.Error) {
|
||||
return e.generated(e.genText)
|
||||
}
|
||||
|
||||
// generated is the internal method that handles email generation.
|
||||
// It creates a Hermes instance with the current configuration and calls
|
||||
// the provided generation function (either HTML or plain text).
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The generation function (genHtml or genText)
|
||||
//
|
||||
// Returns:
|
||||
// - A buffer containing the generated email content
|
||||
// - An error if generation fails
|
||||
func (e email) generated(f func(h hermes.Hermes, m hermes.Email) (string, error)) (*bytes.Buffer, liberr.Error) {
|
||||
var buf = bytes.NewBuffer(make([]byte, 0))
|
||||
|
||||
h := hermes.Hermes{
|
||||
Theme: e.t.getHermes(),
|
||||
TextDirection: e.d.getDirection(),
|
||||
Product: e.p,
|
||||
DisableCSSInlining: e.c,
|
||||
}
|
||||
|
||||
if p, e := f(h, hermes.Email{
|
||||
Body: *e.b,
|
||||
}); e != nil {
|
||||
return nil, ErrorMailerText.Error(e)
|
||||
} else {
|
||||
buf.WriteString(p)
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// genText is a wrapper around the Hermes GeneratePlainText method.
|
||||
// It generates the plain text version of an email using the Hermes library.
|
||||
func (e *email) genText(h hermes.Hermes, m hermes.Email) (string, error) {
|
||||
return h.GeneratePlainText(m)
|
||||
}
|
||||
|
||||
// genHtml is a wrapper around the Hermes GenerateHTML method.
|
||||
// It generates the HTML version of an email using the Hermes library.
|
||||
func (e *email) genHtml(h hermes.Hermes, m hermes.Email) (string, error) {
|
||||
return h.GenerateHTML(m)
|
||||
}
|
||||
38
mail/render/render_suite_test.go
Normal file
38
mail/render/render_suite_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 Nicolas JUHEL
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
package render_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestRender(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Mail Render Suite")
|
||||
}
|
||||
654
mail/render/render_test.go
Normal file
654
mail/render/render_test.go
Normal file
@@ -0,0 +1,654 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 Nicolas JUHEL
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
package render_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-hermes/hermes/v2"
|
||||
"github.com/nabbar/golib/mail/render"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Email Rendering", func() {
|
||||
var mailer render.Mailer
|
||||
|
||||
BeforeEach(func() {
|
||||
mailer = render.New()
|
||||
mailer.SetName("Test Company")
|
||||
mailer.SetLink("https://example.com")
|
||||
mailer.SetLogo("https://example.com/logo.png")
|
||||
mailer.SetCopyright("© 2024 Test Company")
|
||||
mailer.SetTroubleText("Having trouble? Contact us")
|
||||
})
|
||||
|
||||
Describe("HTML Generation", func() {
|
||||
Context("with basic content", func() {
|
||||
BeforeEach(func() {
|
||||
body := &hermes.Body{
|
||||
Name: "John Doe",
|
||||
Intros: []string{"Welcome to our service!"},
|
||||
Outros: []string{"Thank you for using our service."},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
})
|
||||
|
||||
It("should generate valid HTML", func() {
|
||||
buf, err := mailer.GenerateHTML()
|
||||
|
||||
Expect(err).To(BeNil())
|
||||
Expect(buf).ToNot(BeNil())
|
||||
Expect(buf.Len()).To(BeNumerically(">", 0))
|
||||
|
||||
html := buf.String()
|
||||
Expect(html).To(ContainSubstring("<!DOCTYPE html"))
|
||||
Expect(html).To(ContainSubstring("John Doe"))
|
||||
Expect(html).To(ContainSubstring("Welcome to our service!"))
|
||||
})
|
||||
|
||||
It("should include product information in HTML", func() {
|
||||
buf, err := mailer.GenerateHTML()
|
||||
|
||||
Expect(err).To(BeNil())
|
||||
html := buf.String()
|
||||
Expect(html).To(ContainSubstring("Test Company"))
|
||||
Expect(html).To(ContainSubstring("© 2024 Test Company"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("with complex content", func() {
|
||||
BeforeEach(func() {
|
||||
body := &hermes.Body{
|
||||
Name: "User",
|
||||
Intros: []string{"Here is your activity report:"},
|
||||
Dictionary: []hermes.Entry{
|
||||
{Key: "Transaction ID", Value: "123456"},
|
||||
{Key: "Amount", Value: "$99.99"},
|
||||
},
|
||||
Tables: []hermes.Table{{
|
||||
Data: [][]hermes.Entry{
|
||||
{
|
||||
{Key: "Item", Value: "Product A"},
|
||||
{Key: "Price", Value: "$49.99"},
|
||||
},
|
||||
{
|
||||
{Key: "Item", Value: "Product B"},
|
||||
{Key: "Price", Value: "$49.99"},
|
||||
},
|
||||
},
|
||||
Columns: hermes.Columns{
|
||||
CustomWidth: map[string]string{
|
||||
"Item": "80%",
|
||||
"Price": "20%",
|
||||
},
|
||||
},
|
||||
}},
|
||||
Actions: []hermes.Action{
|
||||
{
|
||||
Instructions: "To view your order, click below:",
|
||||
Button: hermes.Button{
|
||||
Text: "View Order",
|
||||
Link: "https://example.com/order/123456",
|
||||
},
|
||||
},
|
||||
},
|
||||
Outros: []string{"Need help? Reply to this email."},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
})
|
||||
|
||||
It("should generate HTML with table data", func() {
|
||||
buf, err := mailer.GenerateHTML()
|
||||
|
||||
Expect(err).To(BeNil())
|
||||
html := buf.String()
|
||||
Expect(html).To(ContainSubstring("Product A"))
|
||||
Expect(html).To(ContainSubstring("Product B"))
|
||||
Expect(html).To(ContainSubstring("$49.99"))
|
||||
})
|
||||
|
||||
It("should generate HTML with action button", func() {
|
||||
buf, err := mailer.GenerateHTML()
|
||||
|
||||
Expect(err).To(BeNil())
|
||||
html := buf.String()
|
||||
Expect(html).To(ContainSubstring("View Order"))
|
||||
Expect(html).To(ContainSubstring("https://example.com/order/123456"))
|
||||
})
|
||||
|
||||
It("should generate HTML with dictionary entries", func() {
|
||||
buf, err := mailer.GenerateHTML()
|
||||
|
||||
Expect(err).To(BeNil())
|
||||
html := buf.String()
|
||||
Expect(html).To(ContainSubstring("Transaction ID"))
|
||||
Expect(html).To(ContainSubstring("123456"))
|
||||
Expect(html).To(ContainSubstring("$99.99"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("with different themes", func() {
|
||||
BeforeEach(func() {
|
||||
body := &hermes.Body{
|
||||
Name: "User",
|
||||
Intros: []string{"Test email"},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
})
|
||||
|
||||
It("should generate HTML with default theme", func() {
|
||||
mailer.SetTheme(render.ThemeDefault)
|
||||
buf, err := mailer.GenerateHTML()
|
||||
|
||||
Expect(err).To(BeNil())
|
||||
Expect(buf.Len()).To(BeNumerically(">", 0))
|
||||
})
|
||||
|
||||
It("should generate HTML with flat theme", func() {
|
||||
mailer.SetTheme(render.ThemeFlat)
|
||||
buf, err := mailer.GenerateHTML()
|
||||
|
||||
Expect(err).To(BeNil())
|
||||
Expect(buf.Len()).To(BeNumerically(">", 0))
|
||||
})
|
||||
})
|
||||
|
||||
Context("with CSS inline options", func() {
|
||||
BeforeEach(func() {
|
||||
body := &hermes.Body{
|
||||
Name: "User",
|
||||
Intros: []string{"Test"},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
})
|
||||
|
||||
It("should generate HTML with CSS inline enabled", func() {
|
||||
mailer.SetCSSInline(false)
|
||||
buf, err := mailer.GenerateHTML()
|
||||
|
||||
Expect(err).To(BeNil())
|
||||
html := buf.String()
|
||||
// With inline CSS, styles should be in style attributes
|
||||
Expect(html).To(ContainSubstring("style="))
|
||||
})
|
||||
|
||||
It("should generate HTML with CSS inline disabled", func() {
|
||||
mailer.SetCSSInline(true)
|
||||
buf, err := mailer.GenerateHTML()
|
||||
|
||||
Expect(err).To(BeNil())
|
||||
Expect(buf).ToNot(BeNil())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Plain Text Generation", func() {
|
||||
Context("with basic content", func() {
|
||||
BeforeEach(func() {
|
||||
body := &hermes.Body{
|
||||
Name: "John Doe",
|
||||
Intros: []string{"Welcome to our service!"},
|
||||
Outros: []string{"Thank you."},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
})
|
||||
|
||||
It("should generate valid plain text", func() {
|
||||
buf, err := mailer.GeneratePlainText()
|
||||
|
||||
Expect(err).To(BeNil())
|
||||
Expect(buf).ToNot(BeNil())
|
||||
Expect(buf.Len()).To(BeNumerically(">", 0))
|
||||
|
||||
text := buf.String()
|
||||
Expect(text).To(ContainSubstring("John Doe"))
|
||||
Expect(text).To(ContainSubstring("Welcome to our service!"))
|
||||
Expect(text).ToNot(ContainSubstring("<html>"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("with complex content", func() {
|
||||
BeforeEach(func() {
|
||||
body := &hermes.Body{
|
||||
Name: "User",
|
||||
Intros: []string{"Report:"},
|
||||
Dictionary: []hermes.Entry{
|
||||
{Key: "ID", Value: "123"},
|
||||
},
|
||||
Tables: []hermes.Table{{
|
||||
Data: [][]hermes.Entry{
|
||||
{{Key: "Item", Value: "A"}},
|
||||
},
|
||||
}},
|
||||
Actions: []hermes.Action{
|
||||
{
|
||||
Instructions: "Click here:",
|
||||
Button: hermes.Button{
|
||||
Text: "Action",
|
||||
Link: "https://example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
})
|
||||
|
||||
It("should include all content in plain text", func() {
|
||||
buf, err := mailer.GeneratePlainText()
|
||||
|
||||
Expect(err).To(BeNil())
|
||||
text := buf.String()
|
||||
Expect(text).To(ContainSubstring("User"))
|
||||
Expect(text).To(ContainSubstring("Report:"))
|
||||
Expect(text).To(ContainSubstring("https://example.com"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("ParseData", func() {
|
||||
Context("with template variables", func() {
|
||||
BeforeEach(func() {
|
||||
body := &hermes.Body{
|
||||
Name: "{{username}}",
|
||||
Intros: []string{"Hello {{username}}, your code is {{code}}"},
|
||||
Greeting: "Hi {{username}}",
|
||||
Signature: "{{company}}",
|
||||
Title: "Welcome {{username}}",
|
||||
Dictionary: []hermes.Entry{
|
||||
{Key: "{{key}}", Value: "{{value}}"},
|
||||
},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
mailer.SetName("{{company}}")
|
||||
mailer.SetCopyright("© {{year}} {{company}}")
|
||||
})
|
||||
|
||||
It("should replace all template variables", func() {
|
||||
data := map[string]string{
|
||||
"{{username}}": "John Doe",
|
||||
"{{code}}": "123456",
|
||||
"{{company}}": "Test Inc",
|
||||
"{{year}}": "2024",
|
||||
"{{key}}": "Key1",
|
||||
"{{value}}": "Value1",
|
||||
}
|
||||
|
||||
mailer.ParseData(data)
|
||||
|
||||
body := mailer.GetBody()
|
||||
Expect(body.Name).To(Equal("John Doe"))
|
||||
Expect(body.Intros[0]).To(Equal("Hello John Doe, your code is 123456"))
|
||||
Expect(body.Greeting).To(Equal("Hi John Doe"))
|
||||
Expect(body.Signature).To(Equal("Test Inc"))
|
||||
Expect(body.Title).To(Equal("Welcome John Doe"))
|
||||
Expect(body.Dictionary[0].Key).To(Equal("Key1"))
|
||||
Expect(body.Dictionary[0].Value).To(Equal("Value1"))
|
||||
|
||||
Expect(mailer.GetName()).To(Equal("Test Inc"))
|
||||
Expect(mailer.GetCopyright()).To(Equal("© 2024 Test Inc"))
|
||||
})
|
||||
|
||||
It("should handle multiple replacements in same string", func() {
|
||||
body := &hermes.Body{
|
||||
Intros: []string{"{{var1}} and {{var2}} and {{var1}}"},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
|
||||
data := map[string]string{
|
||||
"{{var1}}": "First",
|
||||
"{{var2}}": "Second",
|
||||
}
|
||||
|
||||
mailer.ParseData(data)
|
||||
|
||||
body = mailer.GetBody()
|
||||
Expect(body.Intros[0]).To(Equal("First and Second and First"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("with table and action replacements", func() {
|
||||
BeforeEach(func() {
|
||||
body := &hermes.Body{
|
||||
Tables: []hermes.Table{{
|
||||
Data: [][]hermes.Entry{
|
||||
{
|
||||
{Key: "{{header1}}", Value: "{{value1}}"},
|
||||
{Key: "{{header2}}", Value: "{{value2}}"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
Actions: []hermes.Action{
|
||||
{
|
||||
Instructions: "{{instruction}}",
|
||||
InviteCode: "{{code}}",
|
||||
Button: hermes.Button{
|
||||
Text: "{{buttonText}}",
|
||||
Link: "{{buttonLink}}",
|
||||
Color: "{{color}}",
|
||||
TextColor: "{{textColor}}",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
})
|
||||
|
||||
It("should replace variables in table data", func() {
|
||||
data := map[string]string{
|
||||
"{{header1}}": "Name",
|
||||
"{{header2}}": "Email",
|
||||
"{{value1}}": "John",
|
||||
"{{value2}}": "john@example.com",
|
||||
}
|
||||
|
||||
mailer.ParseData(data)
|
||||
|
||||
body := mailer.GetBody()
|
||||
Expect(body.Tables[0].Data[0][0].Key).To(Equal("Name"))
|
||||
Expect(body.Tables[0].Data[0][0].Value).To(Equal("John"))
|
||||
Expect(body.Tables[0].Data[0][1].Key).To(Equal("Email"))
|
||||
Expect(body.Tables[0].Data[0][1].Value).To(Equal("john@example.com"))
|
||||
})
|
||||
|
||||
It("should replace variables in actions", func() {
|
||||
data := map[string]string{
|
||||
"{{instruction}}": "Click below",
|
||||
"{{code}}": "INVITE123",
|
||||
"{{buttonText}}": "Accept",
|
||||
"{{buttonLink}}": "https://example.com/accept",
|
||||
"{{color}}": "#007bff",
|
||||
"{{textColor}}": "#ffffff",
|
||||
}
|
||||
|
||||
mailer.ParseData(data)
|
||||
|
||||
body := mailer.GetBody()
|
||||
action := body.Actions[0]
|
||||
Expect(action.Instructions).To(Equal("Click below"))
|
||||
Expect(action.InviteCode).To(Equal("INVITE123"))
|
||||
Expect(action.Button.Text).To(Equal("Accept"))
|
||||
Expect(action.Button.Link).To(Equal("https://example.com/accept"))
|
||||
Expect(action.Button.Color).To(Equal("#007bff"))
|
||||
Expect(action.Button.TextColor).To(Equal("#ffffff"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("with markdown content", func() {
|
||||
It("should replace variables in free markdown", func() {
|
||||
body := &hermes.Body{
|
||||
FreeMarkdown: hermes.Markdown("# {{title}}\n\nHello {{name}}!"),
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
|
||||
data := map[string]string{
|
||||
"{{title}}": "Welcome",
|
||||
"{{name}}": "User",
|
||||
}
|
||||
|
||||
mailer.ParseData(data)
|
||||
|
||||
body = mailer.GetBody()
|
||||
markdown := string(body.FreeMarkdown)
|
||||
Expect(markdown).To(ContainSubstring("# Welcome"))
|
||||
Expect(markdown).To(ContainSubstring("Hello User!"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("with empty or nil data", func() {
|
||||
It("should handle empty data map", func() {
|
||||
body := &hermes.Body{
|
||||
Name: "Original",
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
|
||||
mailer.ParseData(map[string]string{})
|
||||
|
||||
// Should not modify anything
|
||||
Expect(mailer.GetBody().Name).To(Equal("Original"))
|
||||
})
|
||||
|
||||
It("should handle nil slices gracefully", func() {
|
||||
body := &hermes.Body{
|
||||
Name: "Test",
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
|
||||
// Should not panic
|
||||
mailer.ParseData(map[string]string{"{{test}}": "value"})
|
||||
Expect(mailer.GetBody().Name).To(Equal("Test"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Integration", func() {
|
||||
Context("when using complete workflow", func() {
|
||||
It("should create, configure, parse and generate email", func() {
|
||||
// Configure
|
||||
mailer.SetName("{{company}}")
|
||||
mailer.SetTheme(render.ThemeFlat)
|
||||
|
||||
// Set body
|
||||
body := &hermes.Body{
|
||||
Name: "{{user}}",
|
||||
Intros: []string{"Welcome {{user}}!"},
|
||||
Actions: []hermes.Action{
|
||||
{
|
||||
Instructions: "Verify your email:",
|
||||
Button: hermes.Button{
|
||||
Text: "Verify",
|
||||
Link: "{{link}}",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
|
||||
// Parse data
|
||||
mailer.ParseData(map[string]string{
|
||||
"{{company}}": "Test Inc",
|
||||
"{{user}}": "John",
|
||||
"{{link}}": "https://example.com/verify",
|
||||
})
|
||||
|
||||
// Generate both formats
|
||||
htmlBuf, htmlErr := mailer.GenerateHTML()
|
||||
textBuf, textErr := mailer.GeneratePlainText()
|
||||
|
||||
Expect(htmlErr).To(BeNil())
|
||||
Expect(textErr).To(BeNil())
|
||||
Expect(htmlBuf.Len()).To(BeNumerically(">", 0))
|
||||
Expect(textBuf.Len()).To(BeNumerically(">", 0))
|
||||
|
||||
html := htmlBuf.String()
|
||||
text := textBuf.String()
|
||||
|
||||
Expect(html).To(ContainSubstring("John"))
|
||||
Expect(html).To(ContainSubstring("Welcome John!"))
|
||||
Expect(html).To(ContainSubstring("https://example.com/verify"))
|
||||
|
||||
Expect(text).To(ContainSubstring("John"))
|
||||
Expect(text).To(ContainSubstring("Welcome John!"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Error Handling", func() {
|
||||
Context("with invalid configurations", func() {
|
||||
It("should handle generation with minimal setup", func() {
|
||||
minimalMailer := render.New()
|
||||
minimalMailer.SetBody(&hermes.Body{
|
||||
Name: "Test",
|
||||
})
|
||||
|
||||
htmlBuf, htmlErr := minimalMailer.GenerateHTML()
|
||||
textBuf, textErr := minimalMailer.GeneratePlainText()
|
||||
|
||||
// Should not error with minimal setup
|
||||
Expect(htmlErr).To(BeNil())
|
||||
Expect(textErr).To(BeNil())
|
||||
Expect(htmlBuf).ToNot(BeNil())
|
||||
Expect(textBuf).ToNot(BeNil())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Special Characters", func() {
|
||||
Context("when content contains special characters", func() {
|
||||
It("should handle HTML special characters", func() {
|
||||
body := &hermes.Body{
|
||||
Name: "User <>&\"'",
|
||||
Intros: []string{"Test <script>alert('xss')</script>"},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
|
||||
buf, err := mailer.GenerateHTML()
|
||||
|
||||
Expect(err).To(BeNil())
|
||||
html := buf.String()
|
||||
// Hermes should escape HTML entities
|
||||
Expect(html).ToNot(ContainSubstring("<script>"))
|
||||
})
|
||||
|
||||
It("should handle unicode characters", func() {
|
||||
body := &hermes.Body{
|
||||
Name: "用户 👤",
|
||||
Intros: []string{"Hello 世界 🌍!"},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
|
||||
htmlBuf, htmlErr := mailer.GenerateHTML()
|
||||
textBuf, textErr := mailer.GeneratePlainText()
|
||||
|
||||
Expect(htmlErr).To(BeNil())
|
||||
Expect(textErr).To(BeNil())
|
||||
Expect(htmlBuf.String()).To(ContainSubstring("用户"))
|
||||
Expect(textBuf.String()).To(ContainSubstring("世界"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Text Direction", func() {
|
||||
Context("with different text directions", func() {
|
||||
It("should generate with left-to-right direction", func() {
|
||||
mailer.SetTextDirection(render.LeftToRight)
|
||||
body := &hermes.Body{
|
||||
Name: "User",
|
||||
Intros: []string{"Test"},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
|
||||
buf, err := mailer.GenerateHTML()
|
||||
|
||||
Expect(err).To(BeNil())
|
||||
html := buf.String()
|
||||
Expect(html).To(ContainSubstring("ltr"))
|
||||
})
|
||||
|
||||
It("should generate with right-to-left direction", func() {
|
||||
mailer.SetTextDirection(render.RightToLeft)
|
||||
body := &hermes.Body{
|
||||
Name: "مستخدم",
|
||||
Intros: []string{"مرحبا"},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
|
||||
buf, err := mailer.GenerateHTML()
|
||||
|
||||
Expect(err).To(BeNil())
|
||||
html := buf.String()
|
||||
Expect(html).To(ContainSubstring("rtl"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Empty Content", func() {
|
||||
Context("with empty body sections", func() {
|
||||
It("should handle empty intros", func() {
|
||||
body := &hermes.Body{
|
||||
Name: "User",
|
||||
Intros: []string{},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
|
||||
buf, err := mailer.GenerateHTML()
|
||||
Expect(err).To(BeNil())
|
||||
Expect(buf).ToNot(BeNil())
|
||||
})
|
||||
|
||||
It("should handle empty outros", func() {
|
||||
body := &hermes.Body{
|
||||
Name: "User",
|
||||
Outros: []string{},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
|
||||
buf, err := mailer.GeneratePlainText()
|
||||
Expect(err).To(BeNil())
|
||||
Expect(buf).ToNot(BeNil())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Long Content", func() {
|
||||
Context("with very long strings", func() {
|
||||
It("should handle long intro text", func() {
|
||||
longText := strings.Repeat("Lorem ipsum dolor sit amet. ", 100)
|
||||
body := &hermes.Body{
|
||||
Name: "User",
|
||||
Intros: []string{longText},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
|
||||
buf, err := mailer.GenerateHTML()
|
||||
Expect(err).To(BeNil())
|
||||
Expect(buf.Len()).To(BeNumerically(">", len(longText)))
|
||||
})
|
||||
|
||||
It("should handle many table rows", func() {
|
||||
data := make([][]hermes.Entry, 100)
|
||||
for i := range data {
|
||||
data[i] = []hermes.Entry{
|
||||
{Key: "Item", Value: "Row " + string(rune(i))},
|
||||
}
|
||||
}
|
||||
|
||||
body := &hermes.Body{
|
||||
Name: "User",
|
||||
Tables: []hermes.Table{{
|
||||
Data: data,
|
||||
}},
|
||||
}
|
||||
mailer.SetBody(body)
|
||||
|
||||
buf, err := mailer.GenerateHTML()
|
||||
Expect(err).To(BeNil())
|
||||
Expect(buf).ToNot(BeNil())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
97
mail/render/themes.go
Normal file
97
mail/render/themes.go
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020 Nicolas JUHEL
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
package render
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-hermes/hermes/v2"
|
||||
)
|
||||
|
||||
// Themes represents the available email template themes.
|
||||
// Each theme provides a different visual style for the generated emails.
|
||||
//
|
||||
// The themes are based on github.com/go-hermes/hermes/v2 theme implementations.
|
||||
type Themes uint8
|
||||
|
||||
const (
|
||||
// ThemeDefault is the default theme provided by Hermes.
|
||||
// It offers a classic, clean email design with a centered layout.
|
||||
ThemeDefault Themes = iota
|
||||
|
||||
// ThemeFlat is the flat theme provided by Hermes.
|
||||
// It offers a modern, minimalist email design with flat colors.
|
||||
ThemeFlat
|
||||
)
|
||||
|
||||
// getHermes converts the Themes enum to the corresponding hermes.Theme implementation.
|
||||
// This is an internal method used by the email generation process.
|
||||
func (t Themes) getHermes() hermes.Theme {
|
||||
switch t {
|
||||
case ThemeDefault:
|
||||
return &hermes.Default{}
|
||||
case ThemeFlat:
|
||||
return &hermes.Flat{}
|
||||
}
|
||||
|
||||
return ThemeDefault.getHermes()
|
||||
}
|
||||
|
||||
// String returns the string representation of the theme.
|
||||
// The string value matches the theme name from the Hermes library.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// theme := render.ThemeFlat
|
||||
// fmt.Println(theme.String()) // Output: "flat"
|
||||
func (t Themes) String() string {
|
||||
return t.getHermes().Name()
|
||||
}
|
||||
|
||||
// ParseTheme parses a theme name string and returns the corresponding Themes enum value.
|
||||
// The parsing is case-insensitive.
|
||||
//
|
||||
// Supported theme names:
|
||||
// - "default" -> ThemeDefault
|
||||
// - "flat" -> ThemeFlat
|
||||
//
|
||||
// If the theme name is not recognized, ThemeDefault is returned.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// theme := render.ParseTheme("flat")
|
||||
// theme = render.ParseTheme("FLAT") // Also works
|
||||
// theme = render.ParseTheme("unknown") // Returns ThemeDefault
|
||||
func ParseTheme(theme string) Themes {
|
||||
switch strings.ToLower(theme) {
|
||||
case strings.ToLower(ThemeDefault.String()):
|
||||
return ThemeDefault
|
||||
case strings.ToLower(ThemeFlat.String()):
|
||||
return ThemeFlat
|
||||
}
|
||||
|
||||
return ThemeDefault
|
||||
}
|
||||
228
mail/render/themes_test.go
Normal file
228
mail/render/themes_test.go
Normal file
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 Nicolas JUHEL
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
package render_test
|
||||
|
||||
import (
|
||||
"github.com/nabbar/golib/mail/render"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Themes", func() {
|
||||
Describe("Theme Constants", func() {
|
||||
Context("when using theme constants", func() {
|
||||
It("should have correct default theme value", func() {
|
||||
Expect(render.ThemeDefault).To(BeNumerically("==", 0))
|
||||
})
|
||||
|
||||
It("should have correct flat theme value", func() {
|
||||
Expect(render.ThemeFlat).To(BeNumerically("==", 1))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Theme String Conversion", func() {
|
||||
Context("when converting theme to string", func() {
|
||||
It("should convert default theme to string", func() {
|
||||
str := render.ThemeDefault.String()
|
||||
Expect(str).To(Equal("default"))
|
||||
})
|
||||
|
||||
It("should convert flat theme to string", func() {
|
||||
str := render.ThemeFlat.String()
|
||||
Expect(str).To(Equal("flat"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("ParseTheme", func() {
|
||||
Context("with valid theme names", func() {
|
||||
It("should parse 'default' to ThemeDefault", func() {
|
||||
theme := render.ParseTheme("default")
|
||||
Expect(theme).To(Equal(render.ThemeDefault))
|
||||
})
|
||||
|
||||
It("should parse 'Default' (mixed case) to ThemeDefault", func() {
|
||||
theme := render.ParseTheme("Default")
|
||||
Expect(theme).To(Equal(render.ThemeDefault))
|
||||
})
|
||||
|
||||
It("should parse 'DEFAULT' (upper case) to ThemeDefault", func() {
|
||||
theme := render.ParseTheme("DEFAULT")
|
||||
Expect(theme).To(Equal(render.ThemeDefault))
|
||||
})
|
||||
|
||||
It("should parse 'flat' to ThemeFlat", func() {
|
||||
theme := render.ParseTheme("flat")
|
||||
Expect(theme).To(Equal(render.ThemeFlat))
|
||||
})
|
||||
|
||||
It("should parse 'Flat' (mixed case) to ThemeFlat", func() {
|
||||
theme := render.ParseTheme("Flat")
|
||||
Expect(theme).To(Equal(render.ThemeFlat))
|
||||
})
|
||||
|
||||
It("should parse 'FLAT' (upper case) to ThemeFlat", func() {
|
||||
theme := render.ParseTheme("FLAT")
|
||||
Expect(theme).To(Equal(render.ThemeFlat))
|
||||
})
|
||||
})
|
||||
|
||||
Context("with invalid theme names", func() {
|
||||
It("should default to ThemeDefault for unknown theme", func() {
|
||||
theme := render.ParseTheme("unknown")
|
||||
Expect(theme).To(Equal(render.ThemeDefault))
|
||||
})
|
||||
|
||||
It("should default to ThemeDefault for empty string", func() {
|
||||
theme := render.ParseTheme("")
|
||||
Expect(theme).To(Equal(render.ThemeDefault))
|
||||
})
|
||||
|
||||
It("should default to ThemeDefault for random string", func() {
|
||||
theme := render.ParseTheme("random123")
|
||||
Expect(theme).To(Equal(render.ThemeDefault))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("Text Direction", func() {
|
||||
Describe("Direction Constants", func() {
|
||||
Context("when using direction constants", func() {
|
||||
It("should have correct left to right value", func() {
|
||||
Expect(render.LeftToRight).To(BeNumerically("==", 0))
|
||||
})
|
||||
|
||||
It("should have correct right to left value", func() {
|
||||
Expect(render.RightToLeft).To(BeNumerically("==", 1))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Direction String Conversion", func() {
|
||||
Context("when converting direction to string", func() {
|
||||
It("should convert LeftToRight to string", func() {
|
||||
str := render.LeftToRight.String()
|
||||
Expect(str).To(Equal("Left->Right"))
|
||||
})
|
||||
|
||||
It("should convert RightToLeft to string", func() {
|
||||
str := render.RightToLeft.String()
|
||||
Expect(str).To(Equal("Right->Left"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("ParseTextDirection", func() {
|
||||
Context("with left-to-right variations", func() {
|
||||
It("should parse 'ltr' to LeftToRight", func() {
|
||||
dir := render.ParseTextDirection("ltr")
|
||||
Expect(dir).To(Equal(render.LeftToRight))
|
||||
})
|
||||
|
||||
It("should parse 'left-to-right' to LeftToRight", func() {
|
||||
dir := render.ParseTextDirection("left-to-right")
|
||||
Expect(dir).To(Equal(render.LeftToRight))
|
||||
})
|
||||
|
||||
It("should parse 'left' to LeftToRight", func() {
|
||||
dir := render.ParseTextDirection("left")
|
||||
Expect(dir).To(Equal(render.LeftToRight))
|
||||
})
|
||||
|
||||
It("should parse 'Left->Right' to LeftToRight", func() {
|
||||
dir := render.ParseTextDirection("Left->Right")
|
||||
Expect(dir).To(Equal(render.LeftToRight))
|
||||
})
|
||||
|
||||
It("should parse 'LTR' (upper case) to LeftToRight", func() {
|
||||
dir := render.ParseTextDirection("LTR")
|
||||
Expect(dir).To(Equal(render.LeftToRight))
|
||||
})
|
||||
})
|
||||
|
||||
Context("with right-to-left variations", func() {
|
||||
It("should parse 'rtl' to RightToLeft", func() {
|
||||
dir := render.ParseTextDirection("rtl")
|
||||
Expect(dir).To(Equal(render.RightToLeft))
|
||||
})
|
||||
|
||||
It("should parse 'right-to-left' to RightToLeft", func() {
|
||||
dir := render.ParseTextDirection("right-to-left")
|
||||
Expect(dir).To(Equal(render.RightToLeft))
|
||||
})
|
||||
|
||||
It("should parse 'right' to RightToLeft", func() {
|
||||
dir := render.ParseTextDirection("right")
|
||||
Expect(dir).To(Equal(render.RightToLeft))
|
||||
})
|
||||
|
||||
It("should parse 'Right->Left' to RightToLeft", func() {
|
||||
dir := render.ParseTextDirection("Right->Left")
|
||||
Expect(dir).To(Equal(render.RightToLeft))
|
||||
})
|
||||
|
||||
It("should parse 'RTL' (upper case) to RightToLeft", func() {
|
||||
dir := render.ParseTextDirection("RTL")
|
||||
Expect(dir).To(Equal(render.RightToLeft))
|
||||
})
|
||||
})
|
||||
|
||||
Context("with edge cases", func() {
|
||||
It("should default to LeftToRight for empty string", func() {
|
||||
dir := render.ParseTextDirection("")
|
||||
Expect(dir).To(Equal(render.LeftToRight))
|
||||
})
|
||||
|
||||
It("should default to LeftToRight for unknown string", func() {
|
||||
dir := render.ParseTextDirection("unknown")
|
||||
Expect(dir).To(Equal(render.LeftToRight))
|
||||
})
|
||||
|
||||
It("should parse correctly when both 'left' and 'right' are present", func() {
|
||||
// "right->left" should be RightToLeft (right comes first)
|
||||
dir := render.ParseTextDirection("right->left")
|
||||
Expect(dir).To(Equal(render.RightToLeft))
|
||||
|
||||
// "left->right" should be LeftToRight (left comes first)
|
||||
dir = render.ParseTextDirection("left->right")
|
||||
Expect(dir).To(Equal(render.LeftToRight))
|
||||
})
|
||||
|
||||
It("should handle strings with only 'right' as RightToLeft", func() {
|
||||
dir := render.ParseTextDirection("right")
|
||||
Expect(dir).To(Equal(render.RightToLeft))
|
||||
})
|
||||
|
||||
It("should handle strings with only 'left' as LeftToRight", func() {
|
||||
dir := render.ParseTextDirection("left")
|
||||
Expect(dir).To(Equal(render.LeftToRight))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user