Sends emails based on the MailModel and MailTemplate classes (also included below).
// ---------------------------------------------------------------------
// Emailer.cs
// https://github.com/ChadBurggraf/tasty/blob/master/Source/Tasty/Emailer.cs
// ---------------------------------------------------------------------
namespace Tasty
{
using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Configuration;
using System.Net;
using System.Net.Configuration;
using System.Net.Mail;
using System.Threading;
/// <summary>
/// Sends emails based on <see cref="MailModel"/>s and <see cref="MailTemplate"/>s.
/// </summary>
public class Emailer
{
private MailTemplate template;
private int? port;
private int sent;
/// <summary>
/// Initializes a new instance of the Emailer class.
/// </summary>
/// <param name="template">The <see cref="MailTemplate"/> to use when processing the email(s).</param>
public Emailer(MailTemplate template)
{
if (template == null)
{
throw new ArgumentNullException("template", "template cannot be null.");
}
this.template = template;
this.Attachments = new StringCollection();
this.Bcc = new StringCollection();
this.CC = new StringCollection();
this.To = new StringCollection();
this.InitializeFromConfiguration();
}
/// <summary>
/// Event raised when emails have been sent to all of the addresses in the <see cref="To"/> collection.
/// </summary>
public event EventHandler AllSent;
/// <summary>
/// Event raised when an email is sent to a single destination address.
/// </summary>
public event EventHandler<EmailSentEventArgs> Sent;
/// <summary>
/// Gets the collection of file paths to attach to emails.
/// </summary>
public StringCollection Attachments { get; private set; }
/// <summary>
/// Gets the collection of addresses to BCC on emails.
/// </summary>
public StringCollection Bcc { get; private set; }
/// <summary>
/// Gets the collection of addresses to CC on emails.
/// </summary>
public StringCollection CC { get; private set; }
/// <summary>
/// Gets or sets the email address of the email sender.
/// Defaults to value found in <mailSettings/> if not set.
/// </summary>
public string From { get; set; }
/// <summary>
/// Gets or sets the display name of the email sender.
/// </summary>
public string FromDisplayName { get; set; }
/// <summary>
/// Gets or sets the password to use when connecting to the server.
/// Defaults to value found in <mailSettings/> if not set.
/// </summary>
public string Password { get; set; }
/// <summary>
/// Gets or sets the port to connect to the server on.
/// Defaults to value found in <mailSettings/> if not set.
/// </summary>
public int Port
{
get { return (int)(this.port ?? (this.port = 25)); }
set { this.port = value; }
}
/// <summary>
/// Gets or sets the IP address or host name of the SMTP server.
/// Defaults to value found in <mailSettings/> if not set.
/// </summary>
public string SmtpServer { get; set; }
/// <summary>
/// Gets or sets the email subject.
/// </summary>
public string Subject { get; set; }
/// <summary>
/// Gets the collection of destination addresses to send to.
/// </summary>
public StringCollection To { get; private set; }
/// <summary>
/// Gets or sets the username to use when authenticating with the server.
/// Defaults to value found in <mailSettings/> if not set.
/// </summary>
public string UserName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to use SSL when connecting to the server.
/// </summary>
public bool UseSsl { get; set; }
/// <summary>
/// Sends the email(s) current configured by this instance.
/// </summary>
/// <param name="model">The model to use when sending email.
/// WARNING: The model's <see cref="MailModel.Email"/> property will be set for each recipient.</param>
public void Send(MailModel model)
{
if (model == null)
{
throw new ArgumentNullException("model", "model cannot be null.");
}
this.Validate();
SmtpClient client = this.CreateClient();
string body = this.template.Transform(model);
using (MailMessage message = this.CreateMessage())
{
foreach (string to in this.To)
{
model.Email = to;
message.To.Clear();
message.To.Add(to);
message.Body = body;
client.Send(message);
this.RaiseEvent(this.Sent, new EmailSentEventArgs(to));
}
this.RaiseEvent(this.AllSent, EventArgs.Empty);
}
}
/// <summary>
/// Sends the email(s) current configured by this instance.
/// WARNING: A giant assumption is made that changes to the <see cref="To"/> collection will not be made
/// while this call is in progress, as well as that no other calls to <see cref="Send(MailModel)"/>
/// or <see cref="SendAsync(MailModel)"/> are made on this instance while this call is in progress.
/// </summary>
/// <param name="model">The model to use when sending email.
/// WARNING: The model's <see cref="MailModel.Email"/> property will be set for each recipient.</param>
public void SendAsync(MailModel model)
{
if (model == null)
{
throw new ArgumentNullException("model", "model cannot be null.");
}
this.Validate();
Thread thread = new Thread(new ParameterizedThreadStart(delegate(object state)
{
using (MailMessage message = this.CreateMessage())
{
foreach (string to in this.To)
{
model.Email = to;
message.Body = this.template.Transform(model);
SmtpClient client = this.CreateClient();
client.SendCompleted += new SendCompletedEventHandler(this.ClientSendCompleted);
client.SendAsync(message, to);
}
}
}));
thread.Start();
}
/// <summary>
/// Initializes this instance's state from anything found in the configuration.
/// </summary>
private void InitializeFromConfiguration()
{
SmtpSection smtp = ConfigurationManager.GetSection("system.net/mailSettings/smtp") as SmtpSection;
if (smtp != null)
{
this.From = smtp.From;
if (smtp.Network != null)
{
this.SmtpServer = smtp.Network.Host;
this.Port = smtp.Network.Port;
this.UserName = smtp.Network.UserName;
this.Password = smtp.Network.Password;
}
}
}
/// <summary>
/// Raises an <see cref="SmtpClient"/>'s SendCompleted event.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void ClientSendCompleted(object sender, AsyncCompletedEventArgs e)
{
this.sent++;
this.RaiseEvent(this.Sent, new EmailSentEventArgs((string)e.UserState));
if (this.sent == this.To.Count)
{
this.sent = 0;
this.RaiseEvent(this.AllSent, EventArgs.Empty);
}
}
/// <summary>
/// Creates a new <see cref="SmtpClient"/> from this instance's state.
/// </summary>
/// <returns>The created <see cref="SmtpClient"/>.</returns>
private SmtpClient CreateClient()
{
SmtpClient client = new SmtpClient(this.SmtpServer, this.Port);
client.EnableSsl = this.UseSsl;
if (!String.IsNullOrEmpty(this.UserName) && !String.IsNullOrEmpty(this.Password))
{
client.Credentials = new NetworkCredential(this.UserName, this.Password);
}
return client;
}
/// <summary>
/// Creates a new <see cref="MailMessage"/> object from this instance's state.
/// </summary>
/// <returns>The created <see cref="MailMessage"/>.</returns>
private MailMessage CreateMessage()
{
MailMessage message = new MailMessage();
foreach (string attachment in this.Attachments)
{
message.Attachments.Add(new Attachment(attachment));
}
foreach (string bcc in this.Bcc)
{
message.Bcc.Add(bcc);
}
foreach (string cc in this.CC)
{
message.CC.Add(cc);
}
message.From = new MailAddress(this.From, this.FromDisplayName);
message.Subject = this.Subject;
message.IsBodyHtml = true;
return message;
}
/// <summary>
/// Validates this instance's state before sending.
/// </summary>
private void Validate()
{
if (String.IsNullOrEmpty(this.From))
{
throw new InvalidOperationException("From must be set to a value before sending.");
}
if (String.IsNullOrEmpty(this.SmtpServer))
{
throw new InvalidOperationException("SmtpServer must be set to a value before sending.");
}
if (this.To.Count == 0)
{
throw new InvalidOperationException("To must contain at least one email address before sending.");
}
}
}
}
// ---------------------------------------------------------------------
// EmailSentEventArgs.cs
// https://github.com/ChadBurggraf/tasty/blob/master/Source/Tasty/EmailSentEventArgs.cs
// ---------------------------------------------------------------------
namespace Tasty
{
using System;
/// <summary>
/// Event arguments for <see cref="Emailer.Sent"/> events.
/// </summary>
public class EmailSentEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the EmailSentEventArgs class.
/// </summary>
/// <param name="to">The destination address of the email.</param>
public EmailSentEventArgs(string to)
{
if (String.IsNullOrEmpty(to))
{
throw new ArgumentNullException("to", "to must contain a value.");
}
this.To = to;
}
/// <summary>
/// Gets the destination address of the email.
/// </summary>
public string To { get; private set; }
}
}
// ---------------------------------------------------------------------
// MailModel.cs
// https://github.com/ChadBurggraf/tasty/blob/master/Source/Tasty/MailModel.cs
// ---------------------------------------------------------------------
namespace Tasty
{
using System;
using System.Globalization;
using System.IO;
using System.Runtime.Serialization;
using System.Text;
using System.Xml;
using System.Xml.XPath;
/// <summary>
/// Represents the base class for templated email models.
/// </summary>
[DataContract(Namespace = MailModel.XmlNamespace)]
public abstract class MailModel
{
/// <summary>
/// Gets the XML namespace used during mail model serialization.
/// </summary>
public const string XmlNamespace = "http://tastycodes.com/tasty-dll/mailmodel/";
private DateTime? today;
/// <summary>
/// Gets or sets the destination address of the email being modeled.
/// </summary>
[DataMember(IsRequired = true)]
public string Email { get; set; }
/// <summary>
/// Gets or sets the current date.
/// </summary>
[DataMember(IsRequired = true)]
public DateTime Today
{
get { return (DateTime)(this.today ?? (this.today = DateTime.Now)); }
set { this.today = value; }
}
/// <summary>
/// Serializes this instance to XML.
/// </summary>
/// <returns>The serialized XML.</returns>
public IXPathNavigable ToXml()
{
DataContractSerializer serializer = new DataContractSerializer(GetType());
StringBuilder sb = new StringBuilder();
using (StringWriter sw = new StringWriter(sb, CultureInfo.InvariantCulture))
{
using (XmlWriter xw = new XmlTextWriter(sw))
{
serializer.WriteObject(xw, this);
}
}
XmlDocument document = new XmlDocument();
document.LoadXml(sb.ToString());
return document;
}
}
}
// ---------------------------------------------------------------------
// MailTemplate.cs
// https://github.com/ChadBurggraf/tasty/blob/master/Source/Tasty/MailTemplate.cs
// ---------------------------------------------------------------------
namespace Tasty
{
using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Xsl;
/// <summary>
/// Represents an XSLT email template and its transformation.
/// </summary>
public class MailTemplate : IDisposable
{
private bool disposed, streamDisposable;
private Stream templateStream;
/// <summary>
/// Initializes a new instance of the MailTemplate class.
/// </summary>
/// <param name="templateStream">The stream of template data to initialize this instance with.</param>
public MailTemplate(Stream templateStream)
{
if (templateStream == null)
{
throw new ArgumentNullException("templateStream", "templateStream cannot be null.");
}
this.templateStream = templateStream;
}
/// <summary>
/// Initializes a new instance of the MailTemplate class.
/// </summary>
/// <param name="templatePath">The path to the XSLT template file to use.</param>
public MailTemplate(string templatePath)
{
if (String.IsNullOrEmpty(templatePath))
{
throw new ArgumentNullException("templatePath", "templatePath must contain a value.");
}
if (!File.Exists(templatePath))
{
throw new ArgumentException(String.Format(CultureInfo.InvariantCulture, @"The path ""{0}"" does not exist.", templatePath), "templatePath");
}
this.templateStream = File.OpenRead(templatePath);
this.streamDisposable = true;
}
/// <summary>
/// Finalizes an instance of the MailTemplate class.
/// </summary>
~MailTemplate()
{
this.Dispose(false);
}
/// <summary>
/// Disposes of resources used by this instance.
/// </summary>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Transforms the given model using this instance's template.
/// </summary>
/// <param name="model">The model to transform.</param>
/// <returns>A string of XML representing the transformed model.</returns>
public string Transform(MailModel model)
{
StringBuilder sb = new StringBuilder();
using (StringWriter sw = new StringWriter(sb, CultureInfo.InvariantCulture))
{
using (XmlWriter xw = new XmlTextWriter(sw))
{
this.Transform(model, xw);
}
}
return sb.ToString();
}
/// <summary>
/// Transforms the given model using this instance's template.
/// </summary>
/// <param name="model">The model to transform.</param>
/// <param name="writer">The <see cref="XmlWriter"/> to write the results of the transformation to.</param>
public void Transform(MailModel model, XmlWriter writer)
{
if (model == null)
{
throw new ArgumentNullException("model", "model cannot be null.");
}
if (writer == null)
{
throw new ArgumentNullException("writer", "writer cannot be null.");
}
XmlDocument stylesheet = new XmlDocument();
stylesheet.Load(this.templateStream);
XslCompiledTransform transform = new XslCompiledTransform();
transform.Load(stylesheet);
transform.Transform(model.ToXml(), writer);
}
/// <summary>
/// Disposes of resources used by this instance.
/// </summary>
/// <param name="disposing">A value indicating whether to actively dispose of managed resources.</param>
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
if (this.templateStream != null && this.streamDisposable)
{
this.templateStream.Dispose();
this.templateStream = null;
}
}
this.disposed = true;
}
}
}
}
// ---------------------------------------------------------------------
// EmptyMailModel.cs
// https://github.com/ChadBurggraf/tasty/blob/master/Source/Tasty/EmptyMailModel.cs
// ---------------------------------------------------------------------
namespace Tasty
{
using System;
using System.Runtime.Serialization;
/// <summary>
/// Represents an empty implementation of <see cref="MailModel"/>.
/// </summary>
[DataContract(Namespace = MailModel.XmlNamespace)]
public sealed class EmptyMailModel : MailModel
{
/// <summary>
/// Initializes a new instance of the EmptyMailModel class.
/// </summary>
public EmptyMailModel()
: base()
{
}
}
}