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 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
, 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
exec @rc = master.dbo.xp_getfiledetails @ResultFile
-- use attachment or body depending on file size
if (select Size from #filedetails) > 64000
-- attach it
-- 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 createing a potential infinite loop that will eventually choke the network. To prevent this, the table variable get's 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 accomodate 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 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 located in the sswug.org Script Library. 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.