SQL Server 2005 Database Mail Failures - sql

I am running database mail on a SQL 2005 box. Occasionally mails fail to send, by quering the msdb.dbo.sysmail_mailitems table i can see there are items with a sent_status of "2", which is failed. I can query the sysmail_faileditems table to list all failed mails.
Is there anyway i can process/re-send these failed mail's?
Would it be reasonable to create a daily job to query this table looping through using a CURSOR to re-send the mails one by one, and then delete them from the table one by one.
If you have a better suggestion / ideas then please let me know.
Many thanks Karl

First up, i suggest you query faileditems to determine your main cause of failure:
SELECT items.subject ,
items.last_mod_date ,
l.description
FROM dbo.sysmail_faileditems AS items
INNER JOIN dbo.sysmail_event_log AS l ON items.mailitem_id = l.mailitem_id
If it's nothing that can be easily fixed, you can re-send them by looping through the sysmail_mailitems table and re-sending them based on the failure type (timeouts etc) in the faileditems log - some good examples in the suggestions of this blog: http://justgeeks.blogspot.co.uk/2007/05/resending-sysmail-emails.html
My personal favourite:
CREATE PROCEDURE sysmail_resend_timeout
AS
BEGIN
SET NOCOUNT ON
DECLARE SYSMAIL_LOG_RESEND_CURSOR CURSOR READ_ONLY
FOR
SELECT DISTINCT
l.mailitem_id ,
p.name ,
m.recipients ,
m.subject ,
m.body_format ,
m.body
FROM msdb.dbo.sysmail_log l WITH ( NOLOCK )
JOIN msdb.dbo.sysmail_mailitems m WITH ( NOLOCK ) ON m.mailitem_id = l.mailitem_id
JOIN msdb.dbo.sysmail_profile p WITH ( NOLOCK ) ON p.profile_id = m.profile_id
WHERE l.event_type = 3
AND m.sent_status = 2
AND l.description LIKE '%The operation has timed out%'
ORDER BY l.mailitem_id
OPEN SYSMAIL_LOG_RESEND_CURSOR
WHILE ( 1 = 1 )
BEGIN
DECLARE #mailitem_id INT ,
#profile_name NVARCHAR(128) ,
#recipients VARCHAR(MAX) ,
#subject NVARCHAR(255) ,
#body_format VARCHAR(20) ,
#body NVARCHAR(MAX)
FETCH NEXT FROM SYSMAIL_LOG_RESEND_CURSOR INTO #mailitem_id, #profile_name, #recipients, #subject, #body_format, #body
IF NOT ##FETCH_STATUS = 0
BEGIN
BREAK
END
PRINT CONVERT(VARCHAR, GETDATE(), 121) + CHAR(9) + CONVERT(VARCHAR, #mailitem_id) + CHAR(9) + #recipients
EXEC msdb.dbo.sp_send_dbmail
#profile_name = #profile_name ,
#recipients = #recipients ,
#subject = #subject ,
#body_format = #body_format ,
#body = #body
UPDATE msdb.dbo.sysmail_mailitems
SET sent_status = 3
WHERE mailitem_id = #mailitem_id
END
CLOSE SYSMAIL_LOG_RESEND_CURSOR
DEALLOCATE SYSMAIL_LOG_RESEND_CURSOR
END
GO

I know it's not really the answer you want to hear, but I always try and decouple the mail feature. I might use a trigger to spawn an external process if the mail sending needs to be timely, but I let the external script do the actual job of sending the mail. That way transient connection errors are taken care of by the MTA, and I don't have to worry about special book-keeping algorithms.

Related

Changed Sql-Server version, can't creat job with alert to send e-mail

In the database i manage, we have some internal dossiers that have a dead line, stablished by us on the date of creation. Now, i need to receive an e-mail, telling me which one have terminated.
I have created a simple job with code that would do it all, no go. I have created an alert, no go, always getting errors or, when not geting errors, nothing happens anyway.
select *
from bo
where nmdos like '%preço%'
and datafinal = DATEADD(day, -1, convert(date, GETDATE()))
i need to create an alert that, when the code returns the name and the client number on that dossier
You could create stored procedure to handle the query check, which would check if any rows are returned. If there are rows returned then send an email. This can be achieved by the below. Assuming you have dbsend mail configured in your SQL instance.
CREATE PROCEDURE dbo.DossierEmailSend
AS
DECLARE #rows int;
DECLARE #message varchar(1000);
SET #rows = (SELECT COUNT(*)
FROM bo
WHERE nmdos LIKE '%preço%'
AND datafinal = DATEADD(day, -1, CONVERT(date, GETDATE()))
)
SET NOCOUNT ON
SET #message = '<HTML>As at ' + CONVERT(char(19),GETDATE(),120) + '<BR><BR>DOSSIER FOUND'
IF #rows > 0
BEGIN
EXEC dbo.uspSendEmail 'FOUND DOSSIER', 'YOUR EMAIL', #message, NULL, 'CC EMAIL 1; CC EMAIL 2'
SET NOCOUNT OFF
END
You can then create a job within SQL Agent that will execute the check on whatever frequency you require.
If you are missing the send mail sp you can create it:
CREATE proc [dbo].[uspSendEmail]
#subject nvarchar(max),
#to nvarchar(max),
#body nvarchar(max),
#file nvarchar(max)='',
#cc nvarchar(max)='',
#bcc nvarchar(max)='',
#query nvarchar(max)='',
#attach_query_result_as_file tinyint=0,
#query_attachment_filename nvarchar(max)=''
as
EXEC msdb.dbo.sp_send_dbmail #profile_name = NULL,
#body_format = 'HTML',
#copy_recipients = #cc,
#blind_copy_recipients = #bcc,
#recipients = #to,
#body = #body,
#subject = #subject,
#file_attachments = #file,
#query = #query,
#attach_query_result_as_file = #attach_query_result_as_file,
#query_attachment_filename = #query_attachment_filename
return 0

how I can resend all failed emails (msdb.dbo.sp_send_dbmail)

I am sending emails using msdb.dbo.sp_send_dbmail.
Sometimes the emails do not send. I have got the list of emails which have failed.
SELECT TOP 10 * from msdb.dbo.sysmail_event_log
log_id event_type log_date process_id mailitem_id account_id last_mod_date last_mod_user
9022 error 50:15.9 5608 20428 NULL 50:15.9 sa
9023 error 51:23.3 5608 20428 NULL 51:23.3 sa
Now, I want to resend all failed emails again which are available in the sysmail_event_log table.
How can I resend all failed emails?
Use the following query for sending back a failed item.
Or use a CURSOR for each row from msdb.dbo.sysmail_faileditems with the same query
DECLARE #to varchar(max)
DECLARE #copy varchar(max)
DECLARE #title nvarchar(255)
DECLARE #msg nvarchar(max)
SELECT #to = recipients, #copy = copy_recipients, #title = [subject], #msg = body
FROM msdb.dbo.sysmail_faileditems
WHERE mailitem_id = 56299
EXEC msdb.dbo.sp_send_dbmail
#recipients = #to,
#copy_recipients = #copy,
#body = #msg,
#subject = #title,
#body_format = 'HTML';
References
resending-failed-emails-through-sp_send_email
CURSOR
Building from Hybris95's answer, here is a snippet that sends all failed items after a cutoff timestamp without a cursor. Mail profiles are taken in consideration.
DECLARE #MailitemId INT = 0
DECLARE #Cutoff DATE = CAST(GETDATE() AS DATE)
WHILE (1 = 1)
BEGIN
SELECT TOP 1 #MailitemId = mailitem_id
FROM msdb.dbo.sysmail_faileditems
WHERE
mailitem_id > #MailitemId
AND send_request_date > #Cutoff
ORDER BY mailitem_id
IF ##ROWCOUNT = 0 BREAK;
DECLARE #to VARCHAR(MAX)
DECLARE #copy VARCHAR(MAX)
DECLARE #title NVARCHAR(255)
DECLARE #msg NVARCHAR(MAX)
DECLARE #profile_name NVARCHAR(MAX),
#file_attachments NVARCHAR(MAX),
#attach_query_result_as_file BIT
SELECT #profile_name = p.name, #to = recipients, #copy = copy_recipients, #title = [subject], #msg = body, #file_attachments = i.file_attachments, #attach_query_result_as_file = i.attach_query_result_as_file
FROM msdb.dbo.sysmail_faileditems AS i LEFT OUTER JOIN msdb.dbo.sysmail_profile AS p
ON p.profile_id = i.profile_id
WHERE
mailitem_id = #MailitemId
EXEC msdb.dbo.sp_send_dbmail
#profile_name = #profile_name,
#recipients = #to,
#copy_recipients = #copy,
#body = #msg,
#subject = #title,
#body_format = 'HTML',
#file_attachments = #file_attachments,
#attach_query_result_as_file = #attach_query_result_as_file
END
Or you can do what send_dbmail itself does after building the message in source tables (which still exists in case of failed mail), and just push the failed message back onto the service broker send queue, and let it manage all the correct updates.
Declare #sendmailxml VARCHAR(max), #mailitem_id INT
-- Create the primary SSB xml maessage
SET #sendmailxml = '<requests:SendMail xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://schemas.microsoft.com/databasemail/requests RequestTypes.xsd" xmlns:requests="http://schemas.microsoft.com/databasemail/requests"><MailItemId>'
+ CONVERT(NVARCHAR(20), #mailitem_id) + N'</MailItemId></requests:SendMail>'
-- Send the send request on queue.
EXEC sp_SendMailQueues #sendmailxml
where #mailitem_id is the id of the message to resend that you have identified.
Just be aware the exact format of the XML Message is an undocumented internal, so may change by SQL version. The indented lines (including misspelled comment!) are pulled straight out of sp_send_dbmail on a SQL 17 server, and are same on 19. I believe they were different on some earlier version, but don't have installs to check.

T-SQL query result into variable

I am trying to assign the result of a SQL query into a variable but running into issues.
Here is my query:
use db
SELECT name
,ID
,pID
,pName
,group
FROM mName as m
INNER JOIN pID_Check AS p ON m.ID= p.pID
WHERE p.pName NOT LIKE m.name
Query works fine however I'm trying to schedule it to run hourly via SQL Server Agent and email on results.
Most of the time the query will not return any data but in the event there are rows I need it to email out.
Essentially need values of the row added to #results variable and email triggered only if #results not null
SET #sub = 'Subject'
EXEC msdb.dbo.sp_send_dbmail
#profile_name = 'Profile',
#recipients = 'email#email.com;',
#body = #results,
#subject = #sub
You seem to want to send the result set to a mail recipient - while that is possible, I'm not aware of being possible to stick into the body in the method which you have attempted.
It can be included as an attachment via another property of SP_SEND_DBMAIL. Please see your example below, modified to include the result set as an attachment:
DECLARE #myquery varchar(max) = '
SELECT name
,ID
,pID
,pName
,group
FROM mName as m
INNER JOIN pID_Check AS p ON m.ID= p.pID
WHERE p.pName NOT LIKE m.name
'
SET #sub = 'Subject'
EXEC msdb.dbo.sp_send_dbmail
#profile_name = 'Profile',
#recipients = 'email#email.com;',
#body = #results,
#subject = #sub,
#query = #myquery,
#attach_query_result_as_file = 1,
#query_attachment_filename= 'MyFileName.csv';
All SP_SEND_DBMAIL properties can be seen here: https://learn.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-send-dbmail-transact-sql

Multiple table SQL trigger an email to update

Ok, much closer here for sure, but now it sends mass amounts of emails on one record update? I know I'm missing something here. First shot at cursors, so I'm guessing I've done something wrong in there? As always, Thanks for the help!!
create trigger eMailScheduleChange on dbo.BOOKINGS after update as
set nocount on;
Declare EmailCursor Cursor read_only for
select r.Name as RName
, c.Name as CName
, i.BookingTypeId
, i.Start
, i.Finish
, r.Email
from Wallchart.dbo.BOOKINGS b
inner join Wallchart.dbo.CUSTOMERS c on b.CustomerId = c.CustomerId
inner join Wallchart.dbo.RESOURCES r on b.ResourceId = r.ResourceId
inner join Inserted i on i.CustomerId = b.CustomerId
where i.BookingTypeId <> b.BookingTypeId
Declare #Email as varchar(50)
Declare #CName as varchar(100)
Declare #Start as datetime
Declare #RName as varchar (100)
Declare #Finish as datetime
Declare #body as varchar (255)
Declare #BookingTypeId as varchar (50)
open EmailCursor
Fetch next from EmailCursor
INTO #Email, #CName, #Start, #RName, #Finish, #BookingTypeId
While ##FETCH_STATUS=0
BEGIN
Set #body = '<Account cancelled>' + #CName + #Start + #Finish
EXEC msdb.dbo.sp_send_dbmail
#profile_name = 'SQLMail',
#recipients = #Email,
#subject = 'Account Update',
#body = #body
FETCH NEXT FROM EmailCursor INTO #Email, #CName, #Start, #RName, #Finish, #BookingTypeId
SET NOCOUNT OFF
END
Close EmailCursor
Deallocate EmailCursor
I think perhaps you have the impression that a query in a trigger is not allowed to read data from another table. This is not the case or triggers would be fairly useless. Here is a stub of how your trigger might look. I demonstrated how to pull the information you want. From there it is simply calling sp_send_dbmail.
create trigger MyTrigger on dbo.BOOKINGS for update as
set nocount on;
select r.Name
, c.Name
, i.BookingTypeId
, i.Start
, i.Finish
, r.Email
from Schedule.dbo.BOOKINGS b
inner join Wallchart.dbo.CUSTOMERS c on b.CustomerId = c.CustomerId
inner join Schedule.dbo.RESOURCES r on b.ResourceId = r.ResourceId
inner join Inserted i on i.SomeKeyValue = b.SomeKeyValue
where i.BookingTypeID <> b.BookingTypeID
--Now you can see how to capture all the information. All that is left is to call sp_send_dbmail
--https://msdn.microsoft.com/en-us/library/ms190307.aspx
SQL Server does not support direct mailing from the scripts. But you have other options.
First Create a table to capture the records that are being changed and also set a EmailFlag so that you can identify whether or not you have notified that user about the change.
The create a store procedure that will give you the list of all users that has undergone some changes but not notified by email (EmailFlag ="N" or something like that)
Now use the SSIS to create a send mail task to send the details email for all these users.
Once you have sent the email for each user, then you can update the EmailFlag as "Y" for that record. You can make the SSIS package a scheduled job in the SQL Server so that it will automatically run between certain interval.
You can use a for each loop container in SSIS so that you can send mail individually to each recipient
Please refer the following Links for more details
SSIS - How to configure a send mail task

Trouble turning a Simple MsSQL select into a monitor

I have a SQL Statement that works as I want.
select COUNT(*), MIN(emailed_to)from email.email_archive
group by emailed_to
order by COUNT(*) desc
The output looks like this.
13 deadlockIE12388nnhy32#hepmeplease.com;
8 deadlockIE1277yhygt#hepmeplease.com;
4 deadlockFF17uyt9xx967#hepmeplease.com;
...
...
...
1 deadlockFF17uytsdfa7#hepmeplease.com;
This is simple enough, but then I would have to remember to run the select every day and make sure things are ok. I want to have the stored procedure email me once in a while and I can decide if I have an issue. So I have hacked together the following from many resources:
use MYDB;
go
IF SCHEMA_ID('monitors') IS NULL EXECUTE('CREATE SCHEMA monitors AUTHORIZATION dbo')
GO
if object_id('monitors.email_abuse') is null
exec('create procedure monitors.email_abuse as print ''stub'' return');
GO
alter procedure monitors.email_abuse
(#to varchar(max) = 'itops#hepmeplease.com',
#sendemail tinyint = 1)
as
set nocount on ;
set transaction isolation level read uncommitted ;
begin try
declare #errmsg varchar(max) = '',
#subject nvarchar(255);
select #subject = 'Run Away Email Monitor';
select #errmsg = REPLICATE(char(10),1)+
'# of Emails'+
REPLICATE(char(9),1)+
'Email Address'+
REPLICATE(CHAR(10),1);
select #errmsg = #errmsg +REPLICATE(char(9),1)+
CAST(COUNT(*) as CHAR(10))+
REPLICATE(char(9),1)+
CAST(MIN(emailed_to) as CHAR(45))
from
email.email_archive
group by
emailed_to
order by
COUNT(*) desc;
print #errmsg;
if #sendemail = 1
begin
exec master.dbo.sp_email
#to = #to,
#subject = #subject,
#body = #errmsg;
end
end try
begin catch
-- unexpected errors
exec sp_raise_error #rethrow = 1, #textdata = N'Error in monitors.email_abuse', #emailTo = N'itops#hepmeplease.com'
end catch
go
But it then emails me the following output that is just one line. I know that there are many lines but for some reason when I put the COUNT(*), MIN(emailed_to) into the CAST statement this no longer works. I get a email that has the header and one line. If I just print the output of #errmsg I exactly what I get in the email, the header and the one line. just like below.
# of Emails Email Address
1 y#y.com;
I am not sure what I am doing wrong with my cast statement.
Explanation:
My guess is that the code you are actually using is slightly different from the code you have posted here because when I take your code and the following data in a test database, things work out fine.
create table email_archive
(
id int,
emailed_to nvarchar(255)
)
insert into email_archive values
( 1, 'one#helpme.com'), ( 2, 'two#helpme.com'), ( 3, 'three#helpme.com'),
( 4, 'four#helpme.com'), ( 5, 'one#helpme.com'), ( 6, 'two#helpme.com'),
( 7, 'three#helpme.com'), ( 8, 'four#helpme.com'), ( 9, 'one#helpme.com'),
(10, 'two#helpme.com'), (11, 'three#helpme.com'), (12, 'four#helpme.com'),
(13, 'one#helpme.com'), (14, 'two#helpme.com'), (15, 'three#helpme.com'),
(16, 'four#helpme.com'), (17, 'one#helpme.com'), (18, 'one#helpme.com'),
(19, 'one#helpme.com'), (20, 'three#helpme.com'), (21, 'three#helpme.com')
I am thinking you may have hit upon an issue discussed here: http://bit.ly/cMlnjt
Since I can't be sure I offer you two alternative solutions that will definitely get the job done, even though as others have mentioned this aggregate concatenation should work without an issue.
Alternatives:
To get what you are looking for, I prefer one of the following two options
1) Just make sp_send_dbmail do the work for you.
2) Go with a cursor solution
Option 1:
EXEC msdb..sp_send_dbmail #profile_name = 'MyMailProfile',
#recipients = 'my_email#domain.com',
#subject = 'Runaway Email Monitor',
#body = 'Runaway emails found',
#query = 'SELECT COUNT(*), emailed_to FROM mydb.dbo.email_archive GROUP BY emailed_to HAVING COUNT(*) > 5 ORDER BY COUNT(*) DESC'
Note: The having clause makes this only display rows where the count is greater than 5.
Option 2:
USE test
IF EXISTS ( SELECT name FROM test.sys.sysobjects WHERE type = 'P' AND name = 'usp_MonitorEmails' )
BEGIN
DROP PROCEDURE dbo.usp_MonitorEmails
END
GO
CREATE PROCEDURE usp_MonitorEmails
#Subject nvarchar(255) = '',
#Importance varchar(6) = 'NORMAL',
#Sensitivity varchar(12) = 'NORMAL',
#Recipients varchar(MAX) = NULL,
#MinimumCount int = 0
AS
BEGIN
SET NOCOUNT ON
IF UPPER(#Importance) NOT IN ('LOW', 'NORMAL', 'HIGH') SET #Importance = 'NORMAL'
IF UPPER(#Sensitivity) NOT IN ('NORMAL', 'PERSONAL', 'PRIVATE', 'CONFIDENTIAL') SET #Sensitivity = 'NORMAL'
DECLARE #run bit,
#message nvarchar(MAX)
SELECT #run = 0,
#subject = 'Run Away Email Monitor',
#message = 'Run away emails found' + CHAR(13)+CHAR(10) +
'Count Email Address' + CHAR(13)+CHAR(10) +
'----------- ------------------------------------------------------------------------------' + CHAR(13)+CHAR(10)
DECLARE #count int,
#email nvarchar(255)
DECLARE BodyCursor CURSOR STATIC FOR
SELECT COUNT(*), emailed_to FROM email_archive GROUP BY emailed_to HAVING COUNT(*) > #MinimumCount ORDER BY COUNT(*) DESC
OPEN BodyCursor
FETCH NEXT FROM BodyCursor
INTO #count, #email
WHILE ##FETCH_STATUS = 0
BEGIN
SELECT #message = #message + REPLICATE(N' ', 11-LEN(CAST(#count AS nvarchar(22)))) + CAST(#count AS nvarchar(22)) + ' ' + #email + CHAR(13)+CHAR(10), #run = 1
FETCH NEXT FROM BodyCursor
INTO #count, #email
END
CLOSE BodyCursor
DEALLOCATE BodyCursor
IF #run = 1 AND LEN(#Recipients) > 0
BEGIN
EXEC msdb..sp_send_dbmail #profile_name = 'MyMailProfile',
#recipients = #Recipients,
#subject = #Subject,
#body = #Message,
#body_format = 'TEXT',
#importance = #Importance,
#sensitivity = #Sensitivity
END
END
GO
Note: I prefer this method because of the flexibility I have in the way the messages are formatted. This will also only send the email if there are rows returned where the minimum count is reached.
You will only get one value because you have no loop through the recordset containing the emails. You will need to use a CURSOR to traverse the set and build up your #errmsg variable with the contents of each record.
Nimble SQL jockeys will probably point out you could avoid the CURSOR with the correct kind of join in the query but I always find that harder than this method and if it doesn't have to be high performance it does the job.
Here's some example code;
DECLARE #myGlobalVar nvarchar(255);
DECLARE #myVarCol1 nvarchar(10);
DECLARE #myVarCol2 nvarchar(10);
DECLARE myCursor CURSOR FOR
SELECT col1, col2
FROM table1
WHERE wanted = 1
ORDER BY col1;
OPEN myCursor;
FETCH NEXT FROM myCursor
INTO #myVarCol1, #myVarCol2;
WHILE ##FETCH_STATUS = 0
BEGIN
SELECT #myGlobalVar = #myGlobalVar + ' ' + #myVarCol1 + ' ' + #myVarCol2;
FETCH NEXT FROM myCursor
INTO #myVarCol1, #myVarCol2;
END
CLOSE vendor_cursor;
DEALLOCATE vendor_cursor;
Always remember to FETCH NEXT inside the loop unless you want to have to kill the process! You should always CLOSE and DEALLOCATE the cursor too or you'll run out of memory eventually.
I've had similar issues when trying to use this approach to concatenation in the past. Often some messing around with the SELECT resolves it. The FOR XML approach to concatenation is more robust.
I tried to reproduce the issue here though and couldn't (it worked correctly for me and output the 2 different email addresses in the concatenation).
Does this work for you? If so maybe compare the execution plans and see what's different.
set nocount on
create table #email_archive
(
emailed_to CHAR(45)
)
insert into #email_archive
select 'deadlockIE12388nnhy32#hepmeplease.com;' union all
select 'deadlockIE12388nnhy32#hepmeplease.com;' union all
select 'deadlockIE12388nnhy32#hepmeplease.com;' union all
select 'deadlockIE12388nnhy32#hepmeplease.com;' union all
select 'deadlockIE12388nnhy32#hepmeplease.com;' union all
select 'deadlockIE12388nnhy32#hepmeplease.com;' union all
select 'deadlockIE12388nnhy32#hepmeplease.com;' union all
select 'deadlockIE12388nnhy32#hepmeplease.com;' union all
select 'deadlockIE1277yhygt#hepmeplease.com;'
declare #errmsg varchar(max) = '',
#subject nvarchar(255) = 'Run Away Email Monitor';
select #errmsg = REPLICATE(char(10),1)+
'# of Emails'+
REPLICATE(char(9),1)+
'Email Address'+
REPLICATE(CHAR(10),1);
select #errmsg = #errmsg +REPLICATE(char(9),1)+
CAST(COUNT(*) as CHAR(10))+
REPLICATE(char(9),1)+
CAST(MIN(emailed_to) as CHAR(45))
from
#email_archive
group by
emailed_to
order by
COUNT(*) desc;
print #errmsg;
drop table #email_archive;
The answers above were all great answers..but the simplest of all came from Mike Talley overnight.
select #errmsg = #errmsg +REPLICATE(char(9),1)+
CAST(COUNT(*) as CHAR(10))+
REPLICATE(CHAR(9),1)+
cast(substring(emailed_to, 1, 45) as char(45))+
REPLICATE(CHAR(10),1)
Once I dropped the MIN and added in the substring the monitor worked like a charm..