diff --git a/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendControlsPolicyData.cs b/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendControlsPolicyData.cs
index 4aa09aa1ce48..dee6f55f79bb 100644
--- a/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendControlsPolicyData.cs
+++ b/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendControlsPolicyData.cs
@@ -13,4 +13,6 @@ public class SendControlsPolicyData : IPolicyDataModel
[Display(Name = "AllowedDomains")]
[StringLength(1000)]
public string? AllowedDomains { get; set; }
+ [Display(Name = "DeletionDays")]
+ public int? DeletionDays { get; set; }
}
diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEvent.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEvent.cs
index b33445713ca9..5f78596ce371 100644
--- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEvent.cs
+++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEvent.cs
@@ -109,6 +109,10 @@ private async Task UpdateSendsByPolicyAsync(Policy postUpsertedPolicyState, Send
{
await sendRepository.UpdateManyDisabledAsync(disabled, true);
}
+ if (sendControlsPolicyData.DeletionDays != null)
+ {
+ await sendRepository.UpdateManyDeletionDatesByIdsAsync(sendIdsChunk, sendControlsPolicyData.DeletionDays.GetValueOrDefault(0));
+ }
}
}
}
diff --git a/src/Core/Tools/Repositories/ISendRepository.cs b/src/Core/Tools/Repositories/ISendRepository.cs
index 4f7ced15df5e..ddd052222765 100644
--- a/src/Core/Tools/Repositories/ISendRepository.cs
+++ b/src/Core/Tools/Repositories/ISendRepository.cs
@@ -98,4 +98,12 @@ UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId,
/// The IDs of the ss to load
///
Task> GetManyByIdsAsync(IEnumerable ids);
+
+ ///
+ /// Update deletion dates in bulk by IDs
+ ///
+ /// The IDs of the s to update
+ /// The number of hours after the s' creation dates to set the deletion date
+ ///
+ Task UpdateManyDeletionDatesByIdsAsync(IEnumerable ids, int deletionHours);
}
diff --git a/src/Infrastructure.Dapper/Tools/Repositories/SendRepository.cs b/src/Infrastructure.Dapper/Tools/Repositories/SendRepository.cs
index ea112bfefedb..ef550b26ce83 100644
--- a/src/Infrastructure.Dapper/Tools/Repositories/SendRepository.cs
+++ b/src/Infrastructure.Dapper/Tools/Repositories/SendRepository.cs
@@ -224,6 +224,15 @@ public async Task> GetManyByIdsAsync(IEnumerable ids)
return sends;
}
+ public async Task UpdateManyDeletionDatesByIdsAsync(IEnumerable ids, int deletionHours)
+ {
+ using var connection = new SqlConnection(ConnectionString);
+ await connection.ExecuteAsync(
+ $"[{Schema}].[Send_UpdateDeletionDatesByIds]",
+ new { Ids = ids.ToGuidIdArrayTVP(), DeletionHours = deletionHours },
+ commandType: CommandType.StoredProcedure);
+ }
+
private async Task ProtectDataAndSaveAsync(Send send, Func saveTask)
{
if (send == null)
diff --git a/src/Infrastructure.EntityFramework/Tools/Repositories/SendRepository.cs b/src/Infrastructure.EntityFramework/Tools/Repositories/SendRepository.cs
index 133879c45ec5..d03a18e1d0de 100644
--- a/src/Infrastructure.EntityFramework/Tools/Repositories/SendRepository.cs
+++ b/src/Infrastructure.EntityFramework/Tools/Repositories/SendRepository.cs
@@ -156,6 +156,20 @@ public async Task> GetIdsByOrganizationIdAsync(Guid organizati
return Mapper.Map>(orgUserSendIds);
}
+ public async Task UpdateManyDeletionDatesByIdsAsync(IEnumerable ids, int deletionHours)
+ {
+ using var scope = ServiceScopeFactory.CreateScope();
+ var dbContext = GetDatabaseContext(scope);
+ var sends = dbContext.Sends.Where(s => ids.Contains(s.Id));
+ await sends.ExecuteUpdateAsync(setters => setters
+ .SetProperty(s => s.DeletionDate, s => s.CreationDate.AddHours(deletionHours))
+ .SetProperty(s => s.RevisionDate, DateTime.UtcNow)
+ );
+ var userIds = await sends.Select(s => s.User.Id).ToArrayAsync() ?? [];
+ await dbContext.UserBumpManyAccountRevisionDatesAsync(userIds);
+ await dbContext.SaveChangesAsync();
+ }
+
public async Task> GetManyByIdsAsync(IEnumerable ids)
{
using var scope = ServiceScopeFactory.CreateScope();
diff --git a/src/Sql/dbo/Tools/Stored Procedures/Send_UpdateDeletionDatesByIds.sql b/src/Sql/dbo/Tools/Stored Procedures/Send_UpdateDeletionDatesByIds.sql
new file mode 100644
index 000000000000..94be904b61f1
--- /dev/null
+++ b/src/Sql/dbo/Tools/Stored Procedures/Send_UpdateDeletionDatesByIds.sql
@@ -0,0 +1,27 @@
+CREATE PROCEDURE [dbo].[Send_UpdateDeletionDatesByIds]
+ @Ids AS [dbo].[GuidIdArray] READONLY,
+ @DeletionHours INT
+AS
+BEGIN
+ SET NOCOUNT ON
+
+ -- Set field
+ UPDATE
+ [dbo].[Send]
+ SET
+ [DeletionDate] = DATEADD(HOUR, @DeletionHours, [CreationDate]),
+ [RevisionDate] = GETUTCDATE()
+ WHERE
+ [Id] IN (SELECT * FROM @Ids)
+
+ -- Bump account revision dates
+ EXEC [dbo].[User_BumpManyAccountRevisionDates]
+ (
+ SELECT DISTINCT
+ UserId
+ FROM
+ [dbo].[Send]
+ WHERE
+ [Id] IN (SELECT * FROM @Ids)
+ )
+END
\ No newline at end of file
diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEventTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEventTests.cs
index f77b0eb6bf2a..ae3124df7eb2 100644
--- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEventTests.cs
+++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEventTests.cs
@@ -449,4 +449,50 @@ await sutProvider.GetDependency()
.Received(1)
.UpdateManyDisabledAsync(Arg.Is>(l => l.Count == 3 && l.Contains(nonCompliantSend1.Id) && l.Contains(nonCompliantSend2.Id) && l.Contains(nonCompliantSend3.Id)), true);
}
+
+ [Theory, BitAutoData]
+ public async Task ExecutePostUpsertSideEffectAsync_DeletionDateUpdatesSendDeletionDates(
+ [PolicyUpdate(PolicyType.SendControls, enabled: true)] PolicyUpdate policyUpdate,
+ [Policy(PolicyType.SendControls, enabled: true)] Policy postUpsertedPolicy,
+ [Policy(PolicyType.DisableSend, enabled: false)] Policy existingDisableSendPolicy,
+ [Policy(PolicyType.SendOptions, enabled: false)] Policy existingSendOptionsPolicy,
+ SutProvider sutProvider)
+ {
+ postUpsertedPolicy.OrganizationId = policyUpdate.OrganizationId;
+ existingDisableSendPolicy.OrganizationId = policyUpdate.OrganizationId;
+ existingSendOptionsPolicy.OrganizationId = policyUpdate.OrganizationId;
+ postUpsertedPolicy.SetDataModel(new SendControlsPolicyData { DeletionDays = 48 });
+
+ sutProvider.GetDependency()
+ .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.DisableSend)
+ .Returns(existingDisableSendPolicy);
+ sutProvider.GetDependency()
+ .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SendOptions)
+ .Returns(existingSendOptionsPolicy);
+
+ var send1 = new Send
+ {
+ Id = Guid.NewGuid(),
+ CreationDate = DateTime.UtcNow
+ };
+ var send2 = new Send
+ {
+ Id = Guid.NewGuid(),
+ CreationDate = DateTime.UtcNow
+ };
+ var sendIds = new List([ send1.Id, send2.Id ]);
+ sutProvider.GetDependency()
+ .GetIdsByOrganizationIdAsync(policyUpdate.OrganizationId)
+ .Returns(sendIds);
+ sutProvider.GetDependency()
+ .GetManyByIdsAsync(Arg.Any>())
+ .Returns([ send1, send2 ]);
+
+ await sutProvider.Sut.ExecutePostUpsertSideEffectAsync(
+ new SavePolicyModel(policyUpdate), postUpsertedPolicy, null);
+
+ await sutProvider.GetDependency()
+ .Received(1)
+ .UpdateManyDeletionDatesByIdsAsync(Arg.Is(l => l.Count() == 2), 48);
+ }
}
diff --git a/util/Migrator/DbScripts/2026-04-20_01_SendUpdateDeletionDaysByIds.sql b/util/Migrator/DbScripts/2026-04-20_01_SendUpdateDeletionDaysByIds.sql
new file mode 100644
index 000000000000..8087cd09b3fe
--- /dev/null
+++ b/util/Migrator/DbScripts/2026-04-20_01_SendUpdateDeletionDaysByIds.sql
@@ -0,0 +1,27 @@
+CREATE OR ALTER PROCEDURE [dbo].[Send_UpdateDeletionDatesByIds]
+ @Ids AS [dbo].[GuidIdArray] READONLY,
+ @DeletionHours INT
+AS
+BEGIN
+ SET NOCOUNT ON
+
+ -- Set field
+ UPDATE
+ [dbo].[Send]
+ SET
+ [DeletionDate] = DATEADD(HOUR, @DeletionHours, [CreationDate]),
+ [RevisionDate] = GETUTCDATE()
+ WHERE
+ [Id] IN (SELECT * FROM @Ids)
+
+ -- Bump account revision dates
+ EXEC [dbo].[User_BumpManyAccountRevisionDates]
+ (
+ SELECT DISTINCT
+ UserId
+ FROM
+ [dbo].[Send]
+ WHERE
+ [Id] IN (SELECT * FROM @Ids)
+ )
+END
\ No newline at end of file