C#: Working with SMTP email (Part 1)
Introduction
This article, part one of a series will begin by showing how to setup the foundation for sending email messages with System.Net.SmtpClient and System.Net.MailMessage classes. The first part which is covered is setting up an object capable of sending an email message. This object is created from the class SmtpClient.
Setting up a SmtpClient object is simple, create a new instance of SmtpClient, create a MailMessage object, provide a "From" and "To" address, a title and body for the message and invoked the Send method of the SmtpClient object. Unfortunately more is required the majority of the time.
Part 2 of the series
https://social.technet.microsoft.com/wiki/contents/articles/51936.c-working-with-smtp-email-part-2.aspx
SmtpClient issues
If the Host property is not setup properly an email cannot be sent. If the Host is correct but the Port is incorrect, an email may be sent but the server (the host) may refuse the request to accept the email operation.
If the Credentials are not setup properly an unauthorized runtime exception will be thrown. A Host may require a username and password but you want to send an email without user intervention. This can be a tough one to handle but this topic will be covered with a working solution.
Some Host like Gmail will have security measures in place that must be setup by signing into your account and setting up permissions.
The Send method for sending email messages is synchronous which means your application is on hold until the intended message has been sent. This could be a second or two or perhaps a minute.
These topics will be covered within a custom mail class that is easy to use in your solution along with other useful features which work in desktop and web solutions alike with unit testing.
Preparation for client setup
Setting the foundation for working with SmtpClient which allows multiple configurations, for example, a primary configuration may be using Comcast as the Host while a secondary/backup configuration for Gmail.
The basics for setting the client is to provide a host and port. Depending on the environment the credentials and enableSsl may not need to be set while in other environments both properties may be required. Example, on a company network Active Directory will handle credentials for desktop or internal web solutions.
The following shows a raw example for setting up the parts to send an email using SMTP.
MailMessage mail = new MailMessage();
//set the addresses
mail.From = new MailAddress("me@mycompany.com");
mail.To.Add("you@yourcompany.com");
//set the content
mail.Subject = "This is an email";
mail.Body = "this is a sample body";
//send the message
SmtpClient smtp = new SmtpClient("127.0.0.1");
smtp.Send(mail);
This may be all that is needed yet a developer may find themselves battling with various properties to get the setup of the SmtpClient correct.
mailSettings (Network Settings)
The .NET Framework provides us with a way to setup properties which can be read by your code in app.config (desktop solutions) or web.config (for web solutions) under mailSettings. Note in web.config you can use transformations and have different settings for different environments (dev, test, and production). In the app.config example under Custom configuration class the pickup folder will not work for web solutions. Let's say you are using IIS, generally, the pickup folder location is under c:\Inetpub\mailroot\Pickup, you can use SmtpDeliveryMethod.PickupDirectoryFromIis to get the location (directly from IIS metabase) rather than hard-coding the path and this should be fully tested in a unit test.
Basic example
<system.net>
<mailSettings>
<smtp deliveryMethod="Network" from="smthg@smthg.net">
<network defaultCredentials="false" host="localhost" port="587"
userName="smthg@smthg.net" password=""/>
</smtp>
</mailSettings>
</system.net>
To read the above settings use
using System.Configuration;
using System.Net.Configuration;
// .
var smtpSection = (SmtpSection)ConfigurationManager.GetSection("system.net/mailSettings/smtp");
string username = smtpSection.Network.UserName;
The above code permits settings to be changed in the project configuration file rather than have setting in code which means rather than editing a configuration file to change setting you must alter the code, compile (optionally test) then place back into production.
Custom configuration class
The following class is best placed in a class project as it's generic and can be used in all your solutions.
Create a new class project, in this case the name is SmtpMailConfiguration. Add a reference to the project for System.Configuration. Change Class1.cs to MailConfiguration and replace its contents with the following.
using System;
using System.Configuration;
using System.IO;
using System.Net.Configuration;
namespace SmtpMailConfiguration
{
public class MailConfiguration
{
private readonly SmtpSection _smtpSection;
/// <summary>
/// Configure which setting to use for getting SMTP settings
/// from the configuration file.
/// </summary>
/// <param name="section"></param>
public MailConfiguration(string section = "system.net/mailSettings/smtp")
{
_smtpSection = (ConfigurationManager.GetSection(section) as SmtpSection);
}
/// <summary>
/// Email address for the system
/// </summary>
public string FromAddress => _smtpSection.From;
#region Should be encrypted (will show how in part 2 of this series)
public string UserName => _smtpSection.Network.UserName;
public string Password => _smtpSection.Network.Password;
#endregion
/// <summary>
/// Gets the system credentials of the application.
/// </summary>
public bool DefaultCredentials => _smtpSection.Network.DefaultCredentials;
/// <summary>
/// Specify whether the SmtpClient uses Secure Sockets Layer (SSL)
/// to encrypt the connection.
/// </summary>
public bool EnableSsl => _smtpSection.Network.EnableSsl;
/// <summary>
/// Gets or sets the folder where applications save mail messages to
/// be processed by the local SMTP server.
/// </summary>
public string PickupFolder
{
get
{
string mailDrop = _smtpSection.SpecifiedPickupDirectory.PickupDirectoryLocation;
if (mailDrop != null)
{
mailDrop = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory,
_smtpSection.SpecifiedPickupDirectory.PickupDirectoryLocation);
}
return mailDrop;
}
}
/// <summary>
/// Determine if pickup folder exists
/// </summary>
/// <returns></returns>
public bool PickupFolderExists()
{
return Directory.Exists(PickupFolder);
}
/// <summary>
/// Gets the name or IP address of the host used for SMTP transactions.
/// </summary>
public string Host => _smtpSection.Network.Host;
/// <summary>
/// Gets the port used for SMTP transactions
/// </summary>
public int Port => _smtpSection.Network.Port;
/// <summary>
/// Gets a value that specifies the amount of time after
/// which a synchronous Send call times out.
/// </summary>
public int TimeOut => 2000;
/// <summary>
/// Allows, for debugging to review from address, host and port properties
/// </summary>
/// <returns>A string with main properties</returns>
public override string ToString() => $"From: [ {FromAddress} ]" +
$"Host: [{Host}] Port: [{Port}] "+
$"Pickup: {Directory.Exists(PickupFolder)}";
}
}
Create a new unit test project followed by adding a new application configuration file (app.config). Replace app.config contents with the following.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="mailSettings">
<section name="smtp_1" type="System.Net.Configuration.SmtpSection"/>
</sectionGroup>
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
</startup>
<!--
extra mail setting, main could be in here also.
If all settings are here you should provide names that are meaningful
rather than smtp_n where n is the index for the SMTP settings.
-->
<mailSettings>
<smtp_1 from="someone@gmail.com">
<network
host="smtp.gmail.com"
port="587"
enableSsl="true"
userName="MssGMail"
password=""
defaultCredentials="false" />
<!--
where to create emails for testing, could be a different folder then
the one in main but if so the Post Build event need to have another
macro for that folder.
-->
<specifiedPickupDirectory pickupDirectoryLocation="MailDrop"/>
</smtp_1>
</mailSettings>
<!-- main -->
<system.net>
<mailSettings>
<smtp from="Someone@comcast.net">
<network
host="smtp.comcast.net"
port="587"
enableSsl="true"
userName="MissComcast"
password=""
defaultCredentials="true" />
<!--
where to create emails for testing, could be a different folder then the one in smtp_1
but if so the Post Build event need to have another macro for that folder.
-->
<specifiedPickupDirectory pickupDirectoryLocation="MailDrop"/>
</smtp>
</mailSettings>
</system.net>
</configuration>
Add a reference to the project SmtpMailConfiguration. The next part will be used in the next part in the series. Open the property window for the unit test project, select the Build Events tab and insert the following into the "Post-build event command line". Each time your test project builds the code below will create a folder MailDrop under Bin\Debug if the folder does not exist. This allows you to test sending email messages without actually sending them but instead create .eml files. Although we will get into this later the basics are you can validate the MailMessage aspect of sending messages. Unfortunately, the .eml files generated for you are restrictive in reading content and verifying things like BCC. There are libraries on GitHub which can assist in this matter along with using HTML Agility pack.
Post-build event command line
if not exist $(TargetDir)\MailDrop mkdir $(TargetDir)\MailDrop
In the default class created in the unit test project add the following test. using System;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SmtpMailConfiguration;
namespace smtpUnitTests
{
[TestClass]
public class UnitTest1
{
protected string GmailConfiguration = "mailSettings/smtp_1";
protected string ComcastConfiguration = "system.net/mailSettings/smtp";
[TestMethod]
public void GMailFrom()
{
var mc = new MailConfiguration(GmailConfiguration);
Assert.IsTrue(mc.FromAddress == "someone@gmail.com",
"Wrong from address for gmail");
}
[TestMethod]
public void GMailUserName()
{
var mc = new MailConfiguration(GmailConfiguration);
Assert.IsTrue(mc.UserName == "MssGMail",
"Wrong user name for gmail");
}
[TestMethod]
public void GMailHost()
{
var mc = new MailConfiguration(GmailConfiguration);
Assert.IsTrue(mc.Host == "smtp.gmail.com",
"Wrong host for gmail");
}
[TestMethod]
public void GMailPort()
{
var mc = new MailConfiguration(GmailConfiguration);
Assert.IsTrue(mc.Port == 587,
"Wrong port for gmail");
}
[TestMethod]
public void GMailEnableSsl()
{
var mc = new MailConfiguration(GmailConfiguration);
Assert.IsTrue(mc.EnableSsl,
"Wrong EnableSsl for gmail");
}
[TestMethod]
public void GMailDefaultCredentials()
{
var mc = new MailConfiguration(GmailConfiguration);
Assert.IsTrue(mc.DefaultCredentials == false,
"Wrong DefaultCredentials for gmail");
}
[TestMethod]
public void GMailPickupFolderExists()
{
var mc = new MailConfiguration(GmailConfiguration);
Assert.IsTrue(mc.PickupFolderExists(),
"gmail pickup folder does not exists");
}
[TestMethod]
public void ComcastPickupFolderExists()
{
var mc = new MailConfiguration(ComcastConfiguration);
Assert.IsTrue(mc.PickupFolderExists(),
"Comcast pickup folder does not exists");
}
[TestMethod]
public void ComcastFrom()
{
var mc = new MailConfiguration(ComcastConfiguration);
Assert.IsTrue(mc.FromAddress == "Someone@comcast.net",
"Wrong from address for Comcast");
}
[TestMethod]
public void ComcastUserName()
{
var mc = new MailConfiguration(ComcastConfiguration);
Assert.IsTrue(mc.UserName == "MissComcast",
"Wrong user name for Comcast");
}
[TestMethod]
public void ComcastHost()
{
var mc = new MailConfiguration(ComcastConfiguration);
Assert.IsTrue(mc.Host == "smtp.comcast.net",
"Wrong host for Comcast");
}
[TestMethod]
public void ComcastPort()
{
var mc = new MailConfiguration(ComcastConfiguration);
Assert.IsTrue(mc.Port == 587,
"Wrong port for Comcast");
}
[TestMethod]
public void ComcastEnableSsl()
{
var mc = new MailConfiguration(ComcastConfiguration);
Assert.IsTrue(mc.EnableSsl,
"Wrong EnableSsl for Comcast");
}
[TestMethod]
public void ComcastDefaultCredentials()
{
var mc = new MailConfiguration(ComcastConfiguration);
Assert.IsTrue(mc.DefaultCredentials,
"Wrong DefaultCredentials for Comcast");
}
}
}
At this point, you can run the test to ensure the proper setting are being returned for both Gmail and Comcast setting. Suppose you don't need multiple configurations. The following would be how your configuration file looks.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="mailSettings">
<section name="smtp_1" type="System.Net.Configuration.SmtpSection"/>
</sectionGroup>
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
</startup>
<system.net>
<mailSettings>
<smtp from="Someone@comcast.net">
<network
host="smtp.comcast.net"
port="587"
enableSsl="true"
userName="MissComcast"
password=""
defaultCredentials="true" />
<specifiedPickupDirectory pickupDirectoryLocation="MailDrop"/>
</smtp>
</mailSettings>
</system.net>
</configuration>
Then in MailConfiguration class, the constructor requires no parameter passed as the default will read the above settings. The next article will cover Setting up MailMessage in tangent with SmptClient to send email messages for both testing and live environment where the configuration class presented in this article will be used for unit testing your solution.
Requirements
The code presented requires C# 6 or higher as several features of C# 6 are used but with small modifications, this code will work in prior versions of C#.
Summary
In the article, you have learned how to setup mail settings in your configuration file within a class which is reusable across multiple solutions and projects.
See also
C#: Working with SMTP email (Part 2)
SmptClient class, MailMessage class, MailSettingsSectionGroup class.