From 198c3a0f296c3c65ea44087a31dbb2f52d27ffce Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Sat, 21 Mar 2026 12:31:31 +0100 Subject: [PATCH] fix missing cascading of deleting an email used by templates or campaigns Signed-off-by: Ronni Skansing --- backend/app/services.go | 17 ++++--- backend/repository/campaignTemplate.go | 64 ++++++++++++++++++++++++ backend/service/campaignTemplate.go | 67 ++++++++++++++++++++++++++ backend/service/email.go | 26 +++++++--- 4 files changed, 159 insertions(+), 15 deletions(-) diff --git a/backend/app/services.go b/backend/app/services.go index e1a6357..31270a6 100644 --- a/backend/app/services.go +++ b/backend/app/services.go @@ -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, diff --git a/backend/repository/campaignTemplate.go b/backend/repository/campaignTemplate.go index 3651362..c9a72d3 100644 --- a/backend/repository/campaignTemplate.go +++ b/backend/repository/campaignTemplate.go @@ -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( diff --git a/backend/service/campaignTemplate.go b/backend/service/campaignTemplate.go index 893b7bf..3b0cdc1 100644 --- a/backend/service/campaignTemplate.go +++ b/backend/service/campaignTemplate.go @@ -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( diff --git a/backend/service/email.go b/backend/service/email.go index ef1e055..8f99417 100644 --- a/backend/service/email.go +++ b/backend/service/email.go @@ -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,