Skip to content

Commit efbb5a8

Browse files
committed
fixes & tests
1 parent 68e178f commit efbb5a8

File tree

7 files changed

+323
-171
lines changed

7 files changed

+323
-171
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.
66

77
- **Segments**: Added template filter to email activity conditions, allowing segments like "opened template X at least 3 times"
88
- **SMTP**: Fixed TLS override option not working for system emails (magic codes, invitations, alerts). The "Use TLS" toggle was ignored, causing certificate errors on local SMTP relays (#275)
9+
- **Automations**: Fixed automation emails rendering `{{ notification_center_url }}` and `{{ unsubscribe_url }}` as empty strings by using the shared template data builder (#279)
10+
- **Segments**: Fixed race condition where background task execution could pick up unrelated tasks, causing flaky segment recompute behavior
911

1012
## [27.2] - 2026-02-21
1113

internal/app/app.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,7 @@ func (a *App) InitServices() error {
953953
a.contactRepo,
954954
a.workspaceRepo,
955955
a.contactListRepo,
956+
a.listRepo,
956957
a.templateRepo,
957958
a.emailQueueRepo,
958959
a.messageHistoryRepo,

internal/service/automation_executor.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ func NewAutomationExecutor(
3131
contactRepo domain.ContactRepository,
3232
workspaceRepo domain.WorkspaceRepository,
3333
contactListRepo domain.ContactListRepository,
34+
listRepo domain.ListRepository,
3435
templateRepo domain.TemplateRepository,
3536
emailQueueRepo domain.EmailQueueRepository,
3637
messageRepo domain.MessageHistoryRepository,
@@ -43,7 +44,7 @@ func NewAutomationExecutor(
4344
executors := map[domain.NodeType]NodeExecutor{
4445
domain.NodeTypeTrigger: NewTriggerNodeExecutor(),
4546
domain.NodeTypeDelay: NewDelayNodeExecutor(),
46-
domain.NodeTypeEmail: NewEmailNodeExecutor(emailQueueRepo, templateRepo, workspaceRepo, apiEndpoint, log),
47+
domain.NodeTypeEmail: NewEmailNodeExecutor(emailQueueRepo, templateRepo, workspaceRepo, listRepo, apiEndpoint, log),
4748
domain.NodeTypeBranch: NewBranchNodeExecutor(qb, workspaceRepo),
4849
domain.NodeTypeFilter: NewFilterNodeExecutor(qb, workspaceRepo),
4950
domain.NodeTypeAddToList: NewAddToListNodeExecutor(contactListRepo),

internal/service/automation_node_executor.go

Lines changed: 38 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ type EmailNodeExecutor struct {
147147
emailQueueRepo domain.EmailQueueRepository
148148
templateRepo domain.TemplateRepository
149149
workspaceRepo domain.WorkspaceRepository
150+
listRepo domain.ListRepository
150151
apiEndpoint string
151152
logger logger.Logger
152153
}
@@ -156,13 +157,15 @@ func NewEmailNodeExecutor(
156157
emailQueueRepo domain.EmailQueueRepository,
157158
templateRepo domain.TemplateRepository,
158159
workspaceRepo domain.WorkspaceRepository,
160+
listRepo domain.ListRepository,
159161
apiEndpoint string,
160162
log logger.Logger,
161163
) *EmailNodeExecutor {
162164
return &EmailNodeExecutor{
163165
emailQueueRepo: emailQueueRepo,
164166
templateRepo: templateRepo,
165167
workspaceRepo: workspaceRepo,
168+
listRepo: listRepo,
166169
apiEndpoint: apiEndpoint,
167170
logger: log,
168171
}
@@ -226,13 +229,10 @@ func (e *EmailNodeExecutor) Execute(ctx context.Context, params NodeExecutionPar
226229
return nil, fmt.Errorf("failed to get template: %w", err)
227230
}
228231

229-
// 5. Build template data from contact + automation
230-
templateData := buildAutomationTemplateData(params.ContactData, params.Automation)
231-
232-
// 6. Generate message ID
232+
// 5. Generate message ID
233233
messageID := fmt.Sprintf("%s_%s", params.WorkspaceID, uuid.New().String())
234234

235-
// 7. Setup tracking settings
235+
// 6. Setup tracking settings
236236
endpoint := e.apiEndpoint
237237
if workspace.Settings.CustomEndpointURL != nil && *workspace.Settings.CustomEndpointURL != "" {
238238
endpoint = *workspace.Settings.CustomEndpointURL
@@ -249,13 +249,39 @@ func (e *EmailNodeExecutor) Execute(ctx context.Context, params NodeExecutionPar
249249
MessageID: messageID,
250250
}
251251

252+
// 7. Build template data using shared domain.BuildTemplateData
253+
var listID, listName string
254+
if params.Automation.ListID != "" {
255+
list, err := e.listRepo.GetListByID(ctx, params.WorkspaceID, params.Automation.ListID)
256+
if err != nil {
257+
return nil, fmt.Errorf("failed to get list: %w", err)
258+
}
259+
listID = list.ID
260+
listName = list.Name
261+
}
262+
263+
templateData, err := domain.BuildTemplateData(domain.TemplateDataRequest{
264+
WorkspaceID: params.WorkspaceID,
265+
WorkspaceSecretKey: workspace.Settings.SecretKey,
266+
ContactWithList: domain.ContactWithList{Contact: params.ContactData, ListID: listID, ListName: listName},
267+
MessageID: messageID,
268+
TrackingSettings: trackingSettings,
269+
ProvidedData: domain.MapOfAny{
270+
"automation_id": params.Automation.ID,
271+
"automation_name": params.Automation.Name,
272+
},
273+
})
274+
if err != nil {
275+
return nil, fmt.Errorf("failed to build template data: %w", err)
276+
}
277+
252278
// 8. Compile template
253279
compiledTemplate, err := notifuse_mjml.CompileTemplate(
254280
notifuse_mjml.CompileTemplateRequest{
255281
WorkspaceID: params.WorkspaceID,
256282
MessageID: messageID,
257283
VisualEditorTree: template.Email.VisualEditorTree,
258-
TemplateData: templateData,
284+
TemplateData: notifuse_mjml.MapOfAny(templateData),
259285
TrackingSettings: trackingSettings,
260286
},
261287
)
@@ -314,7 +340,12 @@ func (e *EmailNodeExecutor) Execute(ctx context.Context, params NodeExecutionPar
314340
UpdatedAt: time.Now().UTC(),
315341
}
316342

317-
// 12. Enqueue the email
343+
// 12. Add List-Unsubscribe header for RFC-8058 compliance
344+
if url, ok := templateData["oneclick_unsubscribe_url"].(string); ok && url != "" {
345+
entry.Payload.EmailOptions.ListUnsubscribeURL = url
346+
}
347+
348+
// 13. Enqueue the email
318349
if err := e.emailQueueRepo.Enqueue(ctx, params.WorkspaceID, []*domain.EmailQueueEntry{entry}); err != nil {
319350
return nil, fmt.Errorf("failed to enqueue email: %w", err)
320351
}
@@ -358,47 +389,6 @@ func parseEmailNodeConfig(config map[string]interface{}) (*domain.EmailNodeConfi
358389
return &c, nil
359390
}
360391

361-
// buildAutomationTemplateData creates template data for automation emails
362-
func buildAutomationTemplateData(contact *domain.Contact, automation *domain.Automation) map[string]interface{} {
363-
data := make(map[string]interface{})
364-
365-
if contact != nil {
366-
// Add contact fields
367-
data["email"] = contact.Email
368-
369-
// Convert contact to map for proper liquid template rendering
370-
// This ensures NullableString fields are converted to plain strings
371-
contactData, err := contact.ToMapOfAny()
372-
if err != nil {
373-
// Fallback to empty map if conversion fails
374-
contactData = domain.MapOfAny{}
375-
}
376-
data["contact"] = contactData
377-
378-
// Add standard contact fields if they exist (for backward compatibility)
379-
if contact.FirstName != nil && !contact.FirstName.IsNull {
380-
data["first_name"] = contact.FirstName.String
381-
}
382-
if contact.LastName != nil && !contact.LastName.IsNull {
383-
data["last_name"] = contact.LastName.String
384-
}
385-
if contact.FullName != nil && !contact.FullName.IsNull {
386-
data["full_name"] = contact.FullName.String
387-
}
388-
if contact.Country != nil && !contact.Country.IsNull {
389-
data["country"] = contact.Country.String
390-
}
391-
}
392-
393-
if automation != nil {
394-
// Add automation context
395-
data["automation_id"] = automation.ID
396-
data["automation_name"] = automation.Name
397-
}
398-
399-
return data
400-
}
401-
402392
// BranchNodeExecutor executes branch nodes using database queries
403393
type BranchNodeExecutor struct {
404394
queryBuilder *QueryBuilder

0 commit comments

Comments
 (0)