From xp_sendmail to safe_sendmail
By Bill
Wunder
In the
previous column, “Take
my SQLMail, Please!”, I presented a case for an alternative
for SQL mail. I’m going to continue on the premise that you
found the argument at least somewhat compelling and dive right
in to a discussion of one possible alternative. As always, you
may see things that don’t suit your needs. Feel free to take
what ever you see here that may be useful and add to it or
change it as you see fit to make your SQL Server a better one.
Foremost in
moving from SQLMail, is to keep the switch as easy and painless
as possible. To accomplish this, first consider moving away from
xp_sendmail and then you will have a foundation upon which to
move the SQLAgent’s SQLMail capabilities.
Each existing
application will have some level of usage of xp_sendmail and any
replacement subsystem must minimize sweeping changes necessary
to what already exists to assure a clean transition. At the same
time, it would make sense to decouple the sending of email and
the execution of all application stored procedures and scripts.
Not only will this serve to prevent the application from
breaking when the mail subsystem breaks as happens with
xp_sendmail, it will also allow changes the underlying email
architecture at will without having to make a pass through all
existing T-SQL with each revision in the future, provide an
opportunity to apply system wide rules to all SQL Server
originated email, and generate a sent items history easily
accessible inside SQL Server.
These design
goals can be easily met by creating a stored procedure that
accepts all the parameters of xp_sendmail and inserting those
parameters into a table that will act as a queue. All we need do
to convert applications to the new architecture is replace
xp_sendmail with this new stored procedure name. From the table,
any process or procedure – even xp_sendmail - can easily pick up
rows and send emails asynchronously from the application stored
procedures. For this discussion mail will be processed from the
SQL Agent by a stored procedure name SendAsyncEmail.
Get
procedures/triggers that include a reference to xp_sendmail
exec sp_MSforeachdb 'use ?
Select db_name()
, object_name(id)
from syscomments
where text like ''%xp_sendmail%'''
or if
you want to generate a script so you can review each reference
exec
sp_MSforeachdb 'use ?
Select ''use '' + db_name() +
''exec sp_helptext '' + object_name(id)
from syscomments
where text like ''%xp_sendmail%'''
Of course the
table should also include a column to indicate that a mail needs
to be, has been, or even won’t be sent as well as columns to
indicate who requested the send, when, and any features that I
want to add to my replacement subsystem. The basic table
structure is something like:
use
sysmon
GO
CREATE
TABLE [dbo].[AsyncEmailQueue] (
[EmailId] [int] IDENTITY (1, 1) NOT NULL
,
[RecCreatedDt] [datetime] NOT NULL
CONSTRAINT [DF_AsyncEmailQueue__DateTime]
DEFAULT (getdate())
,
[SentFlag] [int] NOT NULL
CONSTRAINT [DF_AsyncEmailQueue__SentFlag]
DEFAULT (0)
,
[Recipients] [varchar] (512) NULL
,
[Message] [varchar] (2000) NULL
,
[Query] [varchar] (1000) NULL
,
[Attachments] [varchar] (2000) NULL
,
[CopyRecipients] [varchar] (255) NULL
,
[BlindCopyRecipients] [varchar] (255) NULL
,
[Subject] [varchar] (255) NULL
,
[AttachResults] [varchar] (5) NULL
,
[NoHeader] [varchar] (5) NULL
,
[Width] [int] NULL
,
[Separator] [varchar] (255) NULL
,
[DBuse] [varchar] (255) NULL
,
CONSTRAINT [pk_AsycnEmailQueue__EmailId]
PRIMARY KEY CLUSTERED
(
[EmailId]
) ON [PRIMARY]
) ON
[PRIMARY]
I deviate from
my usual modus operandi of always building tools in the
admin database (see my article
http://www.sswug.org/see/14791
T-SQL Tool
House) and build this table and the supporting procedures in
another database because I want this tool to be used by anyone
interested. This is very different from the data and stored
procedures of the admin database which I am not so keen on
opening up to the general developer population. I’ll put this
new email subsystem in a database named sysmon because I want
the developers to use email notification within SQL Server to
help them track, monitor and respond to events and conditions
within the data set. Sysmon then is an acronym for system
monitoring.
The procedure
that will replace xp_sendmail I’ll call safe_sendmail. I could
just replace each call to xp_sendmail with a call to
sysmon.dbo.safe_sendmail and be done with it, but I’m going to
take it a couple of steps farther. I’ll set a standard that
requires an explicit name for each parameter. That sets up a
little more work for me during the conversion, but will make the
new mail subsystem clear in purpose and more self documenting
moving forward. One more thing I will do is convert any
recipient names to be valid SMTP email addresses. That’s right,
no more Address Book Display names.
Note: You can
use the Address book to determine the SMTP name for any
recipient by looking at the “Email Addresses” tab of the
Properties for a user. It’s even possible that your organization
is still stuck in the dark ages of Exchange Server and you’ll
need to motivate a sleepy mail server admin to add SMTP
addresses for all users. Trust me, the
Exchange 2000
documentation and mail server best practices have been
telling them to do so for quite some time.
To illustrate
the changes necessary for each call consider this basic example:
Before:
Exec master.dbo.xp_sendmail
'Bill Wunder'
, 'This is a test'
, 'Just testing, please ignore'
After:
Exec sysmon.dbo.safe_sendmail
@recipients = 'bill.wunder@wallst.com'
, @subject = 'This is a test'
, @mesage = 'Just testing, please ignore'
There are
parameters to xp_sendmail that will not be fully supported by
this asynchronous subsystem. I find they are seldom used and
even less often needed. The safe_sendmail stored procedure will
accept them if specified, I just won’t do anything with them
when I read the table later. The parameters @no_output, and
@echo_error no longer have meaning in an asynchronous mail
delivery model. The @type parameter is MAPI specific. @set_user
may be of value in an environment where column level permissions
are set or security around sensitive data is an issue. Since the
SQLAgent will be executing all queries submitted by
safe_sendmail, all permissions will be in the context of the
SQLAgent service account. Usually this service account is a
member of the sysadmins server role so no query is restricted by
permissions. This is not an important consideration in our
environment so I did not incorporate @set_user into this
solution. Certainly anyone that found value in these parameters
could include the xp_sendmail behavior if necessary and
desirable. See the xp_sendmail documentation in SQL Server Books
on Line for a full description of these parameters. Use your
creative skills to implement them as necessary for your
environment. Here’s what I’d suggest as a starting place for the
safe_sendmail procedure:
use
sysmon
GO
SET
QUOTED_IDENTIFIER ON
GO
SET
ANSI_NULLS ON
GO
CREATE
PROC dbo.safe_sendmail
@recipients varchar(512)
,
@message varchar(2000) = null
,
@query varchar(1000) = null
,
@attachments varchar(2000) = null
,
@copy_recipients varchar(255) = null
,
@blind_copy_recipients varchar(255) = null
,
@subject varchar(255) = 'SQL Server Message'
,
@attach_results varchar(5) = null
,
@no_header varchar(5) = null
,
@width int = null
,
@separator varchar(255) = null
,
@echo_error varchar(5) = null
,
@no_output varchar(5) = null
,
@set_user varchar(255) = null
,
@type varchar(255) = null
,
@dbuse varchar(255) = null
as
/***********************************************************************
*
sysmon.dbo.safe_sendmail
*
*
replacement for xp_sendmail to allow asynchronous email
delivery.
*
**********************************************************************/
set
nocount on
declare
@send_Date datetime
-- warn
about unused parameters if provided
if
@echo_error is not null
print '@echo_error is not supported'
if
@no_output is not null
print '@no_output is not supported'
if
@set_user is not null
print '@set_user is not supported'
if @type
is not null
print '@type is not supported'
--
insert into the asyncronous table
insert
into sysmon.dbo.AsyncEmailQueue
(recipients
,
Message
,
Query
,
Attachments
,
CopyRecipients
,
BlindCopyRecipients
,
Subject
,
AttachResults
,
NoHeader
,
Width
,
Separator
,
DBuse)
values
(@recipients
,
@message
,
@query
,
@attachments
,
@copy_recipients
,
@blind_copy_recipients
,
@subject
,
@attach_results
,
@no_header
,
@width
,
@separator
,
@dbuse)
GO
SET
QUOTED_IDENTIFIER OFF
GO
SET
ANSI_NULLS ON
GO
You’ll need to
make sure every database user that should have the ability to
send emails has adequate permissions to access the safe_sendmail
stored procedure. Much better than having to extend execute
permissions for xp_sendmail to users other than sysadmins. In
fact, check out what Books on Line has to say about permissions
for xp_sendmail.
Now all you
need is a stored procedure that will select from the table and
use the parameters to execute xp_sendmail and you will have an
asynchronous email subsystem. Something like:
use
sysmon
GO
SET
QUOTED_IDENTIFIER ON
GO
SET
ANSI_NULLS ON
GO
CREATE
PROC dbo.SendAsyncEmail
as
/***********************************************************************
*
sysmon.dbo.SendAsyncEmail
*
* call
xp_sendmail from SQLAgent job
*
**********************************************************************/
set
nocount on
declare
@Recipients varchar(512)
,
@Message varchar(2000)
,
@Query varchar(1000)
,
@Attachments varchar(2000)
,
@CopyRecipients varchar(255)
,
@BlindCopyRecipients varchar(255)
,
@Subject varchar(255)
,
@AttachResults varchar(5)
,
@NoHeader varchar(5)
,
@Width int
,
@Separator varchar(255)
,
@NoUutput varchar(5)
,
@DBUse varchar(255)
while
exists (select EmailId
from sysmon.dbo.AsyncEmailQueue
where SentFlag = 0)
begin
select top 1 @Recipients = Recipients
, @Message = Message
, @Query = Query
, @Attachments = Attachments
, @CopyRecipients = CopyRecipients
, @BlindCopyRecipients = BlindCopyRecipients
, @Subject = Subject
, @AttachResults = AttachResults
, @NoHeader = NoHeader
, @Width = Width
, @Separator = Separator
, @DBUse = DBUse
from sysmon.dbo.AsyncEmailQueue
where SentFlag = 0
exec master.dbo.xp_sendmail
@recipients = @Recipients
, @message = @Message
, @query = @Query
, @attachments = @Attachments
, @copy_recipients = @CopyRecipients
, @blind_copy_recipients = @BlindCopyRecipients
, @subject = @Subject
, @attach_results = @AttachResults
, @no_header = @NoHeader
, @width = @Width
, @separator = @Separator
, @dbuse = @DBUse
end
GO
SET
QUOTED_IDENTIFIER OFF
GO
SET
ANSI_NULLS ON
GO
All that’s
left is to add a SQL Agent job that calls the SendAsyncEmail
stored procedure at what ever frequency you deem correct. Once
you have fully implemented the changes suggested in here you
will have achieved two important milestones. First, sending an
email will no longer cause any of your application code to fail
or hang because of problems with SQL Server’s MAPI interface or
your mail client or even the Exchange Server. Second, you will
have put yourself in a position so that you can consider any
email solution you want simply by changing what happens in the
SendAsyncEmail procedure. How you send email from SQL Server
will be independent from your application.
For my
subsystem, the SendAsyncEmail stored procedure is a wrapper for
the very cool xp_smtp_sendmail extended stored procedure
available for free download at
www.sqldev.net.
I don’t think you’ll find an SMTP relay agent better suited to
SQL Server. In the next article I’ll discuss how I have extended
the technology presented in this article to incorporate this
extended stored procedure into the backend component of my email
subsystem. Hope to see you there.
Bill |