Skip to main content

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()
        {
        }
    }
}