fix missing cascading of deleting an email used by templates or campaigns

Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing
2026-03-21 12:31:31 +01:00
parent 43d6415894
commit 198c3a0f29
4 changed files with 159 additions and 15 deletions
+9 -8
View File
@@ -184,14 +184,15 @@ func NewServices(
}
ipAllowListService := service.NewIPAllowListService(logger, repositories.Proxy)
email := &service.Email{
Common: common,
AttachmentPath: attachmentPath,
AttachmentService: attachment,
DomainService: domain,
EmailRepository: repositories.Email,
SMTPService: smtpConfiguration,
RecipientService: recipient,
TemplateService: templateService,
Common: common,
AttachmentPath: attachmentPath,
AttachmentService: attachment,
DomainService: domain,
EmailRepository: repositories.Email,
SMTPService: smtpConfiguration,
RecipientService: recipient,
TemplateService: templateService,
CampaignTemplateService: campaignTemplate,
}
campaign := &service.Campaign{
Common: common,
+64
View File
@@ -657,6 +657,70 @@ func (r *CampaignTemplate) RemoveSmtpIDFromAll(
return nil
}
// GetByEmailID gets all campaign templates by email ID
func (r *CampaignTemplate) GetByEmailID(
ctx context.Context,
emailID *uuid.UUID,
options *CampaignTemplateOption,
) ([]*model.CampaignTemplate, error) {
db := r.DB
if options.Columns != nil && len(options.Columns) > 0 {
db = db.Select(strings.Join(options.Columns, ","))
}
db = r.load(options, db)
db, err := useQuery(db, database.CAMPAIGN_TEMPLATE_TABLE, options.QueryArgs, allowdCampaignTemplatesColumns...)
if err != nil {
return nil, errs.Wrap(err)
}
db = db.Where(
fmt.Sprintf(
"%s = ?",
TableColumn(database.CAMPAIGN_TEMPLATE_TABLE, "email_id"),
),
emailID.String(),
)
var tmpl []database.CampaignTemplate
res := db.Find(&tmpl)
if res.Error != nil {
return nil, res.Error
}
templates := []*model.CampaignTemplate{}
for _, t := range tmpl {
tmpl, err := ToCampaignTemplate(&t)
if err != nil {
return nil, errs.Wrap(err)
}
templates = append(templates, tmpl)
}
return templates, nil
}
// RemoveEmailIDFromAll removes the email ID from all campaign templates that reference it
// and marks them as not usable
func (r *CampaignTemplate) RemoveEmailIDFromAll(
ctx context.Context,
emailID *uuid.UUID,
) error {
row := map[string]any{}
AddUpdatedAt(row)
row["email_id"] = nil
row["is_usable"] = false
res := r.DB.
Model(&database.CampaignTemplate{}).
Where(
fmt.Sprintf(
"%s = ?",
TableColumn(database.CAMPAIGN_TEMPLATE_TABLE, "email_id"),
),
emailID.String(),
).
Updates(row)
if res.Error != nil {
return res.Error
}
return nil
}
// RemovePageIDFromAll removes the page ID from any matching columns
// landing_page_id, before_landing_page_id and after_landing_page_id
func (r *CampaignTemplate) RemovePageIDFromAll(
+67
View File
@@ -412,6 +412,73 @@ func (c *CampaignTemplate) RemoveSmtpBySmtpID(
return nil
}
// RemoveEmailByEmailID removes the email ID from all campaign templates that reference it,
// marks them as not usable, and closes any active campaigns that use those templates.
func (c *CampaignTemplate) RemoveEmailByEmailID(
ctx context.Context,
session *model.Session,
emailID *uuid.UUID,
) error {
ae := NewAuditEvent("CampaignTemplate.RemoveEmailByEmailID", session)
ae.Details["emailId"] = emailID.String()
// check permissions
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
c.LogAuthError(err)
return err
}
if !isAuthorized {
c.AuditLogNotAuthorized(ae)
return errs.ErrAuthorizationFailed
}
// find all campaign templates that reference this email
templatesAffected, err := c.CampaignTemplateRepository.GetByEmailID(
ctx,
emailID,
&repository.CampaignTemplateOption{},
)
if err != nil {
c.Logger.Errorw("failed to get affected campaign templates", "error", err)
return err
}
templateIDs := []*uuid.UUID{}
for _, t := range templatesAffected {
id := t.ID.MustGet()
templateIDs = append(templateIDs, &id)
}
// close any active campaigns that use the affected templates
campaignsAffected, err := c.CampaignRepository.GetByTemplateIDs(ctx, templateIDs)
if err != nil {
c.Logger.Errorw("failed to get affected campaigns", "error", err)
return err
}
for _, campaign := range campaignsAffected {
if !campaign.IsActive() {
continue
}
err := campaign.Close()
if err != nil {
c.Logger.Errorw("failed to close campaign", "error", err)
}
campaignID := campaign.ID.MustGet()
err = c.CampaignRepository.UpdateByID(
ctx,
&campaignID,
campaign,
)
if err != nil {
c.Logger.Errorw("failed to update closed campaign", "error", err)
}
}
// remove the email id from all affected templates
err = c.CampaignTemplateRepository.RemoveEmailIDFromAll(ctx, emailID)
if err != nil {
c.Logger.Errorw("failed to remove email ID from all campaign templates", "error", err)
return err
}
return nil
}
// RemovePageByPageID removes the page ID from a template
// this makes the template unusable until a domain has been added.
func (c *CampaignTemplate) RemovePagesByPageID(
+19 -7
View File
@@ -28,13 +28,14 @@ import (
// Email is a Email service
type Email struct {
Common
EmailRepository *repository.Email
SMTPService *SMTPConfiguration
DomainService *Domain
RecipientService *Recipient
TemplateService *Template
AttachmentService *Attachment
AttachmentPath string
EmailRepository *repository.Email
SMTPService *SMTPConfiguration
DomainService *Domain
RecipientService *Recipient
TemplateService *Template
AttachmentService *Attachment
CampaignTemplateService *CampaignTemplate
AttachmentPath string
}
// AttachmentWithInline represents an attachment with inline flag
@@ -947,6 +948,17 @@ func (m *Email) DeleteByID(
m.AuditLogNotAuthorized(ae)
return errs.ErrAuthorizationFailed
}
// remove the email from any campaign templates that reference it and close
// any active campaigns that use those templates
err = m.CampaignTemplateService.RemoveEmailByEmailID(
ctx,
session,
id,
)
if err != nil {
m.Logger.Errorw("failed to remove email relation from campaign templates", "error", err)
return errs.Wrap(err)
}
// delete email by id
err = m.EmailRepository.DeleteByID(
ctx,