SET ANSI_NULLS ON; GO SET QUOTED_IDENTIFIER ON; GO IF OBJECT_ID(N'dbo.EmailQueue', N'U') IS NULL BEGIN CREATE TABLE dbo.EmailQueue ( EmailQueueID bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_EmailQueue PRIMARY KEY, ToEmail nvarchar(256) NOT NULL, ToName nvarchar(200) NULL, FromEmail nvarchar(256) NOT NULL, FromName nvarchar(200) NULL, Subject nvarchar(300) NOT NULL, PlainTextBody nvarchar(max) NOT NULL, HtmlBody nvarchar(max) NULL, Status nvarchar(30) NOT NULL CONSTRAINT DF_EmailQueue_Status DEFAULT N'Pending', AttemptCount int NOT NULL CONSTRAINT DF_EmailQueue_AttemptCount DEFAULT 0, MaxAttempts int NOT NULL CONSTRAINT DF_EmailQueue_MaxAttempts DEFAULT 5, NextAttemptUtc datetime2 NOT NULL CONSTRAINT DF_EmailQueue_NextAttemptUtc DEFAULT SYSUTCDATETIME(), LastAttemptUtc datetime2 NULL, SentUtc datetime2 NULL, LastError nvarchar(max) NULL, CreatedUtc datetime2 NOT NULL CONSTRAINT DF_EmailQueue_CreatedUtc DEFAULT SYSUTCDATETIME(), UpdatedUtc datetime2 NOT NULL CONSTRAINT DF_EmailQueue_UpdatedUtc DEFAULT SYSUTCDATETIME(), CONSTRAINT CK_EmailQueue_Status CHECK (Status IN (N'Pending', N'Sending', N'Sent', N'Failed')) ); END; GO IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_EmailQueue_StatusNextAttempt' AND object_id = OBJECT_ID(N'dbo.EmailQueue')) CREATE INDEX IX_EmailQueue_StatusNextAttempt ON dbo.EmailQueue(Status, NextAttemptUtc) INCLUDE (AttemptCount, MaxAttempts, CreatedUtc); GO IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_EmailQueue_CreatedUtc' AND object_id = OBJECT_ID(N'dbo.EmailQueue')) CREATE INDEX IX_EmailQueue_CreatedUtc ON dbo.EmailQueue(CreatedUtc); GO CREATE OR ALTER PROCEDURE dbo.EmailQueue_Create @ToEmail nvarchar(256), @ToName nvarchar(200) = NULL, @FromEmail nvarchar(256), @FromName nvarchar(200) = NULL, @Subject nvarchar(300), @PlainTextBody nvarchar(max), @HtmlBody nvarchar(max) = NULL, @MaxAttempts int = 5 AS BEGIN SET NOCOUNT ON; INSERT INTO dbo.EmailQueue ( ToEmail, ToName, FromEmail, FromName, Subject, PlainTextBody, HtmlBody, Status, AttemptCount, MaxAttempts, NextAttemptUtc, CreatedUtc, UpdatedUtc ) VALUES ( @ToEmail, @ToName, @FromEmail, @FromName, @Subject, @PlainTextBody, @HtmlBody, N'Pending', 0, @MaxAttempts, SYSUTCDATETIME(), SYSUTCDATETIME(), SYSUTCDATETIME() ); SELECT CAST(SCOPE_IDENTITY() AS bigint); END; GO CREATE OR ALTER PROCEDURE dbo.EmailQueue_DequeueBatch @BatchSize int = 10 AS BEGIN SET NOCOUNT ON; IF @BatchSize IS NULL OR @BatchSize <= 0 SET @BatchSize = 10; ;WITH NextEmails AS ( SELECT TOP (@BatchSize) * FROM dbo.EmailQueue WITH (UPDLOCK, READPAST, ROWLOCK) WHERE Status = N'Pending' AND NextAttemptUtc <= SYSUTCDATETIME() AND AttemptCount < MaxAttempts ORDER BY CreatedUtc, EmailQueueID ) UPDATE NextEmails SET Status = N'Sending', LastAttemptUtc = SYSUTCDATETIME(), UpdatedUtc = SYSUTCDATETIME() OUTPUT inserted.EmailQueueID, inserted.ToEmail, inserted.ToName, inserted.FromEmail, inserted.FromName, inserted.Subject, inserted.PlainTextBody, inserted.HtmlBody, inserted.Status, inserted.AttemptCount, inserted.MaxAttempts, inserted.NextAttemptUtc, inserted.LastAttemptUtc, inserted.SentUtc, inserted.LastError, inserted.CreatedUtc, inserted.UpdatedUtc; END; GO CREATE OR ALTER PROCEDURE dbo.EmailQueue_MarkSending @EmailQueueID bigint AS BEGIN SET NOCOUNT ON; UPDATE dbo.EmailQueue SET Status = N'Sending', LastAttemptUtc = SYSUTCDATETIME(), UpdatedUtc = SYSUTCDATETIME() WHERE EmailQueueID = @EmailQueueID AND Status = N'Pending' AND AttemptCount < MaxAttempts; END; GO CREATE OR ALTER PROCEDURE dbo.EmailQueue_MarkSent @EmailQueueID bigint AS BEGIN SET NOCOUNT ON; UPDATE dbo.EmailQueue SET Status = N'Sent', SentUtc = SYSUTCDATETIME(), LastError = NULL, UpdatedUtc = SYSUTCDATETIME() WHERE EmailQueueID = @EmailQueueID; END; GO CREATE OR ALTER PROCEDURE dbo.EmailQueue_MarkFailed @EmailQueueID bigint, @LastError nvarchar(max) AS BEGIN SET NOCOUNT ON; DECLARE @Now datetime2 = SYSUTCDATETIME(); UPDATE dbo.EmailQueue SET AttemptCount = AttemptCount + 1, LastAttemptUtc = @Now, LastError = @LastError, Status = CASE WHEN AttemptCount + 1 >= MaxAttempts THEN N'Failed' ELSE N'Pending' END, NextAttemptUtc = CASE WHEN AttemptCount + 1 >= MaxAttempts THEN NextAttemptUtc WHEN AttemptCount + 1 = 1 THEN DATEADD(minute, 1, @Now) WHEN AttemptCount + 1 = 2 THEN DATEADD(minute, 5, @Now) ELSE DATEADD(minute, 15, @Now) END, UpdatedUtc = @Now WHERE EmailQueueID = @EmailQueueID; END; GO CREATE OR ALTER PROCEDURE dbo.EmailQueue_ResetStuckSending AS BEGIN SET NOCOUNT ON; DECLARE @Now datetime2 = SYSUTCDATETIME(); DECLARE @Cutoff datetime2 = DATEADD(minute, -10, @Now); UPDATE dbo.EmailQueue SET Status = CASE WHEN AttemptCount >= MaxAttempts THEN N'Failed' ELSE N'Pending' END, NextAttemptUtc = CASE WHEN AttemptCount >= MaxAttempts THEN NextAttemptUtc ELSE @Now END, UpdatedUtc = @Now WHERE Status = N'Sending' AND LastAttemptUtc IS NOT NULL AND LastAttemptUtc < @Cutoff; END; GO