From xp_sendmail to xp_smtp_sendmail
By
Bill Wunder
Earlier, in
the article “Take my
SQLMail, Please!”, I presented a case for an alternative for
SQL mail. Then I described step one of a strategy for moving
away from the frailties xp_sendmail by decoupling application
code from the actual sending of mail in the article “From
xp_sendmail to safe_sendmail”. After this first step is
completed we have an environment where there are not hundreds of
places in the application that must be changed in order to
change how email is processed from the SQL Server. Just like in
the movie "What about Bob" we must recognize that this is a baby
step and we must move on to the next step. Now I would like to
explore the business of replacing the asynchronous call to
xp_sendmail with something else. The something else I’ll be
exploring is the extended store procedure xp_smtp_sendmail
available as a free download from
www.sqldev.net,
but the general approach can be applied to any alternative most
appropriate for a given development shop or solution.
I added some
additional parameters to safe_sendmail procedure and to the
asynchronous email queue table as presented in to “From
xp_sendmail to safe_sendmail” to better exploit the features of
xp_smtp_sendmail. First off, I am able to specify the file name
when I attach results so I provided a parameter for the
developer to name the attachment file. Cool eh? But there’s
more! xp_smtp_sendmail supports an email from address, email
from name, and even a reply to address. These parameters allow
the sender’s email address and display name to be customized and
for the reply to be sent to a person or distribution list rather
than just getting thrown back into the inbox on the SQL Servers
Outlook client when someone hits “Reply to All” for an email
sent from the SQL Server. xp_smtp_sendmail even allows me to set
the priority of the email, any of high, medium, or low so that
the urgency of a mail can be determined upon receipt. All useful
features that make an email from the SQL Server better for my
environments.
There are
actually some capabilities of xp_smtp_sendmail that I do not
need so my subsystem does not support them. Others may find
these features irresistible and would therefore want to
incorporate them into their solution. xp_smtp_sendmail can send multiple attachments, specify an alternate port on the
SMTP server, define the codepage for attachments, and even send
as html rather than the default plain text format. You’ll want
to review the documentation of this or any other SMTP agent you
decide to use to help determine the possibilities and which of
those possibilities you want to go after for your safe_sendmail.
One thing that
xp_smtp_sendmail doesn’t do is execute a query. It has the
ability to include a file attachment, even multiple attachments,
or it can include the contents of a file up to 64K in size in
the body of the mail. That’s half way there but I still needed
to get the query results to a file so I can let the extended
stored procedure either attach or embed them in an email as
appropriate. One truth about human nature motivates me to make
this work: It’s much more likely that the recipient will read
the body of an email than open an attachment. It is worthwhile
to briefly cover how I did this because it highlights the
requirements when the subsystem needs some file server storage
space where it can generate result files.
The
SendAsyncEmail procedure uses a trusted osql call via
xp_cmdshell to move the results of a query to a text file. Once
the file is created the undocumented xp_getfiledetails extended stored procedure is used to determine
the size of the result file – only if @attach_results is not set
to true. If the xp_getfiledetails indicate that the file is less
than 64K when combined with any @message text specified it will
embed the results in the email body. If that size is more than
64K the results are always attached. I find xp_getfiledetails
useful in many situations, so I'll show you a chopped down
example of how xp_getfiledetails can be used to get information
about a query result file. You can use this extended stored
procedure to get the same details for any file that the SQL
Server can access.
--
define a container for xp_getfiledetails results
create
table #filedetails
(AlternateName varchar(33)
,
Size int
,
CreationDate int
,
CreationTime int
,
LastWrittenDate int
,
LastWrittenTime int
,
LastAccessedDate int
,
LastAccessedTime int
,
Attributes int)
-- build
on osql string to move query results to a file
select
@SQLStr = 'osql -E -Q"' + @Query + '" -o"' + @ResultFile + '"'
exec @rc
= master.dbo.xp_cmdshell @SQLStr
-- now
see how big the results file is
insert
#filedetails
exec @rc
= master.dbo.xp_getfiledetails @ResultFile
-- use
attachment or body depending on file size
if
(select Size from #filedetails) > 64000
-- attach it
else
-- put it in the body
Another nuance
of SendAsyncEmail that may or may not be of interest and use in
other environments is it’s built in governor. Seems in my shop,
particularly in the development environments, there is an
inadvertent tendency to generate hundreds or even thousands of
the same email request on occasion. Since I use the same SMTP
server for development and production SQL originated email I
have included code to avoid bringing the SMTP server to it’s
knees when this happens. In fact, I have set the limit at 10
emails. So if SendAsyncEmail sees over 10 pending requests to
send an email with the exact same subject to the exact same
recipient list the procedure will take those request out of the
queue and send a designated default recipient a single email
with the subject, recipient list and the count requests
attempted. The SMTP server is not swamped and I still have a way
to follow up on the requests. I have the SQLAgent job that calls
SendAsyncEmail scheduled to run once a minute so the threshold
of 10 emails works pretty good for me even though the developers
think it should be higher so they can send an email in each
iteration of a loop (no way!).
Yet another
subtlety of SendAsyncEmail is the use of a table variable to
identify which emails will be sent for the current invocation of
the procedure. In he more primitive example presented in the
last article, the procedure simple set up a loop to get the next
email that hadn't been sent and send it until there were no more
emails to send. This leaves open the possibility that another
process is inserting new rows into the table as fast as they are
being sent creating a potential infinite loop that will
eventually choke the network. To prevent this, the table
variable gets the emails that need to be sent with a single
query to AsyncEmailQueue and then processes on that guaranteed
to be static list.
I also found
that I needed to go to great lengths to accommodate carriage
returns, line feeds, tabs and single quotes/apostrophes in the
message text in order to keep the developers at least somewhat
pleased.
If you’d like
to try out this SMTP replacement for SQLMail download and
install the .dll containing xp_smtp_email from www.sqldev.net.
Be sure to verify that the extended stored procedure is able to
successfully send and email through the your SMTP server of
choice before proceeding with the safe_sendmail configuration.
The easy details on installing and using this extended stored
procedure can also be found on the site. I want to re-emphasize:
make sure xp_smtp_sendmail is properly working through an SMTP
server BEFORE installing the other components presented here.
After you’ve
successfully sent a simple test email, create a database named
sysmon and compile the
scripts for table AsyncEmailQueue and the stored procedures
safe_sendmail and SendAsyncEmail. Finally, create a SQL Agent job that calls
SendAsyncEmail. In our shop the SQL Agent job runs once a
minute. In the job you will need to identify the SMTP server
that you will use to send your emails, the UNC or local file
system location where you will place attachments, and also
specify a default recipient mailbox for all emails as parameters
to SendAsyncEmail. Make sure you take steps to prevent the file
system location from filling over time. The default recipient
helps insure that all email request have someone to send the
email to and also provides a recipient for any error handling
messages within the procedure.
OK! I’ll give
you a few days to get this piece up and running, then I’ll be
back with a method for making your SQL Agent job notifications
and alerts better by using safe_sendmail rather than the
invisible SQLMail interface. See you then.
Bill |