Programmatically setting the SmtpClient pickup directory location at runtime
In my last post on using an smtp pickup directory for ASP.NET development, I explained some of the reasons to use a pickup folder instead of an SMTP server during ASP.NET development. One of the caveats of that configuration is that you have to enter the full path of the folder you want to store the application's mail in. This is fine if you're working solo, or can agree on a common folder/path use amongst your colleagues but it reduces the instant portability of your application, since you'll have the full path of the folder set in the web.config.
You can always use the
configSource
property for your web.config/system.net
configuration element, and
specify the 'per machine details' in a separate file which isn't source
controlled. Not a bad idea.
However, it still doesn't solve the 'portability' issue. ASP.NET 2.0 (Visual Studio 2005) introduced the new Web Project model, where you can simply open a folder from the file system as a website and start coding. You can copy or move the folder to another location, or another machine, and open the folder in VS2005, and once again you're away.
With that in mind, configuring the SpecifiedPickupDirectory PickupDirectoryLocation as an absolute path (which is required by the framework) isn't the best solution as it ties the mail drop folder to a fixed path and you'll have to reconfigure if you move your project.
Changing the mailSettings through the configuration model
It's not entirely straight forward to change the location of the pickup folder at runtime. Initially, I thought of using the frameworks built-in functionality to change the application's mailSettings. Which looks something like this:
Configuration config = WebConfigurationManager.OpenWebConfiguration("~/web.config");
MailSettingsSectionGroup mail = (MailSettingsSectionGroup)config.GetSectionGroup("system.net/mailSettings");
if (mail.Smtp.DeliveryMethod == SmtpDeliveryMethod.SpecifiedPickupDirectory)
{
string path = Path.Combine(HttpRuntime.AppDomainAppPath, @"..\Mail");
mail.Smtp.SpecifiedPickupDirectory.PickupDirectoryLocation = path;
}
But on closer inspection of the SmtpClient class using
Reflector, you'll find that the
default smtp settings utilise a static instance of an internal class
called MailSettingsSectionGroupInternal
, and is initialised from the
default web.config settings. So changes to the runtime configuration
don't effect the actual values the SmtpClient uses. That pretty much
means the settings can only be 'set' when the configuration is loaded.
The only way we can get our changes to be applied is to save the
configuration (with a call to config.Save()
), which means writing to
the web.config file. This in turn triggers the application to reload the
changes. It's not ideal, since the app will be restarted and it's bad
because it updates the web.config file with the new absolute path to the
pickup folder. It's pretty much back to square one.
Reflection to the rescue
In .NET, nothing is too far from our reach if you know how to go about it. To programmatically set the PickupDirectoryLocation at runtime, all we need to do is use reflection to navigate our way down a path of internal classes and properties, to the single private variable that needs to change, and then simply set it.
The hierarchy that we need to change looks like this:
System.Net.Mail.SmtpClient.MailConfiguration.Smtp.SpecifiedPickupDirectory.PickupDirectoryLocation
Since we can access the public member SmtpClient
, we'll start there
and get the first internal (and static) property, MailConfiguration
.
This should be called or defined in the Application_Start
method of
your Global.asax.
First, let's define some variables for working with, as well as the path we want to use for our drop folder. In this instance, I've set the path to the folder above the current application root, in a folder called "Mail".
BindingFlags instanceFlags = BindingFlags.Instance | BindingFlags.NonPublic;
PropertyInfo prop;
object mailConfiguration, smtp, specifiedPickupDirectory;
string path = Path.Combine(HttpRuntime.AppDomainAppPath, @"..\Mail");
Then get the first static property and object from the SmtpClient
// get static internal property: MailConfiguration
prop = typeof(SmtpClient).GetProperty("MailConfiguration", BindingFlags.Static | BindingFlags.NonPublic);
mailConfiguration = prop.GetValue(null, null);
Continue down the hierarchy from the object we just assigned to
mailConfiguration
, getting the properties, and then the value, for
each instance.
// get internal property: Smtp
prop = mailConfiguration.GetType().GetProperty("Smtp", instanceFlags);
smtp = prop.GetValue(mailConfiguration, null);
// get internal property: SpecifiedPickupDirectory
prop = smtp.GetType().GetProperty("SpecifiedPickupDirectory", instanceFlags);
specifiedPickupDirectory = prop.GetValue(smtp, null);
Lastly, get the field we want to set, as the corresponding property doesn't provide a setter.
// get private field: pickupDirectoryLocation, then set it to the supplied path
FieldInfo field = specifiedPickupDirectory.GetType().GetField("pickupDirectoryLocation", instanceFlags);
field.SetValue(specifiedPickupDirectory, path);
And now, whenever an instance of SmtpClient is created, it will be initialised with the new pickup folder path.
A C# helper class, for your convenience.
So now you know how to set the property, here is the full listing of the helper class I use to achieve it.
How to use the CSharpVitamins.MailHelper class.
void Application_Start(object sender, EventArgs e)
{
// set drop folder for mail
if (MailHelper.IsUsingPickupDirectory)
MailHelper.SetRelativePickupDirectoryLocation(@"..\Mail");
}
Source code for the CSharpVitamins.MailHelper class.
using System;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Net.Configuration;
using System.Net.Mail;
using System.Reflection;
using System.Web;
using System.Web.Configuration;
namespace CSharpVitamins
{
/// <summary>
/// Exposes helper methods for dealing with system.net/mailSettings
/// </summary>
public static class SmtpPickupDirectory
{
static bool? _isUsingPickupDirectory;
/// <summary>
/// Gets a value to indicate if the default SMTP Delivery method is SpecifiedPickupDirectory (using web.config, not tested with app.config)
/// </summary>
public static bool IsUsingPickupDirectory
{
get
{
if (!_isUsingPickupDirectory.HasValue)
{
Configuration config = WebConfigurationManager.OpenWebConfiguration("~/web.config");
var mail = (MailSettingsSectionGroup)config.GetSectionGroup("system.net/mailSettings");
_isUsingPickupDirectory = mail.Smtp.DeliveryMethod == SmtpDeliveryMethod.SpecifiedPickupDirectory;
}
return _isUsingPickupDirectory.Value;
}
}
/// <summary>
/// Sets the default PickupDirectoryLocation for the SmtpClient.
/// </summary>
/// <remarks>
/// This method should be called to set the PickupDirectoryLocation
/// for the SmtpClient at runtime (Application_Start)
///
/// Reflection is used to set the private variable located in the
/// internal class for the SmtpClient's mail configuration:
/// System.Net.Mail.SmtpClient.MailConfiguration.Smtp.SpecifiedPickupDirectory.PickupDirectoryLocation
///
/// The folder must exist.
///
/// Alternate configuration method saves the web.config, triggering an app restart
/// Configuration config = WebConfigurationManager.OpenWebConfiguration("~/web.config");
/// var mail = (MailSettingsSectionGroup)config.GetSectionGroup("system.net/mailSettings");
/// if (mail.Smtp.DeliveryMethod == SmtpDeliveryMethod.SpecifiedPickupDirectory)
/// {
/// string path = Path.Combine( HttpRuntime.AppDomainAppPath, @"..\..\mymail" );
/// mail.Smtp.SpecifiedPickupDirectory.PickupDirectoryLocation = path;
/// if (!Directory.Exists(path))
/// Directory.CreateDirectory( path );
/// config.Save();
/// }
/// </remarks>
/// <param name="path"></param>
public static void SetPickupDirectoryLocation(string path)
{
if (null == path)
throw new ArgumentNullException(nameof(path));
if (!Path.IsPathRooted(path))
throw new ArgumentException("path must be absolute", nameof(path));
BindingFlags instanceFlags = BindingFlags.Instance | BindingFlags.NonPublic;
PropertyInfo prop;
object mailConfiguration, smtp, specifiedPickupDirectory;
// get static internal property: MailConfiguration
prop = typeof(SmtpClient).GetProperty("MailConfiguration", BindingFlags.Static | BindingFlags.NonPublic);
mailConfiguration = prop.GetValue(null, null);
// get internal property: Smtp
prop = mailConfiguration.GetType().GetProperty("Smtp", instanceFlags);
smtp = prop.GetValue(mailConfiguration, null);
// get internal property: SpecifiedPickupDirectory
prop = smtp.GetType().GetProperty("SpecifiedPickupDirectory", instanceFlags);
specifiedPickupDirectory = prop.GetValue(smtp, null);
// get private field: pickupDirectoryLocation, then set it to the supplied path
FieldInfo field = specifiedPickupDirectory.GetType().GetField("pickupDirectoryLocation", instanceFlags);
field.SetValue(specifiedPickupDirectory, path);
}
/// <summary>
/// Sets the default PickupDirectoryLocation for the SmtpClient
/// to the relative path from the current web root.
/// </summary>
/// <param name="path">Relative path to the web root</param>
public static void SetRelativePickupDirectoryLocation(string path)
{
if (null == path)
throw new ArgumentNullException("path");
SetPickupDirectoryLocation(Path.Combine(HttpRuntime.AppDomainAppPath, path));
}
/// <summary>
/// Sets the default PickupDirectoryLocation for the SmtpClient to the first relative path that exists
/// </summary>
/// <param name="possibilities">An array of relative paths to test existence of</param>
/// <returns>The full path of the folder, if it was found, otherwise null</returns>
public static string SetRelativePickupDirectoryLocationIfPathExists(string[] possibilities)
{
string path = possibilities
.Select(x => Path.Combine(HttpRuntime.AppDomainAppPath, x))
.FirstOrDefault(x => Directory.Exists(x));
if (null != path)
SetPickupDirectoryLocation(path);
return path;
}
}
}
Click here to download the source code for the MailHelper class.
[Update 2015-06-05]
The original 2007 code is available on GitHub.
Available on NuGet. To install, run the following command in the Package Manager Console:
PM> Install-Package CSharpVitamins.SmtpPickupDirectory
Conclusion
In summary, it appears that we need to jump through a few hoops to achieve something fairly small in nature. Although encapsulating our code into a helper class improves the process of applying our desired setting, I couldn't find anything more straight forward to accomplish the task.