@@ -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
403393type BranchNodeExecutor struct {
404394 queryBuilder * QueryBuilder
0 commit comments