Improving the SmtpClient to use multiple gateways and to queue emails.
Most ASP.NET and Windows Forms applications are required to send emails. Many people also use email to send errors. Microsoft providers the System.Net.Mail namespace to do that job, they have improved the email SmtpClient since version 1.1. However sending thousands of emails can become a very long task to keep track.
I develop a component that you can download called FreeMail http://alpascual.com/control/bettermailmessage.htm
I wanted to talk about the magic behind the control. The main class deriving from SmtpClient looks like this:
public class FreeMail : SmtpClient
{
private ArrayList m_aMailServers = null;
// New Queue
public delegate void SendAllNewQueueDelegate();
public event SendAllNewQueueDelegate SendAllNew;
public delegate void SendAllPendingQueueDelegate();
public event SendAllPendingQueueDelegate SendAllPending;
public int MaxHoursToRetry = 24;
public FreeMail(ArrayList aMailServers)
{
// Resolve the all mail servers
m_aMailServers = new ArrayList();
for(int i=0; i<aMailServers.Count; i++)
{
IPHostEntry local = Dns.Resolve(aMailServers[i].ToString());
foreach (IPAddress ipaddress in local.AddressList)
{
m_aMailServers.Add(ipaddress.ToString());
}
}
SendAllNew += new SendAllNewQueueDelegate(FreeMail_SendAllNew);
SendAllPending += new SendAllPendingQueueDelegate(FreeMail_SendAllPending);
}
void FreeMail_SendAllPending()
{
for (int i = 0; i < PendingQueue.Count; i++)
{
BetterMailMessage oMessage = PendingQueue.Dequeue();
if (DoSend(oMessage) == false)
{
if (oMessage.Date.AddHours(MaxHoursToRetry) >= DateTime.Now)
PendingQueue.Add(oMessage);
else
BetterMessageError.Add(oMessage);
}
}
}
void FreeMail_SendAllNew()
{
for (int i = 0; i < NewQueue.Count; i++)
{
BetterMailMessage oMessage = NewQueue.Dequeue();
if (DoSend(oMessage) == false)
PendingQueue.Add(oMessage);
}
}
private new void Send(MailMessage oMessage)
{
base.Send(oMessage);
}
public void Send(BetterMailMessage oMessage)
{
NewQueue.Add(oMessage);
this.SendAllNew();
this.SendAllPending();
}
private bool DoSend(BetterMailMessage oMessage)
{
bool bSucess = false;
if (LoadBalancing == true)
m_aMailServers.Reverse();
for (int i = 0; i < m_aMailServers.Count; i++)
{
base.Host = m_aMailServers[i].ToString();
try
{
MailMessage oMessageBase = oMessage;
if (TrackingUrl.Length > 1)
oMessageBase.Body += BetterTracker.Track(TrackingUrl, oMessageBase.To);
base.Send(oMessageBase);
bSucess = true;
}
catch { bSucess = false; }
if (bSucess == true)
break;
}
return (bSucess);
}
private bool bLoadBalancing = false;
public bool LoadBalancing
{
get { return (bLoadBalancing); }
set { bLoadBalancing = value; }
}
private string sTrackingUrl = "";
public string TrackingUrl
{
get { return (sTrackingUrl); }
set { sTrackingUrl = value; }
}
}
Lets go step by step, the constructor resolves all mail servers to their IP address, the Send function will send all new email and old old queued, you’ll be able to track the email by adding a TrackingUrl property that will be called back when the user opens the email and does not block images.
How do we track the email? This is the class to track them, not guarantied that will work for all email readers of course:
public class BetterTracker
{
public static string Track(string sUrl, MailAddressCollection sTo)
{
string sRet = "<IMG height=1 src=\"" + sUrl + "?Tracking=" + Encoding(sTo) + "\" width=1>";
return (sRet);
}
public static string Encoding(MailAddressCollection sTo)
{
string sAllTo = "";
for (int i = 0; i < sTo.Count; i++)
{
sAllTo += sTo[i].Address + " ";
}
string uidpwd = sAllTo + ":" + DateTime.Now.ToString();
string sEn = AlEncrypt.Encrypt(uidpwd);
return (HttpUtility.UrlEncode(sEn));
}
public static TrackDetails Decode(string EncodedString)
{
string sDe = HttpUtility.UrlDecode(EncodedString);
string sBeforeClass = AlEncrypt.Decrypt(sDe);
TrackDetails oDetails = new TrackDetails();
try
{
int len = sBeforeClass.IndexOf(":");
string sTo = sBeforeClass.Substring(0, len);
string sTime = sBeforeClass.Substring(len + 1);
DateTime dtTime = Convert.ToDateTime(sTime);
oDetails.sTo = sTo;
oDetails.TimeSent = dtTime;
}
catch { }
return (oDetails);
}
}
public class TrackDetails
{
public string sTo = "";
public DateTime TimeSent;
}
public class AlEncrypt
{
private static string sKey = "al%&is($awesome^ohyes$!too*whois)better**than(Albert!";
public static string Encrypt(string sPainText)
{
if (sPainText.Length == 0)
return (sPainText);
return (EncryptString(sPainText, sKey));
}
public static string Decrypt(string sEncryptText)
{
if (sEncryptText.Length == 0)
return (sEncryptText);
return (DecryptString(sEncryptText, sKey));
}
protected static string EncryptString(string InputText, string Password)
{
// "Password" string variable is nothing but the key(your secret key) value which is sent from the front end.
// "InputText" string variable is the actual password sent from the login page.
// We are now going to create an instance of the
// Rihndael class.
RijndaelManaged RijndaelCipher = new RijndaelManaged();
// First we need to turn the input strings into a byte array.
byte[] PlainText = System.Text.Encoding.Unicode.GetBytes(InputText);
// We are using Salt to make it harder to guess our key
// using a dictionary attack.
byte[] Salt = Encoding.ASCII.GetBytes(Password.Length.ToString());
// The (Secret Key) will be generated from the specified
// password and Salt.
//PasswordDeriveBytes -- It Derives a key from a password
PasswordDeriveBytes SecretKey = new PasswordDeriveBytes(Password, Salt);
// Create a encryptor from the existing SecretKey bytes.
// We use 32 bytes for the secret key
// (the default Rijndael key length is 256 bit = 32 bytes) and
// then 16 bytes for the IV (initialization vector),
// (the default Rijndael IV length is 128 bit = 16 bytes)
ICryptoTransform Encryptor = RijndaelCipher.CreateEncryptor(SecretKey.GetBytes(16), SecretKey.GetBytes(16));
// Create a MemoryStream that is going to hold the encrypted bytes
MemoryStream memoryStream = new MemoryStream();
// Create a CryptoStream through which we are going to be processing our data.
// CryptoStreamMode.Write means that we are going to be writing data
// to the stream and the output will be written in the MemoryStream
// we have provided. (always use write mode for encryption)
CryptoStream cryptoStream = new CryptoStream(memoryStream, Encryptor, CryptoStreamMode.Write);
// Start the encryption process.
cryptoStream.Write(PlainText, 0, PlainText.Length);
// Finish encrypting.
cryptoStream.FlushFinalBlock();
// Convert our encrypted data from a memoryStream into a byte array.
byte[] CipherBytes = memoryStream.ToArray();
// Close both streams.
memoryStream.Close();
cryptoStream.Close();
// Convert encrypted data into a base64-encoded string.
// A common mistake would be to use an Encoding class for that.
// It does not work, because not all byte values can be
// represented by characters. We are going to be using Base64 encoding
// That is designed exactly for what we are trying to do.
string EncryptedData = Convert.ToBase64String(CipherBytes);
// Return encrypted string.
return EncryptedData;
}
protected static string DecryptString(string InputText, string Password)
{
RijndaelManaged RijndaelCipher = new RijndaelManaged();
byte[] EncryptedData = Convert.FromBase64String(InputText);
byte[] Salt = Encoding.ASCII.GetBytes(Password.Length.ToString());
PasswordDeriveBytes SecretKey = new PasswordDeriveBytes(Password, Salt);
// Create a decryptor from the existing SecretKey bytes.
ICryptoTransform Decryptor = RijndaelCipher.CreateDecryptor(SecretKey.GetBytes(16), SecretKey.GetBytes(16));
MemoryStream memoryStream = new MemoryStream(EncryptedData);
// Create a CryptoStream. (always use Read mode for decryption).
CryptoStream cryptoStream = new CryptoStream(memoryStream, Decryptor, CryptoStreamMode.Read);
// Since at this point we don't know what the size of decrypted data
// will be, allocate the buffer long enough to hold EncryptedData;
// DecryptedData is never longer than EncryptedData.
byte[] PlainText = new byte[EncryptedData.Length];
// Start decrypting.
int DecryptedCount = cryptoStream.Read(PlainText, 0, PlainText.Length);
memoryStream.Close();
cryptoStream.Close();
// Convert decrypted data into a string.
string DecryptedData = Encoding.Unicode.GetString(PlainText, 0, DecryptedCount);
// Return decrypted string.
return DecryptedData;
}
}
Also we have the class to queue the emails when they failed, but that’s a normal queue. If you want me to show you how it works, please let me know.
Hope this helps with that component.
Cheers
Al


