add override sender on template

Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing
2026-06-18 11:46:31 +02:00
parent a0317d4f61
commit 372ed70b73
7 changed files with 164 additions and 0 deletions
+6
View File
@@ -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"`
+23
View File
@@ -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
+15
View File
@@ -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,
+21
View File
@@ -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()
+21
View File
@@ -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)
+18
View File
@@ -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<ApiResponse>}
*/
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
@@ -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)}
</a>
{#if template.overrideMailHeaderFrom || template.overrideMailEnvelopeFrom || template.overrideSubject}
<span
class="text-xs text-gray-500 dark:text-gray-400"
title="This template overrides the email's sender">sender overridden</span
>
{/if}
{/if}
</TableCell>
<TableCell>
@@ -1145,6 +1169,42 @@ Simulation URLs to allow:\n${allowListingData.simulationUrl}\n
>
</div>
</div>
<h4 class="text-sm font-medium text-pc-darkblue dark:text-white mt-6 mb-3">
Sender override
</h4>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-3">
Reuse the same email with a different sender. Leave a field blank to use the email's
own value.
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<TextField
toolTipText="Overrides the email's header From. Display name is allowed, e.g. Support <support@acme.com>."
optional
maxLength={255}
bind:value={formValues.overrideMailHeaderFrom}
placeholder="Support <support@acme.com>">From override</TextField
>
</div>
<div>
<TextField
toolTipText="Overrides the email's envelope from / Return-Path."
optional
maxLength={255}
bind:value={formValues.overrideMailEnvelopeFrom}
placeholder="bounce@acme.com">Envelope from override</TextField
>
</div>
<div>
<TextField
toolTipText="Overrides the email's subject line."
optional
maxLength={255}
bind:value={formValues.overrideSubject}
placeholder="Action required: verify your account">Subject override</TextField
>
</div>
</div>
</div>
{/if}
</div>