Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ require (
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.9.0
github.com/tidwall/gjson v1.18.0
github.com/wneessen/go-mail v0.7.1
github.com/wneessen/go-mail v0.7.2
go.opencensus.io v0.24.0
golang.org/x/crypto v0.36.0
golang.org/x/sync v0.17.0
Expand Down Expand Up @@ -99,7 +99,7 @@ require (
golang.org/x/net v0.38.0 // indirect
golang.org/x/oauth2 v0.27.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/api v0.171.0 // indirect
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1161,8 +1161,8 @@ github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
github.com/wneessen/go-mail v0.7.1 h1:rvy63sp14N06/kdGqCYwW8Na5gDCXjTQM1E7So4PuKk=
github.com/wneessen/go-mail v0.7.1/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
Expand Down Expand Up @@ -1622,8 +1622,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
Expand Down
2 changes: 1 addition & 1 deletion internal/domain/email_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ type SendEmailProviderRequest struct {
IntegrationID string `validate:"required"`
MessageID string `validate:"required"`
FromAddress string `validate:"required"`
FromName string `validate:"required"`
FromName string // Optional - will use sender's name if empty
To string `validate:"required"`
Subject string `validate:"required"`
Content string `validate:"required"`
Expand Down
47 changes: 47 additions & 0 deletions internal/domain/email_provider_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package domain

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -1240,6 +1241,52 @@ func TestEmailProvider_GetSender_Behavior(t *testing.T) {
assert.Nil(t, empty.GetSender(""))
}

func TestEmailProvider_GetSender_EmptyStringReturnsDefault(t *testing.T) {
// Test that empty string correctly returns the default sender
sender := NewEmailSender("default@test.com", "Default Name")
ep := EmailProvider{
Senders: []EmailSender{sender},
}

// Verify sender has IsDefault set to true by NewEmailSender
assert.True(t, sender.IsDefault, "NewEmailSender should set IsDefault to true")

// Test getting sender with empty string (should fallback to default)
result := ep.GetSender("")
assert.NotNil(t, result, "GetSender with empty string should return default sender")
assert.Equal(t, sender.ID, result.ID, "Should return the default sender ID")
assert.Equal(t, "Default Name", result.Name, "Should return the default sender name")
assert.Equal(t, "default@test.com", result.Email, "Should return the default sender email")
}

func TestEmailProvider_GetSender_AfterJSONSerialization(t *testing.T) {
// Test that IsDefault flag survives JSON serialization/deserialization
sender := NewEmailSender("test@example.com", "Test Sender")
original := EmailProvider{
Kind: EmailProviderKindSMTP,
Senders: []EmailSender{sender},
RateLimitPerMinute: 10,
}

// Serialize to JSON
jsonData, err := json.Marshal(original)
require.NoError(t, err, "Should marshal EmailProvider to JSON")

// Deserialize from JSON
var restored EmailProvider
err = json.Unmarshal(jsonData, &restored)
require.NoError(t, err, "Should unmarshal EmailProvider from JSON")

// Verify IsDefault flag is preserved
assert.Equal(t, 1, len(restored.Senders), "Should have one sender")
assert.True(t, restored.Senders[0].IsDefault, "IsDefault flag should be preserved after JSON round-trip")

// Verify GetSender with empty string works after deserialization
result := restored.GetSender("")
assert.NotNil(t, result, "GetSender with empty string should work after JSON deserialization")
assert.Equal(t, "Test Sender", result.Name, "Should return correct sender name")
}

func TestSendEmailRequest_Validate_Cases(t *testing.T) {
validContact := &Contact{Email: "user@example.com"}
validProvider := &EmailProvider{Kind: EmailProviderKindSMTP, SMTP: &SMTPSettings{Host: "smtp.example.com", Port: 25, Username: "u"}}
Expand Down
100 changes: 100 additions & 0 deletions internal/domain/workspace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3514,3 +3514,103 @@ func TestWorkspace_Validate_WithCustomFieldLabels(t *testing.T) {
})
}
}

func TestIntegration_JSONSerialization_PreservesIsDefault(t *testing.T) {
// Test that IsDefault flag on senders is preserved through JSON serialization
// This simulates what happens when Integration is stored to and loaded from database
sender := NewEmailSender("test@example.com", "Test Sender")

integration := Integration{
ID: "int-123",
Name: "Test Integration",
Type: IntegrationTypeEmail,
EmailProvider: EmailProvider{
Kind: EmailProviderKindSMTP,
Senders: []EmailSender{sender},
SMTP: &SMTPSettings{
Host: "localhost",
Port: 1025,
},
RateLimitPerMinute: 10,
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}

// Verify sender has IsDefault before serialization
assert.True(t, integration.EmailProvider.Senders[0].IsDefault,
"Sender should have IsDefault=true before serialization")

// Serialize to JSON (simulating database storage)
jsonData, err := json.Marshal(integration)
require.NoError(t, err, "Should marshal Integration to JSON")

t.Logf("Serialized JSON: %s", string(jsonData))

// Deserialize from JSON (simulating database retrieval)
var restored Integration
err = json.Unmarshal(jsonData, &restored)
require.NoError(t, err, "Should unmarshal Integration from JSON")

// Verify IsDefault flag is preserved
require.Equal(t, 1, len(restored.EmailProvider.Senders), "Should have one sender")
assert.True(t, restored.EmailProvider.Senders[0].IsDefault,
"IsDefault flag should be preserved after JSON round-trip")
assert.Equal(t, "Test Sender", restored.EmailProvider.Senders[0].Name,
"Sender name should be preserved")

// Verify GetSender with empty string works
result := restored.EmailProvider.GetSender("")
assert.NotNil(t, result, "GetSender with empty string should return default sender")
assert.Equal(t, "Test Sender", result.Name, "Should return correct sender name")
}

func TestWorkspace_GetEmailProvider_PreservesIsDefault(t *testing.T) {
// Test the full workspace flow to ensure IsDefault is preserved
sender := NewEmailSender("default@test.com", "Default Sender")

integration := Integration{
ID: "int-456",
Name: "Email Integration",
Type: IntegrationTypeEmail,
EmailProvider: EmailProvider{
Kind: EmailProviderKindSMTP,
Senders: []EmailSender{sender},
SMTP: &SMTPSettings{
Host: "localhost",
Port: 1025,
},
RateLimitPerMinute: 10,
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}

workspace := Workspace{
ID: "ws-789",
Name: "Test Workspace",
Settings: WorkspaceSettings{
TransactionalEmailProviderID: "int-456",
EncryptedSecretKey: "dummy",
},
Integrations: []Integration{integration},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}

// Get the email provider
emailProvider, integrationID, err := workspace.GetEmailProviderWithIntegrationID(false)
require.NoError(t, err)
require.NotNil(t, emailProvider)
assert.Equal(t, "int-456", integrationID)

// Verify sender still has IsDefault
require.Equal(t, 1, len(emailProvider.Senders))
assert.True(t, emailProvider.Senders[0].IsDefault,
"IsDefault should be preserved when getting email provider from workspace")

// Verify GetSender with empty string works
result := emailProvider.GetSender("")
assert.NotNil(t, result, "GetSender with empty string should return default sender")
assert.Equal(t, "Default Sender", result.Name, "Should return correct sender name")
}
49 changes: 49 additions & 0 deletions internal/service/email_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,12 +259,38 @@ func (s *EmailService) SendEmailForTemplate(ctx context.Context, request domain.
}

// Find the emailSender
s.logger.WithFields(map[string]interface{}{
"template_sender_id": template.Email.SenderID,
"provider_senders_count": len(request.EmailProvider.Senders),
}).Info("Looking up email sender")

// Log all senders for debugging
for i, sender := range request.EmailProvider.Senders {
s.logger.WithFields(map[string]interface{}{
"sender_index": i,
"sender_id": sender.ID,
"sender_email": sender.Email,
"sender_name": sender.Name,
"is_default": sender.IsDefault,
}).Info("Available sender")
}

emailSender := request.EmailProvider.GetSender(template.Email.SenderID)

if emailSender == nil {
s.logger.WithFields(map[string]interface{}{
"template_sender_id": template.Email.SenderID,
"provider_kind": request.EmailProvider.Kind,
}).Error("Sender not found")
return fmt.Errorf("sender not found: %s", template.Email.SenderID)
}

s.logger.WithFields(map[string]interface{}{
"selected_sender_id": emailSender.ID,
"selected_sender_email": emailSender.Email,
"selected_sender_name": emailSender.Name,
}).Info("Selected email sender")

span.AddAttributes(
trace.StringAttribute("template.subject", template.Email.Subject),
trace.StringAttribute("template.from_email", emailSender.Email),
Expand Down Expand Up @@ -337,6 +363,29 @@ func (s *EmailService) SendEmailForTemplate(ctx context.Context, request domain.
fromEmail := emailSender.Email
fromName := emailSender.Name

s.logger.WithFields(map[string]interface{}{
"from_email": fromEmail,
"from_name_initial": fromName,
"has_channel_settings": request.TemplateConfig.Settings != nil,
}).Info("Email sender details before override")

// Override from_name if provided in channel settings
if request.TemplateConfig.Settings != nil {
if fromNameSetting, ok := request.TemplateConfig.Settings["from_name"]; ok {
if fromNameStr, ok := fromNameSetting.(string); ok && fromNameStr != "" {
s.logger.WithFields(map[string]interface{}{
"from_name_override": fromNameStr,
}).Info("Applying from_name override from channel settings")
fromName = fromNameStr
}
}
}

s.logger.WithFields(map[string]interface{}{
"from_email_final": fromEmail,
"from_name_final": fromName,
}).Info("Final email sender details")

// Process subject line through Liquid templating if it contains Liquid tags
subject, err := notifuse_mjml.ProcessLiquidTemplate(
template.Email.Subject,
Expand Down
66 changes: 66 additions & 0 deletions internal/service/email_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,72 @@ func TestEmailService_SendEmailForTemplate(t *testing.T) {
require.NoError(t, err)
})

t.Run("Successfully sends email with from_name override from channel settings", func(t *testing.T) {
// Setup workspace mock
workspace := &domain.Workspace{
ID: workspaceID,
Settings: domain.WorkspaceSettings{
CustomEndpointURL: nil,
},
}
mockWorkspaceRepo.EXPECT().
GetByID(gomock.Any(), workspaceID).
Return(workspace, nil)

// Create template config with from_name in settings
templateConfigWithFromName := domain.ChannelTemplate{
TemplateID: "template-789",
Settings: domain.MapOfAny{
"from_name": "Custom Support Team",
},
}

// Setup template service mock
mockTemplateService.EXPECT().
GetTemplateByID(gomock.Any(), workspaceID, templateConfigWithFromName.TemplateID, int64(0)).
Return(emailTemplate, nil)

// Setup compile template mock
mockTemplateService.EXPECT().
CompileTemplate(gomock.Any(), gomock.Any()).
Return(compileResult, nil)

// Setup message repository mock
mockMessageRepo.EXPECT().
Create(gomock.Any(), workspaceID, gomock.Any()).
Return(nil)

// Setup email provider mock - verify it receives the custom from_name
mockSESService.EXPECT().
SendEmail(
gomock.Any(),
gomock.Any(),
).DoAndReturn(func(_ context.Context, req domain.SendEmailProviderRequest) error {
// Verify the from_name is overridden from channel settings
assert.Equal(t, "Custom Support Team", req.FromName, "FromName should be overridden from channel settings")
assert.Equal(t, "sender@example.com", req.FromAddress, "FromAddress should remain from email sender")
return nil
})

// Call method under test
request := domain.SendEmailRequest{
WorkspaceID: workspaceID,
IntegrationID: "test-integration-id",
MessageID: messageID,
ExternalID: nil,
Contact: contact,
TemplateConfig: templateConfigWithFromName,
MessageData: messageData,
TrackingSettings: trackingSettings,
EmailProvider: emailProvider,
EmailOptions: options,
}
err := emailService.SendEmailForTemplate(ctx, request)

// Assertions
require.NoError(t, err)
})

t.Run("Error getting template", func(t *testing.T) {
// Setup template service mock to return an error
mockTemplateService.EXPECT().
Expand Down
Loading
Loading