From 372ed70b73e64c2baa3c70c319cc718f78a4a3f9 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Thu, 18 Jun 2026 11:46:31 +0200 Subject: [PATCH] add override sender on template Signed-off-by: Ronni Skansing --- backend/database/campaignTemplate.go | 6 ++ backend/model/campaignTemplate.go | 23 +++++++ backend/repository/campaignTemplate.go | 15 +++++ backend/service/campaign.go | 21 +++++++ backend/service/campaignTemplate.go | 21 +++++++ frontend/src/lib/api/api.js | 18 ++++++ .../src/routes/campaign-template/+page.svelte | 60 +++++++++++++++++++ 7 files changed, 164 insertions(+) diff --git a/backend/database/campaignTemplate.go b/backend/database/campaignTemplate.go index b407a40..176500f 100644 --- a/backend/database/campaignTemplate.go +++ b/backend/database/campaignTemplate.go @@ -62,6 +62,12 @@ type CampaignTemplate struct { EmailID *uuid.UUID `gorm:"type:uuid;index;"` Email *Email `gorm:"foreignKey:EmailID;references:ID;"` + // optional sender overrides applied at send time + // an empty value means inherit the value from the email + OverrideMailEnvelopeFrom string `gorm:"not null;default:''"` + OverrideMailHeaderFrom string `gorm:"not null;default:''"` + OverrideSubject string `gorm:"not null;default:''"` + SMTPConfigurationID *uuid.UUID `gorm:"type:uuid;index;"` SMTPConfiguration *SMTPConfiguration `gorm:"foreignKey:SMTPConfigurationID"` diff --git a/backend/model/campaignTemplate.go b/backend/model/campaignTemplate.go index 6e684ce..8688438 100644 --- a/backend/model/campaignTemplate.go +++ b/backend/model/campaignTemplate.go @@ -57,6 +57,11 @@ type CampaignTemplate struct { EmailID nullable.Nullable[uuid.UUID] `json:"emailID"` Email *Email `json:"email"` + // optional sender overrides; an empty value means inherit from the email + OverrideMailEnvelopeFrom nullable.Nullable[vo.MailEnvelopeFrom] `json:"overrideMailEnvelopeFrom"` + OverrideMailHeaderFrom nullable.Nullable[vo.Email] `json:"overrideMailHeaderFrom"` + OverrideSubject nullable.Nullable[vo.OptionalString255] `json:"overrideSubject"` + SMTPConfigurationID nullable.Nullable[uuid.UUID] `json:"smtpConfigurationID"` SMTPConfiguration *SMTPConfiguration `json:"smtpConfiguration"` @@ -222,6 +227,24 @@ func (c *CampaignTemplate) ToDBMap() map[string]any { m["email_id"] = c.EmailID.MustGet() } } + if c.OverrideMailEnvelopeFrom.IsSpecified() { + m["override_mail_envelope_from"] = "" + if v, err := c.OverrideMailEnvelopeFrom.Get(); err == nil { + m["override_mail_envelope_from"] = v.String() + } + } + if c.OverrideMailHeaderFrom.IsSpecified() { + m["override_mail_header_from"] = "" + if v, err := c.OverrideMailHeaderFrom.Get(); err == nil { + m["override_mail_header_from"] = v.String() + } + } + if c.OverrideSubject.IsSpecified() { + m["override_subject"] = "" + if v, err := c.OverrideSubject.Get(); err == nil { + m["override_subject"] = v.String() + } + } if c.SMTPConfigurationID.IsSpecified() { if c.SMTPConfigurationID.IsNull() { m["smtp_configuration_id"] = nil diff --git a/backend/repository/campaignTemplate.go b/backend/repository/campaignTemplate.go index c9a72d3..52cc771 100644 --- a/backend/repository/campaignTemplate.go +++ b/backend/repository/campaignTemplate.go @@ -889,6 +889,18 @@ func ToCampaignTemplate(row *database.CampaignTemplate) (*model.CampaignTemplate if row.Email != nil { email = ToEmail(row.Email) } + overrideMailEnvelopeFrom := nullable.NewNullNullable[vo.MailEnvelopeFrom]() + if row.OverrideMailEnvelopeFrom != "" { + overrideMailEnvelopeFrom.Set(*vo.NewMailEnvelopeFromMust(row.OverrideMailEnvelopeFrom)) + } + overrideMailHeaderFrom := nullable.NewNullNullable[vo.Email]() + if row.OverrideMailHeaderFrom != "" { + overrideMailHeaderFrom.Set(*vo.NewEmailMust(row.OverrideMailHeaderFrom)) + } + overrideSubject := nullable.NewNullNullable[vo.OptionalString255]() + if row.OverrideSubject != "" { + overrideSubject.Set(*vo.NewOptionalString255Must(row.OverrideSubject)) + } smtpConfigurationID := nullable.NewNullNullable[uuid.UUID]() if row.SMTPConfigurationID != nil { smtpConfigurationID.Set(*row.SMTPConfigurationID) @@ -944,6 +956,9 @@ func ToCampaignTemplate(row *database.CampaignTemplate) (*model.CampaignTemplate AfterLandingPageRedirectURL: redirectURL, EmailID: emailID, Email: email, + OverrideMailEnvelopeFrom: overrideMailEnvelopeFrom, + OverrideMailHeaderFrom: overrideMailHeaderFrom, + OverrideSubject: overrideSubject, SMTPConfigurationID: smtpConfigurationID, SMTPConfiguration: smtpConfiguration, APISenderID: apiSenderID, diff --git a/backend/service/campaign.go b/backend/service/campaign.go index 15f7144..71e781c 100644 --- a/backend/service/campaign.go +++ b/backend/service/campaign.go @@ -2088,6 +2088,24 @@ func isCalendarAttachment(path string) bool { return ext == ".ics" || ext == ".ical" } +// applySenderOverride applies the campaign template's optional sender overrides +// onto the in-memory email so all downstream send logic uses the effective values. +// an unset override leaves the email's own value unchanged. +func applySenderOverride(cTemplate *model.CampaignTemplate, email *model.Email) { + if cTemplate == nil || email == nil { + return + } + if v, err := cTemplate.OverrideMailEnvelopeFrom.Get(); err == nil { + email.MailEnvelopeFrom = nullable.NewNullableWithValue(v) + } + if v, err := cTemplate.OverrideMailHeaderFrom.Get(); err == nil { + email.MailHeaderFrom = nullable.NewNullableWithValue(v) + } + if v, err := cTemplate.OverrideSubject.Get(); err == nil { + email.MailHeaderSubject = nullable.NewNullableWithValue(v) + } +} + func (c *Campaign) sendCampaignMessages( ctx context.Context, session *model.Session, @@ -2208,6 +2226,7 @@ func (c *Campaign) sendCampaignMessages( ) return errs.Wrap(errors.Join(err, closeErr)) } + applySenderOverride(cTemplate, email) content, err := email.Content.Get() if err != nil { // if mail templates fails to parse, close the campaign @@ -3413,6 +3432,7 @@ func (c *Campaign) GetCampaignEmailBody( c.Logger.Errorw("failed to get message by id", "error", err) return "", errs.Wrap(err) } + applySenderOverride(cTemplate, email) urlIdentifier := cTemplate.URLIdentifier if urlIdentifier == nil { return "", errors.New("url identifier is nil") @@ -4139,6 +4159,7 @@ func (c *Campaign) sendSingleCampaignMessage( c.Logger.Errorw("failed to get email by id", "error", err) return errs.Wrap(err) } + applySenderOverride(cTemplate, email) // update last attempt timestamp campaignRecipientID := campaignRecipient.ID.MustGet() diff --git a/backend/service/campaignTemplate.go b/backend/service/campaignTemplate.go index 3b0cdc1..fcb4edb 100644 --- a/backend/service/campaignTemplate.go +++ b/backend/service/campaignTemplate.go @@ -655,6 +655,27 @@ func (c *CampaignTemplate) UpdateByID( incoming.EmailID.SetNull() } } + if campaignTemplate.OverrideMailEnvelopeFrom.IsSpecified() { + if v, err := campaignTemplate.OverrideMailEnvelopeFrom.Get(); err == nil { + incoming.OverrideMailEnvelopeFrom.Set(v) + } else { + incoming.OverrideMailEnvelopeFrom.SetNull() + } + } + if campaignTemplate.OverrideMailHeaderFrom.IsSpecified() { + if v, err := campaignTemplate.OverrideMailHeaderFrom.Get(); err == nil { + incoming.OverrideMailHeaderFrom.Set(v) + } else { + incoming.OverrideMailHeaderFrom.SetNull() + } + } + if campaignTemplate.OverrideSubject.IsSpecified() { + if v, err := campaignTemplate.OverrideSubject.Get(); err == nil { + incoming.OverrideSubject.Set(v) + } else { + incoming.OverrideSubject.SetNull() + } + } if campaignTemplate.BeforeLandingPageID.IsSpecified() { if v, err := campaignTemplate.BeforeLandingPageID.Get(); err == nil { incoming.BeforeLandingPageID.Set(v) diff --git a/frontend/src/lib/api/api.js b/frontend/src/lib/api/api.js index 70d79e1..4aef154 100644 --- a/frontend/src/lib/api/api.js +++ b/frontend/src/lib/api/api.js @@ -1074,6 +1074,9 @@ export class API { * @param {string} template.stateIdentifierID * @param {string} template.urlPath * @param {string} template.emailID + * @param {string|null} template.overrideMailHeaderFrom + * @param {string|null} template.overrideMailEnvelopeFrom + * @param {string|null} template.overrideSubject * @returns {Promise} */ create: async ({ @@ -1092,6 +1095,9 @@ export class API { stateIdentifierID, afterLandingPageRedirectURL, emailID: emailID, + overrideMailHeaderFrom = null, + overrideMailEnvelopeFrom = null, + overrideSubject = null, urlPath: urlPath }) => { return await postJSON(this.getPath('/campaign/template'), { @@ -1110,6 +1116,9 @@ export class API { urlIdentifierID: urlIdentifierID, stateIdentifierID: stateIdentifierID, emailID: emailID, + overrideMailHeaderFrom: overrideMailHeaderFrom, + overrideMailEnvelopeFrom: overrideMailEnvelopeFrom, + overrideSubject: overrideSubject, urlPath: urlPath }); }, @@ -1132,6 +1141,9 @@ export class API { * @param {string} template.apiSenderID * @param {string} template.afterLandingPageRedirectURL * @param {string} template.emailID + * @param {string|null} template.overrideMailHeaderFrom + * @param {string|null} template.overrideMailEnvelopeFrom + * @param {string|null} template.overrideSubject * @param {string} template.urlIdentifierID * @param {string} template.stateIdentifierID * @param {string} template.urlPath @@ -1152,6 +1164,9 @@ export class API { apiSenderID, afterLandingPageRedirectURL, emailID: emailID, + overrideMailHeaderFrom = null, + overrideMailEnvelopeFrom = null, + overrideSubject = null, urlIdentifierID: urlIdentifierID, stateIdentifierID: stateIdentifierID, urlPath: urlPath @@ -1170,6 +1185,9 @@ export class API { apiSenderID: apiSenderID, afterLandingPageRedirectURL: afterLandingPageRedirectURL, emailID: emailID, + overrideMailHeaderFrom: overrideMailHeaderFrom, + overrideMailEnvelopeFrom: overrideMailEnvelopeFrom, + overrideSubject: overrideSubject, urlIdentifierID: urlIdentifierID, stateIdentifierID: stateIdentifierID, urlPath: urlPath diff --git a/frontend/src/routes/campaign-template/+page.svelte b/frontend/src/routes/campaign-template/+page.svelte index bdb1686..31049d1 100644 --- a/frontend/src/routes/campaign-template/+page.svelte +++ b/frontend/src/routes/campaign-template/+page.svelte @@ -55,6 +55,9 @@ afterLandingPage: null, afterLandingPageRedirectURL: '', email: null, + overrideMailHeaderFrom: '', + overrideMailEnvelopeFrom: '', + overrideSubject: '', smtpConfiguration: null, apiSender: null, urlIdentifier: 'id', @@ -306,6 +309,9 @@ name: formValues.name, domainID: domainMap.byValue(formValues.domain), emailID: emailMap.byValueOrNull(formValues.email), + overrideMailHeaderFrom: formValues.overrideMailHeaderFrom || null, + overrideMailEnvelopeFrom: formValues.overrideMailEnvelopeFrom || null, + overrideSubject: formValues.overrideSubject || null, smtpConfigurationID: smtpConfigurationMap.byValueOrNull(formValues.smtpConfiguration), apiSenderID: apiSenderMap.byValueOrNull(formValues.apiSender), landingPageID: @@ -346,6 +352,9 @@ name: formValues.name, domainID: domainMap.byValueOrNull(formValues.domain), emailID: emailMap.byValueOrNull(formValues.email), + overrideMailHeaderFrom: formValues.overrideMailHeaderFrom || null, + overrideMailEnvelopeFrom: formValues.overrideMailEnvelopeFrom || null, + overrideSubject: formValues.overrideSubject || null, smtpConfigurationID: smtpConfigurationMap.byValueOrNull(formValues.smtpConfiguration), apiSenderID: apiSenderMap.byValueOrNull(formValues.apiSender), landingPageID: @@ -418,6 +427,9 @@ afterLandingPage: null, afterLandingPageRedirectURL: '', email: null, + overrideMailHeaderFrom: '', + overrideMailEnvelopeFrom: '', + overrideSubject: '', smtpConfiguration: null, apiSender: null, urlIdentifier: 'id', @@ -511,6 +523,9 @@ } formValues.domain = domainMap.byKey(template.domainID); formValues.email = emailMap.byKey(template.emailID); + formValues.overrideMailHeaderFrom = template.overrideMailHeaderFrom || ''; + formValues.overrideMailEnvelopeFrom = template.overrideMailEnvelopeFrom || ''; + formValues.overrideSubject = template.overrideSubject || ''; // handle landing page (page or proxy) if (template.landingPageID) { @@ -544,6 +559,9 @@ (template.urlIdentifierID && identifierMap.byKey(template.urlIdentifierID) !== 'id') || (template.stateIdentifierID && identifierMap.byKey(template.stateIdentifierID) !== 'session') || + template.overrideMailHeaderFrom || + template.overrideMailEnvelopeFrom || + template.overrideSubject || template.apiSenderID ) // Show advanced if using External API ); @@ -643,6 +661,12 @@ > {emailMap.byKey(template.emailID)} + {#if template.overrideMailHeaderFrom || template.overrideMailEnvelopeFrom || template.overrideSubject} + sender overridden + {/if} {/if} @@ -1145,6 +1169,42 @@ Simulation URLs to allow:\n${allowListingData.simulationUrl}\n > +

+ Sender override +

+

+ Reuse the same email with a different sender. Leave a field blank to use the email's + own value. +

+
+
+ From override +
+
+ Envelope from override +
+
+ Subject override +
+
{/if}