fx build fix
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/notification/interface/api/localizer"
|
||||
mmail "github.com/tech/sendico/notification/internal/server/notificationimp/mail/messagebuilder"
|
||||
"github.com/tech/sendico/pkg/domainprovider"
|
||||
"github.com/tech/sendico/pkg/messaging"
|
||||
nn "github.com/tech/sendico/pkg/messaging/notifications/notification"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type AmpliMailer struct {
|
||||
logger mlogger.Logger
|
||||
producer messaging.Producer
|
||||
client Client
|
||||
source string
|
||||
}
|
||||
|
||||
func (am *AmpliMailer) Send(m mmail.MailBuilder) error {
|
||||
err := am.client.Send(m)
|
||||
if err != nil {
|
||||
am.logger.Warn("Failed to send email", zap.Error(err))
|
||||
}
|
||||
opResult := model.OperationResult{
|
||||
IsSuccessful: err == nil,
|
||||
}
|
||||
if !opResult.IsSuccessful {
|
||||
opResult.Error = err.Error()
|
||||
}
|
||||
msg, e := m.Build()
|
||||
if e != nil {
|
||||
am.logger.Warn("Failed to build message content", zap.Error(e))
|
||||
return e
|
||||
}
|
||||
if er := am.producer.SendMessage(nn.NotificationSent(am.source, &model.NotificationResult{
|
||||
Channel: "email",
|
||||
TemplateID: msg.TemplateID(),
|
||||
Locale: msg.Locale(),
|
||||
AmpliEvent: model.AmpliEvent{
|
||||
UserID: "",
|
||||
},
|
||||
Result: opResult,
|
||||
})); er != nil {
|
||||
am.logger.Warn("Failed to send mailing result", zap.Error(er))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (am *AmpliMailer) MailBuilder() mmail.MailBuilder {
|
||||
return am.client.MailBuilder()
|
||||
}
|
||||
|
||||
func NewAmpliMailer(log mlogger.Logger, sender string, producer messaging.Producer, l localizer.Localizer, dp domainprovider.DomainProvider, config *Config) (*AmpliMailer, error) {
|
||||
logger := log.Named("ampli")
|
||||
c, err := createMailClient(logger, producer, l, dp, config)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to create mailng driver", zap.Error(err), zap.String("driver", config.Driver))
|
||||
return nil, err
|
||||
}
|
||||
am := &AmpliMailer{
|
||||
logger: logger,
|
||||
client: c,
|
||||
producer: producer,
|
||||
source: sender,
|
||||
}
|
||||
am.logger.Info("Amplitude wrapper installed")
|
||||
return am, nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package mailimp
|
||||
|
||||
import (
|
||||
mmail "github.com/tech/sendico/notification/internal/server/notificationimp/mail/messagebuilder"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
)
|
||||
|
||||
type MessageBuilderImp struct {
|
||||
message *MessageImp
|
||||
}
|
||||
|
||||
func (mb *MessageBuilderImp) SetAccountID(accountID string) mmail.MailBuilder {
|
||||
mb.message.accountUID = accountID
|
||||
return mb
|
||||
}
|
||||
|
||||
func (mb *MessageBuilderImp) SetTemplateID(templateID string) mmail.MailBuilder {
|
||||
mb.message.templateID = templateID
|
||||
return mb
|
||||
}
|
||||
|
||||
func (mb *MessageBuilderImp) SetLocale(locale string) mmail.MailBuilder {
|
||||
mb.message.locale = locale
|
||||
return mb
|
||||
}
|
||||
|
||||
func (mb *MessageBuilderImp) AddButton(link string) mmail.MailBuilder {
|
||||
mb.message.buttonLink = link
|
||||
return mb
|
||||
}
|
||||
|
||||
func (mb *MessageBuilderImp) AddRecipient(recipientName, recipient string) mmail.MailBuilder {
|
||||
mb.message.recipientName = recipientName
|
||||
mb.message.recipients = append(mb.message.recipients, recipient)
|
||||
return mb
|
||||
}
|
||||
|
||||
func (mb *MessageBuilderImp) AddData(key, value string) mmail.MailBuilder {
|
||||
mb.message.parameters[key] = value
|
||||
return mb
|
||||
}
|
||||
|
||||
func (mb *MessageBuilderImp) Build() (mmail.Message, error) {
|
||||
if len(mb.message.recipients) == 0 {
|
||||
return nil, merrors.InvalidArgument("Recipient not set")
|
||||
}
|
||||
return mb.message, nil
|
||||
}
|
||||
|
||||
func NewMessageBuilder() *MessageBuilderImp {
|
||||
return &MessageBuilderImp{
|
||||
message: createMessageImp(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
package mailimp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
)
|
||||
|
||||
func TestNewMessageBuilder_CreatesValidBuilder(t *testing.T) {
|
||||
builder := NewMessageBuilder()
|
||||
if builder == nil {
|
||||
t.Fatal("Expected non-nil builder")
|
||||
}
|
||||
if builder.message == nil {
|
||||
t.Fatal("Expected builder to have initialized message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_BuildWithoutRecipient_ReturnsError(t *testing.T) {
|
||||
builder := NewMessageBuilder()
|
||||
_, err := builder.Build()
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("Expected error when building without recipient")
|
||||
}
|
||||
|
||||
if !errors.Is(err, merrors.ErrInvalidArg) {
|
||||
t.Errorf("Expected InvalidArgument error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_BuildWithRecipient_Success(t *testing.T) {
|
||||
builder := NewMessageBuilder()
|
||||
msg, err := builder.
|
||||
AddRecipient("John Doe", "john@example.com").
|
||||
Build()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if msg == nil {
|
||||
t.Fatal("Expected non-nil message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_SetAccountID_SetsCorrectValue(t *testing.T) {
|
||||
builder := NewMessageBuilder()
|
||||
accountID := "507f1f77bcf86cd799439011"
|
||||
|
||||
msg, err := builder.
|
||||
SetAccountID(accountID).
|
||||
AddRecipient("Test User", "test@example.com").
|
||||
Build()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if msg.AccountID() != accountID {
|
||||
t.Errorf("Expected AccountID %s, got %s", accountID, msg.AccountID())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_SetTemplateID_SetsCorrectValue(t *testing.T) {
|
||||
builder := NewMessageBuilder()
|
||||
templateID := "welcome"
|
||||
|
||||
msg, err := builder.
|
||||
SetTemplateID(templateID).
|
||||
AddRecipient("Test User", "test@example.com").
|
||||
Build()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if msg.TemplateID() != templateID {
|
||||
t.Errorf("Expected TemplateID %s, got %s", templateID, msg.TemplateID())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_SetLocale_SetsCorrectValue(t *testing.T) {
|
||||
builder := NewMessageBuilder()
|
||||
locale := "en-US"
|
||||
|
||||
msg, err := builder.
|
||||
SetLocale(locale).
|
||||
AddRecipient("Test User", "test@example.com").
|
||||
Build()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if msg.Locale() != locale {
|
||||
t.Errorf("Expected Locale %s, got %s", locale, msg.Locale())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_AddRecipient_AddsToRecipientsList(t *testing.T) {
|
||||
builder := NewMessageBuilder()
|
||||
|
||||
msg, err := builder.
|
||||
AddRecipient("User One", "user1@example.com").
|
||||
AddRecipient("User Two", "user2@example.com").
|
||||
Build()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
recipients := msg.Recipients()
|
||||
if len(recipients) != 2 {
|
||||
t.Fatalf("Expected 2 recipients, got %d", len(recipients))
|
||||
}
|
||||
|
||||
if recipients[0] != "user1@example.com" {
|
||||
t.Errorf("Expected first recipient to be user1@example.com, got %s", recipients[0])
|
||||
}
|
||||
if recipients[1] != "user2@example.com" {
|
||||
t.Errorf("Expected second recipient to be user2@example.com, got %s", recipients[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_AddData_AccumulatesParameters(t *testing.T) {
|
||||
builder := NewMessageBuilder()
|
||||
|
||||
msg, err := builder.
|
||||
AddData("key1", "value1").
|
||||
AddData("key2", "value2").
|
||||
AddRecipient("Test User", "test@example.com").
|
||||
Build()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
params := msg.Parameters()
|
||||
if len(params) != 2 {
|
||||
t.Fatalf("Expected 2 parameters, got %d", len(params))
|
||||
}
|
||||
|
||||
if params["key1"] != "value1" {
|
||||
t.Errorf("Expected key1=value1, got %v", params["key1"])
|
||||
}
|
||||
if params["key2"] != "value2" {
|
||||
t.Errorf("Expected key2=value2, got %v", params["key2"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_AddButton_StoresButtonLink(t *testing.T) {
|
||||
builder := NewMessageBuilder()
|
||||
buttonLink := "https://example.com/verify"
|
||||
|
||||
msg, err := builder.
|
||||
AddButton(buttonLink).
|
||||
AddRecipient("Test User", "test@example.com").
|
||||
Build()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Button link is internal, but we can verify the message was built successfully
|
||||
if msg == nil {
|
||||
t.Fatal("Expected non-nil message with button")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_ChainedMethods_SetsAllFields(t *testing.T) {
|
||||
builder := NewMessageBuilder()
|
||||
|
||||
msg, err := builder.
|
||||
SetAccountID("507f1f77bcf86cd799439011").
|
||||
SetTemplateID("welcome").
|
||||
SetLocale("en-US").
|
||||
AddButton("https://example.com/verify").
|
||||
AddRecipient("John Doe", "john@example.com").
|
||||
AddData("name", "John").
|
||||
AddData("age", "30").
|
||||
Build()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if msg.AccountID() != "507f1f77bcf86cd799439011" {
|
||||
t.Errorf("AccountID not set correctly")
|
||||
}
|
||||
if msg.TemplateID() != "welcome" {
|
||||
t.Errorf("TemplateID not set correctly")
|
||||
}
|
||||
if msg.Locale() != "en-US" {
|
||||
t.Errorf("Locale not set correctly")
|
||||
}
|
||||
if len(msg.Recipients()) != 1 {
|
||||
t.Errorf("Recipients not set correctly")
|
||||
}
|
||||
if len(msg.Parameters()) != 2 {
|
||||
t.Errorf("Parameters not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_MultipleBuilds_IndependentMessages(t *testing.T) {
|
||||
builder1 := NewMessageBuilder()
|
||||
builder2 := NewMessageBuilder()
|
||||
|
||||
msg1, err1 := builder1.
|
||||
SetTemplateID("template1").
|
||||
AddRecipient("User 1", "user1@example.com").
|
||||
Build()
|
||||
|
||||
msg2, err2 := builder2.
|
||||
SetTemplateID("template2").
|
||||
AddRecipient("User 2", "user2@example.com").
|
||||
Build()
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
t.Fatalf("Unexpected errors: %v, %v", err1, err2)
|
||||
}
|
||||
|
||||
if msg1.TemplateID() == msg2.TemplateID() {
|
||||
t.Error("Messages should be independent with different template IDs")
|
||||
}
|
||||
|
||||
if msg1.Recipients()[0] == msg2.Recipients()[0] {
|
||||
t.Error("Messages should be independent with different recipients")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageBuilder_EmptyValues_AreAllowed(t *testing.T) {
|
||||
builder := NewMessageBuilder()
|
||||
|
||||
msg, err := builder.
|
||||
SetAccountID("").
|
||||
SetTemplateID("").
|
||||
SetLocale("").
|
||||
AddButton("").
|
||||
AddRecipient("", "user@example.com").
|
||||
Build()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Empty values should be allowed - business logic validation happens elsewhere
|
||||
if msg == nil {
|
||||
t.Fatal("Expected message to be built even with empty values")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package mailimp
|
||||
|
||||
import (
|
||||
"maps"
|
||||
|
||||
"github.com/tech/sendico/notification/interface/api/localizer"
|
||||
"github.com/tech/sendico/notification/internal/server/notificationimp/mail/internal/mailkey"
|
||||
mmail "github.com/tech/sendico/notification/internal/server/notificationimp/mail/messagebuilder"
|
||||
"github.com/tech/sendico/pkg/domainprovider"
|
||||
"github.com/tech/sendico/pkg/localization"
|
||||
)
|
||||
|
||||
type EmailNotificationTemplate struct {
|
||||
dp domainprovider.DomainProvider
|
||||
l localizer.Localizer
|
||||
data localization.LocData
|
||||
unsubscribable bool
|
||||
hasButton bool
|
||||
}
|
||||
|
||||
func (m *EmailNotificationTemplate) AddData(key, value string) {
|
||||
localization.AddLocData(m.data, key, value)
|
||||
}
|
||||
|
||||
// content:
|
||||
// Greeting: Welcome, Gregory
|
||||
// Content: You're receiving this message because you recently signed up for an account.<br><br>Confirm your email address by clicking the button below. This step adds extra security to your business by verifying you own this email.
|
||||
// LogoLink: link to a logo
|
||||
// Privacy: Privacy Policy
|
||||
// PolicyLink: link to a privacy policy
|
||||
// Unsubscribe: Unsubscribe
|
||||
// UnsubscribeLink: link to an unsubscribe command
|
||||
// MessageTitle: message title
|
||||
|
||||
func (m *EmailNotificationTemplate) prepareUnsubscribe(msg mmail.Message) error {
|
||||
var block string
|
||||
if m.unsubscribable {
|
||||
var d localization.LocData
|
||||
unsubscribe, err := m.l.LocalizeString("mail.template.unsubscribe", msg.Locale())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
localization.AddLocData(d, "Unsubscribe", unsubscribe)
|
||||
unsLink, err := m.dp.GetFullLink("account", "unsubscribe", msg.AccountID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
localization.AddLocData(d, "UnsubscribeLink", unsLink)
|
||||
if block, err = m.l.LocalizeTemplate("mail.template.unsubscribe.block", d, nil, msg.Locale()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
m.AddData("UnsubscribeBlock", block)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *EmailNotificationTemplate) prepareButton(msg mmail.Message) error {
|
||||
var block string
|
||||
if m.hasButton {
|
||||
var err error
|
||||
if block, err = m.l.LocalizeTemplate("mail.template.btn.block", m.data, nil, msg.Locale()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
m.AddData("ButtonBlock", block)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *EmailNotificationTemplate) SignatureData(msg mmail.Message, content, subj string) (string, error) {
|
||||
m.AddData("Content", content)
|
||||
m.AddData("MessageTitle", subj)
|
||||
logoLink, err := m.dp.GetAPILink("logo", msg.AccountID(), msg.TemplateID())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
m.AddData("LogoLink", logoLink)
|
||||
privacy, err := m.l.LocalizeString("mail.template.privacy", msg.Locale())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
m.AddData("Privacy", privacy)
|
||||
ppLink, err := m.dp.GetFullLink("/privacy-policy")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
m.AddData("PolicyLink", ppLink)
|
||||
if err := m.prepareButton(msg); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := m.prepareUnsubscribe(msg); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return m.l.LocalizeTemplate("mail.template.one_button", m.data, nil, msg.Locale())
|
||||
}
|
||||
|
||||
func (m *EmailNotificationTemplate) putOnHTMLTemplate(msg mmail.Message, content, subj string) (string, error) {
|
||||
greeting, err := m.l.LocalizeTemplate(mailkey.Get(msg.TemplateID(), "greeting"), m.data, nil, msg.Locale())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
m.AddData("Greeting", greeting)
|
||||
return m.SignatureData(msg, content, subj)
|
||||
}
|
||||
|
||||
func (m *EmailNotificationTemplate) Build(msg mmail.Message) (string, error) {
|
||||
if m.data != nil {
|
||||
m.data["ServiceName"] = m.l.ServiceName()
|
||||
m.data["SupportMail"] = m.l.SupportMail()
|
||||
var err error
|
||||
if m.data["ServiceOwner"], err = m.l.LocalizeString("service.owner", msg.Locale()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if m.data["OwnerAddress"], err = m.l.LocalizeString("service.address", msg.Locale()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if m.data["OwnerPhone"], err = m.l.LocalizeString("service.phone", msg.Locale()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
maps.Copy(m.data, msg.Parameters())
|
||||
}
|
||||
content, err := mailkey.Body(m.l, m.data, msg.TemplateID(), msg.Locale())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
subject, err := mailkey.Subject(m.l, m.data, msg.TemplateID(), msg.Locale())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return m.putOnHTMLTemplate(msg, content, subject)
|
||||
}
|
||||
|
||||
func (t *EmailNotificationTemplate) SetUnsubscribable(isUnsubscribable bool) {
|
||||
t.unsubscribable = isUnsubscribable
|
||||
}
|
||||
|
||||
func (t *EmailNotificationTemplate) SetButton(hasButton bool) {
|
||||
t.hasButton = hasButton
|
||||
}
|
||||
|
||||
func NewEmailNotification(l localizer.Localizer, dp domainprovider.DomainProvider) *EmailNotificationTemplate {
|
||||
p := &EmailNotificationTemplate{
|
||||
dp: dp,
|
||||
l: l,
|
||||
data: localization.LocData{},
|
||||
}
|
||||
p.unsubscribable = false
|
||||
p.hasButton = false
|
||||
return p
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package mailimp
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/notification/interface/api/localizer"
|
||||
"github.com/tech/sendico/pkg/domainprovider"
|
||||
)
|
||||
|
||||
type MessageImp struct {
|
||||
templateID string
|
||||
accountUID string
|
||||
locale string
|
||||
recipients []string
|
||||
recipientName string
|
||||
buttonLink string
|
||||
parameters map[string]any
|
||||
}
|
||||
|
||||
func (m *MessageImp) TemplateID() string {
|
||||
return m.templateID
|
||||
}
|
||||
|
||||
func (m *MessageImp) Locale() string {
|
||||
return m.locale
|
||||
}
|
||||
|
||||
func (m *MessageImp) AccountID() string {
|
||||
return m.accountUID
|
||||
}
|
||||
|
||||
func (m *MessageImp) Recipients() []string {
|
||||
return m.recipients
|
||||
}
|
||||
|
||||
func (m *MessageImp) Parameters() map[string]any {
|
||||
return m.parameters
|
||||
}
|
||||
|
||||
func (m *MessageImp) Body(l localizer.Localizer, dp domainprovider.DomainProvider) (string, error) {
|
||||
if len(m.buttonLink) == 0 {
|
||||
return NewEmailNotification(l, dp).Build(m)
|
||||
}
|
||||
page := NewOneButton(l, dp)
|
||||
buttonLabel, err := l.LocalizeString("btn."+m.TemplateID(), m.Locale())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
page.AddButton(buttonLabel, m.buttonLink)
|
||||
return page.Build(m)
|
||||
}
|
||||
|
||||
func createMessageImp() *MessageImp {
|
||||
return &MessageImp{
|
||||
parameters: map[string]any{},
|
||||
recipients: []string{},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
package mailimp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Mock implementations for testing
|
||||
|
||||
type mockLocalizer struct {
|
||||
localizeTemplateFunc func(id string, templateData, ctr any, lang string) (string, error)
|
||||
localizeStringFunc func(id, lang string) (string, error)
|
||||
serviceName string
|
||||
supportMail string
|
||||
}
|
||||
|
||||
func (m *mockLocalizer) LocalizeTemplate(id string, templateData, ctr any, lang string) (string, error) {
|
||||
if m.localizeTemplateFunc != nil {
|
||||
return m.localizeTemplateFunc(id, templateData, ctr, lang)
|
||||
}
|
||||
// Return a simple HTML template for testing
|
||||
return fmt.Sprintf("<html><body>Template: %s</body></html>", id), nil
|
||||
}
|
||||
|
||||
func (m *mockLocalizer) LocalizeString(id, lang string) (string, error) {
|
||||
if m.localizeStringFunc != nil {
|
||||
return m.localizeStringFunc(id, lang)
|
||||
}
|
||||
return fmt.Sprintf("string:%s", id), nil
|
||||
}
|
||||
|
||||
func (m *mockLocalizer) ServiceName() string {
|
||||
if m.serviceName != "" {
|
||||
return m.serviceName
|
||||
}
|
||||
return "TestService"
|
||||
}
|
||||
|
||||
func (m *mockLocalizer) SupportMail() string {
|
||||
if m.supportMail != "" {
|
||||
return m.supportMail
|
||||
}
|
||||
return "support@test.com"
|
||||
}
|
||||
|
||||
type mockDomainProvider struct {
|
||||
getFullLinkFunc func(linkElem ...string) (string, error)
|
||||
getAPILinkFunc func(linkElem ...string) (string, error)
|
||||
}
|
||||
|
||||
func (m *mockDomainProvider) GetFullLink(linkElem ...string) (string, error) {
|
||||
if m.getFullLinkFunc != nil {
|
||||
return m.getFullLinkFunc(linkElem...)
|
||||
}
|
||||
return "https://example.com/link", nil
|
||||
}
|
||||
|
||||
func (m *mockDomainProvider) GetAPILink(linkElem ...string) (string, error) {
|
||||
if m.getAPILinkFunc != nil {
|
||||
return m.getAPILinkFunc(linkElem...)
|
||||
}
|
||||
return "https://api.example.com/link", nil
|
||||
}
|
||||
|
||||
// Tests
|
||||
|
||||
func TestMessageImp_TemplateID_ReturnsCorrectValue(t *testing.T) {
|
||||
msg := createMessageImp()
|
||||
msg.templateID = "welcome"
|
||||
|
||||
if msg.TemplateID() != "welcome" {
|
||||
t.Errorf("Expected templateID 'welcome', got '%s'", msg.TemplateID())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageImp_Locale_ReturnsCorrectValue(t *testing.T) {
|
||||
msg := createMessageImp()
|
||||
msg.locale = "en-US"
|
||||
|
||||
if msg.Locale() != "en-US" {
|
||||
t.Errorf("Expected locale 'en-US', got '%s'", msg.Locale())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageImp_AccountID_ReturnsCorrectValue(t *testing.T) {
|
||||
msg := createMessageImp()
|
||||
msg.accountUID = "507f1f77bcf86cd799439011"
|
||||
|
||||
if msg.AccountID() != "507f1f77bcf86cd799439011" {
|
||||
t.Errorf("Expected accountUID '507f1f77bcf86cd799439011', got '%s'", msg.AccountID())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageImp_Recipients_ReturnsCorrectList(t *testing.T) {
|
||||
msg := createMessageImp()
|
||||
msg.recipients = []string{"user1@example.com", "user2@example.com"}
|
||||
|
||||
recipients := msg.Recipients()
|
||||
if len(recipients) != 2 {
|
||||
t.Fatalf("Expected 2 recipients, got %d", len(recipients))
|
||||
}
|
||||
if recipients[0] != "user1@example.com" {
|
||||
t.Errorf("Expected first recipient 'user1@example.com', got '%s'", recipients[0])
|
||||
}
|
||||
if recipients[1] != "user2@example.com" {
|
||||
t.Errorf("Expected second recipient 'user2@example.com', got '%s'", recipients[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageImp_Parameters_ReturnsCorrectMap(t *testing.T) {
|
||||
msg := createMessageImp()
|
||||
msg.parameters["key1"] = "value1"
|
||||
msg.parameters["key2"] = "value2"
|
||||
|
||||
params := msg.Parameters()
|
||||
if len(params) != 2 {
|
||||
t.Fatalf("Expected 2 parameters, got %d", len(params))
|
||||
}
|
||||
if params["key1"] != "value1" {
|
||||
t.Errorf("Expected key1='value1', got '%v'", params["key1"])
|
||||
}
|
||||
if params["key2"] != "value2" {
|
||||
t.Errorf("Expected key2='value2', got '%v'", params["key2"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageImp_Body_WithButton_CallsOneButtonTemplate(t *testing.T) {
|
||||
msg := createMessageImp()
|
||||
msg.templateID = "welcome"
|
||||
msg.locale = "en-US"
|
||||
msg.buttonLink = "https://example.com/verify"
|
||||
|
||||
mockLoc := &mockLocalizer{
|
||||
localizeStringFunc: func(id, lang string) (string, error) {
|
||||
// Mock all localization calls that might occur
|
||||
switch id {
|
||||
case "btn.welcome":
|
||||
return "Verify Account", nil
|
||||
case "service.owner", "service.name":
|
||||
return "Test Service", nil
|
||||
default:
|
||||
return fmt.Sprintf("localized:%s", id), nil
|
||||
}
|
||||
},
|
||||
}
|
||||
mockDP := &mockDomainProvider{}
|
||||
|
||||
body, err := msg.Body(mockLoc, mockDP)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if body == "" {
|
||||
t.Error("Expected non-empty body")
|
||||
}
|
||||
// Body should be HTML from one-button template
|
||||
// We can't test exact content without knowing template implementation,
|
||||
// but we can verify it succeeded
|
||||
}
|
||||
|
||||
func TestMessageImp_Body_WithoutButton_CallsEmailNotification(t *testing.T) {
|
||||
msg := createMessageImp()
|
||||
msg.templateID = "notification"
|
||||
msg.locale = "en-US"
|
||||
msg.buttonLink = "" // No button
|
||||
|
||||
mockLoc := &mockLocalizer{}
|
||||
mockDP := &mockDomainProvider{}
|
||||
|
||||
body, err := msg.Body(mockLoc, mockDP)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if body == "" {
|
||||
t.Error("Expected non-empty body")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageImp_Body_LocalizationError_ReturnsError(t *testing.T) {
|
||||
msg := createMessageImp()
|
||||
msg.templateID = "welcome"
|
||||
msg.locale = "invalid-locale"
|
||||
msg.buttonLink = "https://example.com/verify"
|
||||
|
||||
mockLoc := &mockLocalizer{
|
||||
localizeStringFunc: func(id, lang string) (string, error) {
|
||||
return "", fmt.Errorf("localization failed for lang: %s", lang)
|
||||
},
|
||||
}
|
||||
mockDP := &mockDomainProvider{}
|
||||
|
||||
_, err := msg.Body(mockLoc, mockDP)
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected error from localization failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateMessageImp_InitializesEmptyCollections(t *testing.T) {
|
||||
msg := createMessageImp()
|
||||
|
||||
if msg.parameters == nil {
|
||||
t.Error("Expected parameters map to be initialized")
|
||||
}
|
||||
if msg.recipients == nil {
|
||||
t.Error("Expected recipients slice to be initialized")
|
||||
}
|
||||
if len(msg.parameters) != 0 {
|
||||
t.Error("Expected parameters map to be empty")
|
||||
}
|
||||
if len(msg.recipients) != 0 {
|
||||
t.Error("Expected recipients slice to be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageImp_MultipleParameterTypes_StoresCorrectly(t *testing.T) {
|
||||
msg := createMessageImp()
|
||||
msg.parameters["string"] = "value"
|
||||
msg.parameters["number"] = 42
|
||||
msg.parameters["bool"] = true
|
||||
|
||||
params := msg.Parameters()
|
||||
|
||||
if params["string"] != "value" {
|
||||
t.Error("String parameter not stored correctly")
|
||||
}
|
||||
if params["number"] != 42 {
|
||||
t.Error("Number parameter not stored correctly")
|
||||
}
|
||||
if params["bool"] != true {
|
||||
t.Error("Boolean parameter not stored correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageImp_EmptyTemplateID_AllowedByGetter(t *testing.T) {
|
||||
msg := createMessageImp()
|
||||
msg.templateID = ""
|
||||
|
||||
// Should not panic or error
|
||||
result := msg.TemplateID()
|
||||
if result != "" {
|
||||
t.Errorf("Expected empty string, got '%s'", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageImp_EmptyLocale_AllowedByGetter(t *testing.T) {
|
||||
msg := createMessageImp()
|
||||
msg.locale = ""
|
||||
|
||||
// Should not panic or error
|
||||
result := msg.Locale()
|
||||
if result != "" {
|
||||
t.Errorf("Expected empty string, got '%s'", result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package mailimp
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/notification/interface/api/localizer"
|
||||
"github.com/tech/sendico/pkg/domainprovider"
|
||||
)
|
||||
|
||||
type OneButtonTemplate struct {
|
||||
EmailNotificationTemplate
|
||||
}
|
||||
|
||||
func (b *OneButtonTemplate) AddButtonText(text string) {
|
||||
b.AddData("ButtonText", text)
|
||||
}
|
||||
|
||||
func (b *OneButtonTemplate) AddButtonLink(link string) {
|
||||
b.AddData("ButtonLink", link)
|
||||
}
|
||||
|
||||
func (b *OneButtonTemplate) AddButton(text, link string) {
|
||||
b.AddButtonText(text)
|
||||
b.AddButtonLink(link)
|
||||
}
|
||||
|
||||
func NewOneButton(l localizer.Localizer, dp domainprovider.DomainProvider) *OneButtonTemplate {
|
||||
p := &OneButtonTemplate{
|
||||
EmailNotificationTemplate: *NewEmailNotification(l, dp),
|
||||
}
|
||||
p.SetUnsubscribable(false)
|
||||
p.SetButton(true)
|
||||
|
||||
return p
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package mailimp
|
||||
|
||||
import (
|
||||
mb "github.com/tech/sendico/notification/internal/server/notificationimp/mail/internal/builder"
|
||||
b "github.com/tech/sendico/notification/internal/server/notificationimp/mail/messagebuilder"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
)
|
||||
|
||||
type Dummy struct {
|
||||
logger mlogger.Logger
|
||||
}
|
||||
|
||||
func (d *Dummy) Send(_ b.MailBuilder) error {
|
||||
d.logger.Warn("Unexpected request to send email")
|
||||
return merrors.NotImplemented("MailDummy::Send")
|
||||
}
|
||||
|
||||
func (d *Dummy) MailBuilder() b.MailBuilder {
|
||||
return mb.NewMessageBuilder()
|
||||
}
|
||||
|
||||
func NewDummy(logger mlogger.Logger) (*Dummy, error) {
|
||||
d := &Dummy{
|
||||
logger: logger.Named("dummy"),
|
||||
}
|
||||
d.logger.Info("Mailer installed")
|
||||
return d, nil
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package mailimp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
mb "github.com/tech/sendico/notification/internal/server/notificationimp/mail/internal/builder"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger/factory"
|
||||
)
|
||||
|
||||
func TestNewDummy_CreatesValidClient(t *testing.T) {
|
||||
logger := mlogger.NewLogger(true)
|
||||
dummy, err := NewDummy(logger)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating dummy client: %v", err)
|
||||
}
|
||||
if dummy == nil {
|
||||
t.Fatal("Expected non-nil dummy client")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDummy_Send_ReturnsNotImplementedError(t *testing.T) {
|
||||
logger := mlogger.NewLogger(true)
|
||||
dummy, err := NewDummy(logger)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create dummy client: %v", err)
|
||||
}
|
||||
|
||||
builder := mb.NewMessageBuilder()
|
||||
err = dummy.Send(builder)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("Expected error when calling Send on dummy client")
|
||||
}
|
||||
|
||||
if !errors.Is(err, merrors.ErrNotImplemented) {
|
||||
t.Errorf("Expected NotImplemented error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDummy_MailBuilder_ReturnsValidBuilder(t *testing.T) {
|
||||
logger := mlogger.NewLogger(true)
|
||||
dummy, err := NewDummy(logger)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create dummy client: %v", err)
|
||||
}
|
||||
|
||||
builder := dummy.MailBuilder()
|
||||
|
||||
if builder == nil {
|
||||
t.Fatal("Expected non-nil mail builder")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDummy_MailBuilder_CanBuildMessage(t *testing.T) {
|
||||
logger := mlogger.NewLogger(true)
|
||||
dummy, err := NewDummy(logger)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create dummy client: %v", err)
|
||||
}
|
||||
|
||||
builder := dummy.MailBuilder()
|
||||
msg, err := builder.
|
||||
AddRecipient("Test User", "test@example.com").
|
||||
SetTemplateID("welcome").
|
||||
Build()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error building message: %v", err)
|
||||
}
|
||||
if msg == nil {
|
||||
t.Fatal("Expected non-nil message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDummy_MultipleSendCalls_AllReturnError(t *testing.T) {
|
||||
logger := mlogger.NewLogger(true)
|
||||
dummy, err := NewDummy(logger)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create dummy client: %v", err)
|
||||
}
|
||||
|
||||
builder1 := dummy.MailBuilder()
|
||||
builder2 := dummy.MailBuilder()
|
||||
|
||||
err1 := dummy.Send(builder1)
|
||||
err2 := dummy.Send(builder2)
|
||||
|
||||
if err1 == nil || err2 == nil {
|
||||
t.Error("Expected all Send calls to return errors")
|
||||
}
|
||||
|
||||
if !errors.Is(err1, merrors.ErrNotImplemented) || !errors.Is(err2, merrors.ErrNotImplemented) {
|
||||
t.Error("Expected all errors to be NotImplemented")
|
||||
}
|
||||
}
|
||||
174
api/notification/internal/server/notificationimp/mail/internal/mailimp.go
Executable file
174
api/notification/internal/server/notificationimp/mail/internal/mailimp.go
Executable file
@@ -0,0 +1,174 @@
|
||||
package mailimp
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/notification/interface/api/localizer"
|
||||
mb "github.com/tech/sendico/notification/internal/server/notificationimp/mail/internal/builder"
|
||||
"github.com/tech/sendico/notification/internal/server/notificationimp/mail/internal/mailkey"
|
||||
mmail "github.com/tech/sendico/notification/internal/server/notificationimp/mail/messagebuilder"
|
||||
"github.com/tech/sendico/pkg/domainprovider"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
mutil "github.com/tech/sendico/pkg/mutil/config"
|
||||
mduration "github.com/tech/sendico/pkg/mutil/duration"
|
||||
mail "github.com/xhit/go-simple-mail/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Client implements a mail client
|
||||
type Client struct {
|
||||
logger mlogger.Logger
|
||||
server *mail.SMTPServer
|
||||
client *mail.SMTPClient
|
||||
from string
|
||||
l localizer.Localizer
|
||||
dp domainprovider.DomainProvider
|
||||
}
|
||||
|
||||
// Config represents the mail configuration
|
||||
type GSMConfig struct {
|
||||
Username *string `mapstructure:"username,omitempty" yaml:"username,omitempty"`
|
||||
UsernameEnv *string `mapstructure:"username_env,omitempty" yaml:"username_env,omitempty"`
|
||||
Password *string `mapstructure:"password" yaml:"password"`
|
||||
PasswordEnv *string `mapstructure:"password_env" yaml:"password_env"`
|
||||
Host string `mapstructure:"host" yaml:"host"`
|
||||
Port int `mapstructure:"port" yaml:"port"`
|
||||
From string `mapstructure:"from" yaml:"from"`
|
||||
TimeOut int `mapstructure:"network_timeout" yaml:"network_timeout"`
|
||||
}
|
||||
|
||||
func (c *Client) sendImp(m mmail.Message, msg *mail.Email) error {
|
||||
err := msg.Send(c.client)
|
||||
if err != nil {
|
||||
c.logger.Warn("Error sending email", zap.Error(err), zap.String("template_id", m.TemplateID()), zap.Strings("recipients", msg.GetRecipients()))
|
||||
} else {
|
||||
c.logger.Info("Email sent", zap.Strings("recipients", msg.GetRecipients()), zap.String("template_id", m.TemplateID()))
|
||||
}
|
||||
// TODO: add amplitude notification
|
||||
return err
|
||||
}
|
||||
|
||||
// Send sends an email message to the provided address and with the provided subject
|
||||
func (c *Client) Send(r mmail.MailBuilder) error {
|
||||
// New email simple html with inline and CC
|
||||
|
||||
r.AddData("ServiceName", c.l.ServiceName()).AddData("SupportMail", c.l.SupportMail())
|
||||
m, err := r.Build()
|
||||
if err != nil {
|
||||
c.logger.Warn("Failed to build message", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
body, err := m.Body(c.l, c.dp)
|
||||
if err != nil {
|
||||
c.logger.Warn("Failed to build message body", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
if (len(body) == 0) || (len(m.Recipients()) == 0) {
|
||||
c.logger.Warn("Malformed messge", zap.String("template_id", m.TemplateID()),
|
||||
zap.String("locale", m.Locale()), zap.Strings("recipients", m.Recipients()),
|
||||
zap.Int("body_size", len(body)))
|
||||
return merrors.InvalidArgument("malformed message")
|
||||
}
|
||||
subj, err := mailkey.Subject(c.l, m.Parameters(), m.TemplateID(), m.Locale())
|
||||
if err != nil {
|
||||
c.logger.Warn("Failed to localize subject", zap.Error(err), zap.String("template_id", m.TemplateID()),
|
||||
zap.String("locale", m.Locale()), zap.Strings("recipients", m.Recipients()),
|
||||
zap.Int("body_size", len(body)))
|
||||
return err
|
||||
}
|
||||
msg := mail.NewMSG()
|
||||
msg.SetFrom(c.from).
|
||||
AddTo(m.Recipients()...).
|
||||
SetSubject(subj).
|
||||
SetBody(mail.TextHTML, body)
|
||||
|
||||
// Call Send and pass the client
|
||||
if err = c.sendImp(m, msg); err != nil {
|
||||
c.logger.Info("Failed to send an email, attempting to reconnect...",
|
||||
zap.Error(err),
|
||||
zap.String("template", m.TemplateID()), zap.Strings("addresses", m.Recipients()), zap.String("locale", m.Locale()))
|
||||
|
||||
c.client = nil
|
||||
c.client, err = c.server.Connect()
|
||||
if err != nil {
|
||||
c.logger.Warn("Failed to reconnect mail client",
|
||||
zap.Error(err),
|
||||
zap.String("template", m.TemplateID()), zap.Strings("addresses", m.Recipients()), zap.String("locale", m.Locale()))
|
||||
return err
|
||||
}
|
||||
c.logger.Info("Connection has been successfully restored",
|
||||
zap.Error(err),
|
||||
zap.String("template", m.TemplateID()), zap.Strings("addresses", m.Recipients()), zap.String("locale", m.Locale()))
|
||||
|
||||
err = c.sendImp(m, msg)
|
||||
if err != nil {
|
||||
c.logger.Warn("Failed to send message after mail client recreation",
|
||||
zap.Error(err),
|
||||
zap.String("template", m.TemplateID()), zap.Strings("addresses", m.Recipients()), zap.String("locale", m.Locale()))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) MailBuilder() mmail.MailBuilder {
|
||||
return mb.NewMessageBuilder()
|
||||
}
|
||||
|
||||
// NewClient return a new mail
|
||||
func NewClient(logger mlogger.Logger, l localizer.Localizer, dp domainprovider.DomainProvider, config *GSMConfig) *Client {
|
||||
smtpServer := mail.NewSMTPClient()
|
||||
|
||||
// SMTP Server
|
||||
smtpServer.Host = config.Host
|
||||
if config.Port < 1 {
|
||||
logger.Warn("Invalid mail client port configuration, defaulting to 465", zap.Int("port", config.Port))
|
||||
config.Port = 465
|
||||
}
|
||||
smtpServer.Port = config.Port
|
||||
smtpServer.Username = mutil.GetConfigValue(logger, "username", "username_env", config.Username, config.UsernameEnv)
|
||||
smtpServer.Password = mutil.GetConfigValue(logger, "password", "password_env", config.Password, config.PasswordEnv)
|
||||
smtpServer.Encryption = mail.EncryptionSSL
|
||||
|
||||
// Since v2.3.0 you can specified authentication type:
|
||||
// - PLAIN (default)
|
||||
// - LOGIN
|
||||
// - CRAM-MD5
|
||||
// server.Authentication = mail.AuthPlain
|
||||
|
||||
// Variable to keep alive connection
|
||||
smtpServer.KeepAlive = true
|
||||
|
||||
// Timeout for connect to SMTP Server
|
||||
smtpServer.ConnectTimeout = mduration.Param2Duration(config.TimeOut, time.Second)
|
||||
|
||||
// Timeout for send the data and wait respond
|
||||
smtpServer.SendTimeout = mduration.Param2Duration(config.TimeOut, time.Second)
|
||||
|
||||
// Set TLSConfig to provide custom TLS configuration. For example,
|
||||
// to skip TLS verification (useful for testing):
|
||||
smtpServer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
|
||||
// SMTP client
|
||||
lg := logger.Named("client")
|
||||
smtpClient, err := smtpServer.Connect()
|
||||
if err != nil {
|
||||
lg.Warn("Failed to connect", zap.Error(err))
|
||||
} else {
|
||||
lg.Info("Connected successfully", zap.String("username", smtpServer.Username), zap.String("host", config.Host))
|
||||
}
|
||||
|
||||
from := config.From + " <" + smtpServer.Username + ">"
|
||||
|
||||
return &Client{
|
||||
logger: lg,
|
||||
server: smtpServer,
|
||||
client: smtpClient,
|
||||
from: from,
|
||||
l: l,
|
||||
dp: dp,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package mailkey
|
||||
|
||||
import "github.com/tech/sendico/notification/interface/api/localizer"
|
||||
|
||||
func Get(template, part string) string {
|
||||
return "mail." + template + "." + part
|
||||
}
|
||||
|
||||
func Subject(l localizer.Localizer, data map[string]any, templateID, locale string) (string, error) {
|
||||
return l.LocalizeTemplate(Get(templateID, "subj"), data, nil, locale)
|
||||
}
|
||||
|
||||
func Body(l localizer.Localizer, data map[string]any, templateID, locale string) (string, error) {
|
||||
return l.LocalizeTemplate(Get(templateID, "body"), data, nil, locale)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package mailimp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
mb "github.com/tech/sendico/notification/internal/server/notificationimp/mail/internal/builder"
|
||||
mmail "github.com/tech/sendico/notification/internal/server/notificationimp/mail/messagebuilder"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/messaging"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/sendgrid/sendgrid-go"
|
||||
"github.com/sendgrid/sendgrid-go/helpers/mail"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type KeysConfig struct {
|
||||
Email string `yaml:"email"`
|
||||
Name string `yaml:"name"`
|
||||
URL string `yaml:"url"`
|
||||
ID string `yaml:"id"`
|
||||
}
|
||||
|
||||
type Sender struct {
|
||||
Address string `yaml:"address"`
|
||||
Name string `yaml:"name"`
|
||||
}
|
||||
|
||||
type SGEmailConfig struct {
|
||||
Sender Sender `yaml:"sender"`
|
||||
}
|
||||
|
||||
type SendGridConfig struct {
|
||||
APIKeyEnv string `yaml:"api_key_env"`
|
||||
Email SGEmailConfig `yaml:"email"`
|
||||
Keys KeysConfig `yaml:"keys"`
|
||||
}
|
||||
|
||||
type SendGridNotifier struct {
|
||||
logger mlogger.Logger
|
||||
client *sendgrid.Client
|
||||
config *SendGridConfig
|
||||
producer messaging.Producer
|
||||
}
|
||||
|
||||
func (sg *SendGridNotifier) Send(mb mmail.MailBuilder) error {
|
||||
m := mail.NewV3Mail()
|
||||
|
||||
e := mail.NewEmail(sg.config.Email.Sender.Name, sg.config.Email.Sender.Address)
|
||||
m.SetFrom(e)
|
||||
|
||||
task, err := mb.Build()
|
||||
if err != nil {
|
||||
sg.logger.Warn("Failed to build message", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
m.SetTemplateID(task.TemplateID())
|
||||
|
||||
p := mail.NewPersonalization()
|
||||
for _, recipient := range task.Recipients() {
|
||||
p.AddTos(mail.NewEmail(recipient, recipient))
|
||||
}
|
||||
|
||||
for k, v := range task.Parameters() {
|
||||
p.SetDynamicTemplateData(k, v)
|
||||
}
|
||||
|
||||
m.AddPersonalizations(p)
|
||||
|
||||
response, err := sg.client.Send(m)
|
||||
if err != nil {
|
||||
sg.logger.Warn("Failed to send email", zap.Error(err), zap.Any("task", &task))
|
||||
return err
|
||||
}
|
||||
if (response.StatusCode != http.StatusOK) && (response.StatusCode != http.StatusAccepted) {
|
||||
sg.logger.Warn("Unexpected SendGrid sresponse", zap.Int("status_code", response.StatusCode),
|
||||
zap.String("sresponse", response.Body), zap.Any("task", &task))
|
||||
return merrors.Internal("email_notification_not_sent")
|
||||
}
|
||||
|
||||
sg.logger.Info("Email sent successfully", zap.Strings("recipients", task.Recipients()), zap.String("template_id", task.TemplateID()))
|
||||
// if err = sg.producer.SendMessage(model.NewNotification(model.NTEmail, model.NAComplete), &task); err != nil {
|
||||
// sg.logger.Warn("Failed to send email statistics", zap.Error(err), zap.Strings("recipients", task.Recipients), zap.String("template_id", task.TemplateID))
|
||||
// }
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sg *SendGridNotifier) MailBuilder() mmail.MailBuilder {
|
||||
return mb.NewMessageBuilder()
|
||||
}
|
||||
|
||||
func NewSendGridNotifier(logger mlogger.Logger, producer messaging.Producer, config *SendGridConfig) (*SendGridNotifier, error) {
|
||||
apiKey := os.Getenv(config.APIKeyEnv)
|
||||
if apiKey == "" {
|
||||
logger.Warn("No SendGrid API key")
|
||||
return nil, merrors.NoData("No SendGrid API key")
|
||||
}
|
||||
return &SendGridNotifier{
|
||||
logger: logger.Named("sendgrid"),
|
||||
client: sendgrid.NewSendClient(apiKey),
|
||||
config: config,
|
||||
producer: producer,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/notification/interface/api/localizer"
|
||||
notification "github.com/tech/sendico/notification/interface/services/notification/config"
|
||||
mi "github.com/tech/sendico/notification/internal/server/notificationimp/mail/internal"
|
||||
mb "github.com/tech/sendico/notification/internal/server/notificationimp/mail/messagebuilder"
|
||||
"github.com/tech/sendico/pkg/domainprovider"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/messaging"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Client interface {
|
||||
Send(r mb.MailBuilder) error
|
||||
MailBuilder() mb.MailBuilder
|
||||
}
|
||||
|
||||
type Config = notification.Config
|
||||
|
||||
func createMailClient(logger mlogger.Logger, producer messaging.Producer, l localizer.Localizer, dp domainprovider.DomainProvider, config *Config) (Client, error) {
|
||||
if len(config.Driver) == 0 {
|
||||
return nil, merrors.InvalidArgument("Mail driver name must be provided")
|
||||
}
|
||||
logger.Info("Connecting mail client...", zap.String("driver", config.Driver))
|
||||
if config.Driver == "dummy" {
|
||||
return mi.NewDummy(logger)
|
||||
}
|
||||
if config.Driver == "sendgrid" {
|
||||
var sgconfig mi.SendGridConfig
|
||||
if err := mapstructure.Decode(config.Settings, &sgconfig); err != nil {
|
||||
logger.Error("Failed to decode driver settings", zap.Error(err), zap.String("driver", config.Driver))
|
||||
return nil, err
|
||||
}
|
||||
return mi.NewSendGridNotifier(logger, producer, &sgconfig)
|
||||
}
|
||||
if config.Driver == "client" {
|
||||
var gsmconfing mi.GSMConfig
|
||||
if err := mapstructure.Decode(config.Settings, &gsmconfing); err != nil {
|
||||
logger.Error("Failed to decode driver settings", zap.Error(err), zap.String("driver", config.Driver))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mi.NewClient(logger, l, dp, &gsmconfing), nil
|
||||
}
|
||||
return nil, merrors.InvalidArgument("Unkwnown mail driver: " + config.Driver)
|
||||
}
|
||||
|
||||
func CreateMailClient(logger mlogger.Logger, sender string, producer messaging.Producer, l localizer.Localizer, dp domainprovider.DomainProvider, config *Config) (Client, error) {
|
||||
return NewAmpliMailer(logger, sender, producer, l, dp, config)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package mmail
|
||||
|
||||
type MailBuilder interface {
|
||||
SetAccountID(accountID string) MailBuilder
|
||||
SetTemplateID(templateID string) MailBuilder
|
||||
SetLocale(locale string) MailBuilder
|
||||
AddRecipient(recipientName, recipient string) MailBuilder
|
||||
AddButton(link string) MailBuilder
|
||||
AddData(key, value string) MailBuilder
|
||||
Build() (Message, error)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package mmail
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
mgt "github.com/tech/sendico/pkg/mutil/time/go"
|
||||
)
|
||||
|
||||
func AddDate(b MailBuilder, t time.Time) {
|
||||
b.AddData("Date", mgt.ToDate(t))
|
||||
}
|
||||
|
||||
func AddTime(b MailBuilder, t time.Time) {
|
||||
b.AddData("Time", mgt.ToTime(t))
|
||||
}
|
||||
|
||||
func AddDateAndTime(b MailBuilder, t time.Time) {
|
||||
AddDate(b, t)
|
||||
AddTime(b, t)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package mmail
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/notification/interface/api/localizer"
|
||||
"github.com/tech/sendico/pkg/domainprovider"
|
||||
)
|
||||
|
||||
type Message interface {
|
||||
AccountID() string
|
||||
TemplateID() string
|
||||
Locale() string
|
||||
Recipients() []string
|
||||
Parameters() map[string]any
|
||||
Body(l localizer.Localizer, dp domainprovider.DomainProvider) (string, error)
|
||||
}
|
||||
Reference in New Issue
Block a user