Odesílání emailových zpráv na základě šablony

Tomáš Holan       17. 2. 2014       Komponenty, .NET       6857 zobrazení

Pro odesílání emailové zprávy v .NETu slouží namespace System.Net.Mail a resp. hlavně jeho třídy MailMessage a SmtpClient. Obecný postup je, že pomoci třídy MailMessage nejprve vytvoříme vlastní zprávu a tu následně odešleme pomoci třídy SmtpClient.

Pro odeslání musíme znát adresu SMTP serveru, přes který chceme zprávu odeslat, a musíme mít na něj přístup a nastavená práva. Konfiguraci SMTP klienta lze buď programově nastavit na instanci třídy SmtpClient pomoci vlastností (nejdůležitější jsou Host, Port, Credentials a UseDefaultCredentials) nebo třída použije konfiguraci ze sekce <system.Net>/<mailSettings> konfiguračního app.config nebo web.config souboru.

V konfiguraci lze také SMTP klienta přepnout na pickup directory, což znamená, že se emaily nebudou přímo odesílat, ale jen se uloží  do určené složky jako soubory. Z této složky může emaily odesílat jiný proces nebo lze pickup directory využít např. pro testování, takže se na testovací instanci aplikace nemusíme bát, že se budou rozesílat maily ostrým zákazníkům.

Konfigurace <mailSettings> může vypadat například takto:

<system.net>
  <mailSettings>
    <smtp deliveryMethod="Network" from="noreply@domena.cz">
      <network host="mail.domena.cz" port="25" userName="user" password="password" defaultCredentials="false" />
    </smtp>
  </mailSettings>
</system.net>

Toto API je naprosto obecné a umožňuje odeslat jak textové tak i HTML maily, maily obsahující přílohy (Attachments), zprávu, která je najednou ve více formátech (AlternateViews), i například HTML zprávu obsahující vložené obrázky (LinkedResources).

Velmi často ale potřebujeme odesílat automaticky generované emaily na základě připravené šablony, do které se konkrétní data doplní pouze na určená místa v šabloně (tzv. replacements). Šablonu můžeme mít přitom například buď přímo zabuildovanou v aplikaci jako resource nebo v externím souboru.

Protože je tento typ scénáře dost častý a API třídy MailTemplate je pro něho moc obecné, používám vlastní třídu MailDefinition, která třídu MailMessage pouze interně využívá a dále řeší:

  • Načtení obsahu textové nebo HTML šablony pro emailovou zprávu z externího souboru (vlastnost BodyFileName). Jinak lze obsah šablony předat i přímo v parametru body při volání jednotlivých metod třídy.
  • Načtení subjectu emailové zprávy přímo z elementu <title> HTML šablony (v případě, kdy je výhodné, aby být subject přímo součástí šablony).
  • Doplnění předaných replacements dat jak v těle tak i v subjektu zprávy.
  • Doplnění adresy odesílatele nebo adres příjemců na určená místa v těle nebo subjektu zprávy (například na místo označené jako <%From%>).
  • Třída podporuje přílohy (Attachments) i obrázky vložené do HTML šablony (EmbeddedObjects).

Třída obsahuje metodu CreateMailMessage, která vrací vytvořený objekt MailMessage, a metodu Send pro přímé odeslání zprávy s využitím výchozího nastavení třídy SmtpClient (tj. konfigurace v .config souboru).

Tímto dávám třídu MailDefinition veřejně k dispozici. Její implementace je následující:

using System;
using System.IO;
using System.Linq;
using System.Net.Mail;
using System.Net.Configuration;
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace IMP.Shared
{
    internal class MailDefinition
    {
        #region member types definition
        private enum EmailReplacementType
        {
            From,
            To,
            CC,
            Bcc
        }
        #endregion

        #region member varible and default property initialization
        public string BodyFileName { get; set; }
        public string From { get; set; }
        public string CC { get; set; }
        public string Bcc { get; set; }
        public string Subject { get; set; }
        public bool IsBodyHtml { get; set; }
        public MailPriority Priority { get; set; }

        private List<EmbeddedMailObject> m_EmbeddedObjects;
        #endregion

        #region action methods
        public MailMessage CreateMailMessage(string recipients, ReplacementCollection replacements)
        {
            return CreateMailMessage((string)null, recipients, replacements, null);
        }

        public MailMessage CreateMailMessage(string recipients, ReplacementCollection replacements, IEnumerable<Attachment> attachments)
        {
            return CreateMailMessage((string)null, recipients, replacements, attachments);
        }

        public MailMessage CreateMailMessage(string body, string recipients, ReplacementCollection replacements)
        {
            return CreateMailMessage(body, recipients, replacements, null);
        }

        public MailMessage CreateMailMessage(string body, string recipients, ReplacementCollection replacements, IEnumerable<Attachment> attachments)
        {
            if (body == null && !string.IsNullOrEmpty(this.BodyFileName))
            {
                body = File.ReadAllText(GetFullBodyFileName());
            }

            string from = this.From;
            if (string.IsNullOrEmpty(from))
            {
                from = GetDefaultFrom();
            }
            from = ApplyReplacements(from, replacements);

            if (recipients != null)
            {
                recipients = ApplyEmailReplacement(ApplyReplacements(recipients.Replace(';', ','), replacements), from, EmailReplacementType.From);
            }

            string cc = this.CC;
            if (cc != null)
            {
                cc = ApplyEmailReplacement(ApplyReplacements(cc.Replace(';', ','), replacements), from, EmailReplacementType.From);
            }

            string bcc = this.Bcc;
            if (bcc != null)
            {
                bcc = ApplyEmailReplacement(ApplyReplacements(bcc.Replace(';', ','), replacements), from, EmailReplacementType.From);
            }

            if (!string.IsNullOrEmpty(body))
            {
                //Body replacements
                body = ApplyReplacements(body, replacements, this.IsBodyHtml);
                body = ApplyEmailReplacement(body, from, EmailReplacementType.From, this.IsBodyHtml);
                body = ApplyEmailReplacement(body, recipients, EmailReplacementType.To, this.IsBodyHtml);
                body = ApplyEmailReplacement(body, cc, EmailReplacementType.CC, this.IsBodyHtml);
                body = ApplyEmailReplacement(body, bcc, EmailReplacementType.Bcc, this.IsBodyHtml);
            }

            var message = new MailMessage() { From = new MailAddress(from), IsBodyHtml = this.IsBodyHtml, Priority = this.Priority };

            try
            {
                foreach (var address in ParseRecipients(recipients))
                {
                    message.To.Add(address);
                }
                foreach (var address in ParseRecipients(cc))
                {
                    message.CC.Add(address);
                }
                foreach (var address in ParseRecipients(bcc))
                {
                    message.Bcc.Add(address);
                }

                string subject = this.Subject;
                if (string.IsNullOrEmpty(subject) && this.IsBodyHtml)
                {
                    subject = ExtractSubjectFromHtmlBody(body);
                }

                if (!string.IsNullOrEmpty(subject))
                {
                    //Subject replacements
                    subject = ApplyReplacements(subject, replacements);
                    subject = ApplyEmailReplacement(subject, from, EmailReplacementType.From);
                    subject = ApplyEmailReplacement(subject, recipients, EmailReplacementType.To);
                    subject = ApplyEmailReplacement(subject, cc, EmailReplacementType.CC);
                    subject = ApplyEmailReplacement(subject, bcc, EmailReplacementType.Bcc);

                    message.Subject = subject;
                }

                if (m_EmbeddedObjects != null && m_EmbeddedObjects.Count != 0)
                {
                    message.AlternateViews.Add(GetAlternateView(body));
                }
                else if (!string.IsNullOrEmpty(body))
                {
                    message.Body = body;
                }

                if (attachments != null)
                {
                    foreach (var attachment in attachments)
                    {
                        message.Attachments.Add(attachment);
                    }
                }
            }
            catch
            {
                message.Dispose();
                throw;
            }

            return message;
        }

        public MailMessage CreateMailMessage(Stream body, string recipients, ReplacementCollection replacements)
        {
            return CreateMailMessage(body, recipients, replacements, null);
        }

        public MailMessage CreateMailMessage(Stream body, string recipients, ReplacementCollection replacements, IEnumerable<Attachment> attachments)
        {
            string bodyText = null;
            if (body != null)
            {
                using (var reader = new StreamReader(body))
                {
                    bodyText = reader.ReadToEnd();
                }
            }

            return CreateMailMessage(bodyText, recipients, replacements, attachments);
        }

        public void Send(string recipients, ReplacementCollection replacements)
        {
            using (var msg = CreateMailMessage((string)null, recipients, replacements, null))
            {
                var client = new SmtpClient();
                client.Send(msg);
            }
        }

        public void Send(string recipients, ReplacementCollection replacements, IEnumerable<Attachment> attachments)
        {
            using (var msg = CreateMailMessage((string)null, recipients, replacements, attachments))
            {
                var client = new SmtpClient();
                client.Send(msg);
            }
        }

        public void Send(string body, string recipients, ReplacementCollection replacements)
        {
            using (var msg = CreateMailMessage(body, recipients, replacements, null))
            {
                var client = new SmtpClient();
                client.Send(msg);
            }
        }

        public void Send(string body, string recipients, ReplacementCollection replacements, IEnumerable<Attachment> attachments)
        {
            using (var msg = CreateMailMessage(body, recipients, replacements, attachments))
            {
                var client = new SmtpClient();
                client.Send(msg);
            }
        }

        public void Send(Stream body, string recipients, ReplacementCollection replacements)
        {
            using (var msg = CreateMailMessage(body, recipients, replacements, null))
            {
                var client = new SmtpClient();
                client.Send(msg);
            }
        }

        public void Send(Stream body, string recipients, ReplacementCollection replacements, IEnumerable<Attachment> attachments)
        {
            using (var msg = CreateMailMessage(body, recipients, replacements, attachments))
            {
                var client = new SmtpClient();
                client.Send(msg);
            }
        }
        #endregion

        #region property getters/setters
        public List<EmbeddedMailObject> EmbeddedObjects
        {
            get
            {
                if (m_EmbeddedObjects == null)
                {
                    m_EmbeddedObjects = new List<EmbeddedMailObject>();
                }
                return m_EmbeddedObjects;
            }
            set { m_EmbeddedObjects = value; }
        }
        #endregion

        #region private member functions
        private static string ApplyReplacements(string str, ReplacementCollection replacements, bool IsHtml)
        {
            if (!string.IsNullOrEmpty(str) && replacements != null)
            {
                foreach (var Item in replacements)
                {
                    string Name = Item.Name.StartsWith("<%", StringComparison.Ordinal) ? Item.Name : "<%" + Item.Name + "%>";
                    string Value = Item.Value ?? "";

                    if (IsHtml)
                    {
                        if (Item.HtmlEncoding)
                        {
                            Value = Regex.Replace(System.Web.HttpUtility.HtmlEncode(Value), "\r\n|\r|\n", "<br />");
                        }

                        str = Regex.Replace(str, System.Web.HttpUtility.HtmlEncode(Name), Value, RegexOptions.IgnoreCase);
                    }

                    str = Regex.Replace(str, Name, Value, RegexOptions.IgnoreCase);
                }
            }
            return str;
        }

        private static string ApplyReplacements(string str, ReplacementCollection replacements)
        {
            return ApplyReplacements(str, replacements, false);
        }

        private static string ApplyEmailReplacement(string str, string addresses, EmailReplacementType type, bool isHtml)
        {
            if (!string.IsNullOrEmpty(str))
            {
                switch (type)
                {
                    case EmailReplacementType.From:
                        MailAddress from = null;
                        if (addresses != null)
                        {
                            from = new MailAddress(addresses);
                        }

                        str = Regex.Replace(str, "<%From%>", from == null ? "" : from.Address, RegexOptions.IgnoreCase);
                        str = Regex.Replace(str, "<%FromDisplayName%>", from == null ? "" : string.IsNullOrEmpty(from.DisplayName) ? from.Address : from.DisplayName, RegexOptions.IgnoreCase);
                        str = Regex.Replace(str, "<%FromFullName%>", addresses ?? "", RegexOptions.IgnoreCase);
                        str = Regex.Replace(str, "<%FromFullAddress%>", addresses ?? "", RegexOptions.IgnoreCase);

                        if (isHtml)
                        {
                            str = Regex.Replace(str, "&lt;%From%&gt;", from == null ? "" : from.Address, RegexOptions.IgnoreCase);
                            str = Regex.Replace(str, "&lt;%FromDisplayName%&gt;", from == null ? "" : string.IsNullOrEmpty(from.DisplayName) ? from.Address : from.DisplayName, RegexOptions.IgnoreCase);
                            str = Regex.Replace(str, "&lt;%FromFullName%&gt;", addresses ?? "", RegexOptions.IgnoreCase);
                            str = Regex.Replace(str, "&lt;%FromFullAddress%&gt;", addresses ?? "", RegexOptions.IgnoreCase);
                        }
                        break;
                    case EmailReplacementType.To:
                        var to = ParseRecipients(addresses);

                        str = Regex.Replace(str, "<%Recipients%>", string.Join(",", (from i in to select i.Address).ToArray()), RegexOptions.IgnoreCase);
                        str = Regex.Replace(str, "<%RecipientsDisplayName%>", string.Join(",", (from i in to select string.IsNullOrEmpty(i.DisplayName) ? i.Address : i.DisplayName).ToArray()), RegexOptions.IgnoreCase);
                        str = Regex.Replace(str, "<%RecipientsFullName%>", addresses ?? "", RegexOptions.IgnoreCase);
                        str = Regex.Replace(str, "<%RecipientsAddress%>", addresses ?? "", RegexOptions.IgnoreCase);
                        str = Regex.Replace(str, "<%To%>", string.Join(",", (from i in to select i.Address).ToArray()), RegexOptions.IgnoreCase);
                        str = Regex.Replace(str, "<%ToDisplayName%>", string.Join(",", (from i in to select string.IsNullOrEmpty(i.DisplayName) ? i.Address : i.DisplayName).ToArray()), RegexOptions.IgnoreCase);
                        str = Regex.Replace(str, "<%ToFullName%>", addresses ?? "", RegexOptions.IgnoreCase);
                        str = Regex.Replace(str, "<%ToFullAddress%>", addresses ?? "", RegexOptions.IgnoreCase);

                        if (isHtml)
                        {
                            str = Regex.Replace(str, "&lt;%Recipients%&gt;", string.Join(",", (from i in to select i.Address).ToArray()), RegexOptions.IgnoreCase);
                            str = Regex.Replace(str, "&lt;%RecipientsDisplayName%&gt;", string.Join(",", (from i in to select string.IsNullOrEmpty(i.DisplayName) ? i.Address : i.DisplayName).ToArray()), RegexOptions.IgnoreCase);
                            str = Regex.Replace(str, "&lt;%RecipientsFullName%&gt;", addresses ?? "", RegexOptions.IgnoreCase);
                            str = Regex.Replace(str, "&lt;%RecipientsFullAddress%&gt;", addresses ?? "", RegexOptions.IgnoreCase);
                            str = Regex.Replace(str, "&lt;%To%&gt;", string.Join(",", (from i in to select i.Address).ToArray()), RegexOptions.IgnoreCase);
                            str = Regex.Replace(str, "&lt;%ToDisplayName%&gt;", string.Join(",", (from i in to select string.IsNullOrEmpty(i.DisplayName) ? i.Address : i.DisplayName).ToArray()), RegexOptions.IgnoreCase);
                            str = Regex.Replace(str, "&lt;%ToFullName%&gt;", addresses ?? "", RegexOptions.IgnoreCase);
                            str = Regex.Replace(str, "&lt;%ToFullAddress%&gt;", addresses ?? "", RegexOptions.IgnoreCase);
                        }
                        break;
                    case EmailReplacementType.CC:
                        var cc = ParseRecipients(addresses);

                        str = Regex.Replace(str, "<%CC%>", string.Join(",", (from i in cc select i.Address).ToArray()), RegexOptions.IgnoreCase);
                        str = Regex.Replace(str, "<%CCDisplayName%>", string.Join(",", (from i in cc select string.IsNullOrEmpty(i.DisplayName) ? i.Address : i.DisplayName).ToArray()), RegexOptions.IgnoreCase);
                        str = Regex.Replace(str, "<%CCFullName%>", addresses ?? "", RegexOptions.IgnoreCase);
                        str = Regex.Replace(str, "<%CCFullAddress%>", addresses ?? "", RegexOptions.IgnoreCase);

                        if (isHtml)
                        {
                            str = Regex.Replace(str, "&lt;%CC%&gt;", string.Join(",", (from i in cc select i.Address).ToArray()), RegexOptions.IgnoreCase);
                            str = Regex.Replace(str, "&lt;%CCDisplayName%&gt;", string.Join(",", (from i in cc select string.IsNullOrEmpty(i.DisplayName) ? i.Address : i.DisplayName).ToArray()), RegexOptions.IgnoreCase);
                            str = Regex.Replace(str, "&lt;%CCFullName%&gt;", addresses ?? "", RegexOptions.IgnoreCase);
                            str = Regex.Replace(str, "&lt;%CCFullAddress%&gt;", addresses ?? "", RegexOptions.IgnoreCase);
                        }
                        break;
                    case EmailReplacementType.Bcc:
                        var bcc = ParseRecipients(addresses);

                        str = Regex.Replace(str, "<%Bcc%>", string.Join(",", (from i in bcc select i.Address).ToArray()), RegexOptions.IgnoreCase);
                        str = Regex.Replace(str, "<%BccDisplayName%>", string.Join(",", (from i in bcc select string.IsNullOrEmpty(i.DisplayName) ? i.Address : i.DisplayName).ToArray()), RegexOptions.IgnoreCase);
                        str = Regex.Replace(str, "<%BccFullName%>", addresses ?? "", RegexOptions.IgnoreCase);
                        str = Regex.Replace(str, "<%BccFullAddress%>", addresses ?? "", RegexOptions.IgnoreCase);

                        if (isHtml)
                        {
                            str = Regex.Replace(str, "&lt;%Bcc%&gt;", string.Join(",", (from i in bcc select i.Address).ToArray()), RegexOptions.IgnoreCase);
                            str = Regex.Replace(str, "&lt;%BccDisplayName%&gt;", string.Join(",", (from i in bcc select string.IsNullOrEmpty(i.DisplayName) ? i.Address : i.DisplayName).ToArray()), RegexOptions.IgnoreCase);
                            str = Regex.Replace(str, "&lt;%BccFullName%&gt;", addresses ?? "", RegexOptions.IgnoreCase);
                            str = Regex.Replace(str, "&lt;%BccFullAddress%&gt;", addresses ?? "", RegexOptions.IgnoreCase);
                        }
                        break;
                }
            }

            return str;
        }

        private static string ApplyEmailReplacement(string str, string addresses, EmailReplacementType type)
        {
            return ApplyEmailReplacement(str, addresses, type, false);
        }

        private AlternateView GetAlternateView(string body)
        {
            string mediaType = this.IsBodyHtml ? "text/html" : "text/plain";
            AlternateView item = AlternateView.CreateAlternateViewFromString(body, null, mediaType);

            foreach (var obj in m_EmbeddedObjects)
            {
                string path = obj.FileName;
                if (string.IsNullOrEmpty(path))
                {
                    throw new InvalidOperationException("Missing FileName in EmbeddedMailObject.");
                }
                if (!Path.IsPathRooted(path) && !string.IsNullOrEmpty(this.BodyFileName))
                {
                    string bodyFileName = GetFullBodyFileName();
                    path = Path.Combine(Path.GetDirectoryName(bodyFileName), path);
                }

                LinkedResource resource = new LinkedResource(path) { ContentId = obj.Name };
                try
                {
                    resource.ContentType.Name = Path.GetFileName(path);
                    item.LinkedResources.Add(resource);
                }
                catch
                {
                    resource.Dispose();
                    throw;
                }
            }
            return item;
        }

        private static string ExtractSubjectFromHtmlBody(string body)
        {
            if (body == null)
            {
                return null;
            }

            int index = -1;
            int index2 = -1;
            int index3 = -1;
            var sb = new System.Text.StringBuilder();

            foreach (string line in ReadBodyLines(body))
            {
                if (index == -1)
                {
                    index = line.IndexOf("<head>", StringComparison.OrdinalIgnoreCase);

                    if (index == -1)
                    {
                        continue;
                    }
                }

                if (index2 == -1)
                {
                    index2 = line.IndexOf("<title>", index, StringComparison.OrdinalIgnoreCase);

                    if (index2 == -1)
                    {
                        index2 = line.IndexOf("</head>", index, StringComparison.OrdinalIgnoreCase);

                        if (index2 != -1)
                        {
                            break;
                        }
                        continue;
                    }
                }

                index3 = line.IndexOf("</title>", index2, StringComparison.OrdinalIgnoreCase);

                if (index3 != -1)
                {
                    if (index2 != 0)    //line with "<title>"
                    {
                        sb.Append(line.Substring(index2 + "<title>".Length, index3 - index2 - "<title>".Length));
                        break;
                    }

                    sb.Append(line.Substring(0, index3));
                    break;
                }

                if (index2 != 0)    //line with "<title>"
                {
                    sb.Append(line.Substring(index2 + "<title>".Length));
                    index2 = 0;
                    continue;
                }
                sb.Append(line);
            }

            string subject = System.Web.HttpUtility.HtmlDecode(sb.ToString().Trim());
            return subject.Length == 0 ? null : subject;
        }

        private string GetFullBodyFileName()
        {
            string path = this.BodyFileName;
            if (!Path.IsPathRooted(path))
            {
                string mailTemplateRoot = System.IO.Path.GetDirectoryName((System.Reflection.Assembly.GetEntryAssembly() ?? System.Reflection.Assembly.GetExecutingAssembly()).Location);
                path = Path.Combine(mailTemplateRoot, path);
            }

            return path;
        }

        private static IEnumerable<string> ReadBodyLines(string body)
        {
            using (var reader = new StringReader(body))
            {
                while (true)
                {
                    string line = reader.ReadLine();
                    if (line == null)
                    {
                        yield break;
                    }

                    yield return line;
                }
            }
        }

        private static string GetDefaultFrom()
        {
            var smtpCfg = (SmtpSection)System.Configuration.ConfigurationManager.GetSection("system.net/mailSettings/smtp");
            if (smtpCfg == null || smtpCfg.Network == null || string.IsNullOrEmpty(smtpCfg.From))
            {
                throw new InvalidOperationException("From address is not specified.");
            }
            return smtpCfg.From;
        }

        private static IEnumerable<MailAddress> ParseRecipients(string addresses)
        {
            if (string.IsNullOrEmpty(addresses))
            {
                yield break;
            }

            for (int i = 0; i < addresses.Length; i++)
            {
                yield return ParseMailAddress(addresses, ref i);
            }
        }

        private static MailAddress ParseMailAddress(string addresses, ref int i)
        {
            int index = addresses.IndexOfAny(new[] { '"', ',' }, i);
            if (index != -1 && addresses[index] == '"')
            {
                index = addresses.IndexOf('"', index + 1);
                if (index == -1)
                {
                    throw new FormatException("Invalid mail address format");
                }

                index = addresses.IndexOf(',', index + 1);
            }

            if (index == -1)
            {
                index = addresses.Length;
            }

            var address = new MailAddress(addresses.Substring(i, index - i).Trim(' ', '\t'));
            i = index;
            return address;
        }
        #endregion
    }

    #region EmbeddedMailObject
    internal sealed class EmbeddedMailObject
    {
        #region member varible and default property initialization
        public string Name { get; set; }
        public string FileName { get; set; }
        #endregion

        #region constructors and destructors
        public EmbeddedMailObject(string name, string fileName)
        {
            this.Name = name;
            this.FileName = fileName;
        }
        #endregion
    }
    #endregion

    #region Replacement, ReplacementCollection
    internal class Replacement
    {
        #region constructors and destructors
        internal Replacement()
        {
            this.HtmlEncoding = true;
        }
        #endregion

        #region member varible and default property initialization
        public string Name { get; set; }

        public string Value { get; set; }

        public bool HtmlEncoding { get; set; }
        #endregion

        #region action methods
        public override string ToString()
        {
            return string.Format(System.Globalization.CultureInfo.CurrentCulture, "{0}: {1})", this.Name, this.Value);
        }
        #endregion
    }

    internal class ReplacementCollection : IEnumerable<Replacement>, System.Collections.IEnumerable
    {
        #region member varible and default property initialization
        private Dictionary<string, Replacement> ReplacementsDictionary;
        #endregion

        #region constructors and destructors
        public ReplacementCollection()
        {
            this.ReplacementsDictionary = new Dictionary<string, Replacement>();
        }
        #endregion

        #region action methods
        public void Add(string name, string value)
        {
            if (name == null)
            {
                throw new ArgumentNullException("name");
            }
            if (name.Length == 0)
            {
                throw new ArgumentException("name is empty string.", "name");
            }

            this.ReplacementsDictionary.Add(name, new Replacement() { Name = name, Value = value });
        }

        public void Add(string name, string value, bool htmlEncoding)
        {
            if (name == null)
            {
                throw new ArgumentNullException("name");
            }
            if (name.Length == 0)
            {
                throw new ArgumentException("name is empty string.", "name");
            }

            this.ReplacementsDictionary.Add(name, new Replacement() { Name = name, Value = value, HtmlEncoding = htmlEncoding });
        }

        public bool Remove(string name)
        {
            return this.ReplacementsDictionary.Remove(name);
        }

        public IEnumerator<Replacement> GetEnumerator()
        {
            return this.ReplacementsDictionary.Values.GetEnumerator();
        }
        #endregion

        #region property getters/setters
        public int Count
        {
            get { return this.ReplacementsDictionary.Count; }
        }
        #endregion

        #region IEnumerable Members
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
        #endregion
    }
    #endregion
}

Třída MailDefinition je k dispozici také ke stažení zde

A ještě si ukážeme příklad použití. Předpokládejme například následující HTML šablonu emailové zprávy:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <style type="text/css">
        p.MsoNormal {
            margin-top: 0cm;
            margin-right: 0cm;
            margin-bottom: 10.0pt;
            margin-left: 0cm;
            line-height: 115%;
            font-size: 11.0pt;
            font-family: "Calibri","sans-serif";
        }
        a:link {
            color: blue;
            text-decoration: underline;
            text-underline: single;
        }
    </style>
    <title>Potvrzení přijetí objednávky č. <%CisloObjednavky%></title>
</head>
<body>
    <p class="MsoNormal">
        Dobrý den,<br />
	 děkujeme, že jste nás kontaktovali.<br />
        Toto je automatická zpráva potvrzující přijetí vaší objednávky. Objednávka je registrovana pod číslem &lt;%CisloObjednavky%&gt;.</p>
    <p class="MsoNormal"></p>
    <p class="MsoNormal">
        <b style="mso-bidi-font-weight:normal">Objednávka byla přijata s&nbsp;těmito údaji:</b></p>
    <p class="MsoNormal">
        Číslo objednávky: <span style="mso-tab-count:1">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
        &lt;%CisloObjednavky%&gt;</span><br />
        Stav objednávky:<span style="mso-tab-count:1">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
        &lt;%StavObjednavky%&gt;</span></p>
    <p class="MsoNormal"></p>
    <p class="MsoNormal">
        Stav objednávky můžete sledovat přímo v systému na adrese: &lt;%Link%&gt; </p>
    <p class="MsoNormal"></p>
    <p class="MsoNormal">
        Děkujeme za Vaši důvěru a těšíme se na další spolupráci.</p>
    <p class="MsoNormal">
        S pozdravem,<br />
        Obchodní tým</p>
    <p class="MsoNormal">
        Tento e-mail je generován automaticky. Prosím, neodpovídejte na něj (adresa
        <a href="mailto:&lt;%From%&gt;">&lt;%From%&gt;</a> není monitorována).</p>
</body>
</html>

Pokud budeme mít tuto šablonu umístěnou přímo v resource aplikace (pod identifikátorem PrijetiObjednavkyKlient) bude kód pro odeslání emailové zprávy například následující:

var data = GetObjednavkaData(IDObjednavky);

//Odeslání emailové zprávy klientovi
var replacements = new ReplacementCollection()
{ 
    { "CisloObjednavky", data.CisloObjednavky.ToString() },
    { "StavObjednavky", data.StavObjednavky },
    { "Link", WebApplicationUtil.GetAbsoluteUrl("~/") }
};
var recipients = new System.Net.Mail.MailAddress(data.Email, data.KontaktniOsoba).ToString();

var maildef = new MailDefinition() { IsBodyHtml = true };
maildef.Send(Properties.Resources.PrijetiObjednavkyKlient, recipients, replacements);

Metoda GetObjednavkaData zde vrací objekt obsahující vlastnosti CisloObjednavky, StavObjednavky, Email a KontaktniOsoba s konkrétními daty pro mail. Pomocná třída WebApplicationUtil resp. její metoda GetAbsoluteUrl zde vrací absolutní URL výchozí stránky aktuální web aplikace.

Třída WebApplicationUtil je ke stažení k dispozici zde

 

hodnocení článku

0 bodů / 2 hlasů       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

Šablony

Toto se mi hrubě nelíbí. Proč není použit nějaký osvědčený systém šablon, ale pochybné vlastní řešení? Ideální bych viděl T4 Text Templates.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Způsob použitý v tomto článku (replacements apod.) ideově vycházel z toho, jak to bylo uděláno přímo součástí ASP.NET WebForms viz. třída MailDefinition z namespace System.Web.UI.WebControls.

http://msdn.microsoft.com/en-us/library/...

Možnosti této implementace z ASP.NET WebForms byly ale značně omezené.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Taky si dovolím

Co když prosté nahrazování textů nebude stačit ? Co když budu chtít emailu rozpis celé objednávky s řádky, cenami a rekapitulací DPH ? Není prostě lepší využít nějakého hotového šablonovacího nářadí (v podobném scénáři jsem prostě použil Razor a osvědčilo se). Tohle mě přece první požadavek na podobné rozšíření funkčnosti donutí to buď napsat znovu (už s nějakým template engine) nebo "bastlit".

nahlásit spamnahlásit spam 1 / 1 odpovědětodpovědět

Razor je samozřejmě také možnost. Otazkou je, jestli naopak na jednodušší scénáře nebude Razor trochu "overkill" (ale třeba ne).

Řešení s replacements je velmi jednoduché a přitom je i docela mocné. Ty řádky objednávky bych např. řešil tak, že při odesílání mailu vygeneruji HTML tabulku, která se do šablony vloží celá jako jeden replacement.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

To je právě ono - to je přesně první úprava která člověka napadne a za kterou si musí hned sám nafackovat. Budete tak implementovat další mechanizmus který bude do html vkládat html. Šablona a její čitelnost a znovupoužitelnost pak jde úplne do háje. Pokud šablonujete, je potřeba mít na jednom místě šablonu a vedle model(data) - forma/obsah.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Souhlasím - raději jsme použili mailové šablony přes Razor než si něco bastlit sami. Použití je naprosto jednoduché a vyžadovalo minimum našeho kódu.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Návrhy

Ahoj, velmi praktický článek.

Dovolím si dovolím pár návrhů/dotazů:

- proč je vše internal?

- dokážu si představit, že budu potřebovat přílohy i jiné, než jsou jen soubory - z toho důvodu bych nahradil sealed class EmbeddedMailObject za rozhraní:

public interface IEmbeddedMailObject {
  string Name { get; }
  AttachmentBase ResolveAttachment();
}
nahlásit spamnahlásit spam 0 odpovědětodpovědět

Ahoj,

Internal je to pro použití přímo jako třída vložená do projektu, aby to zbytečně "nezaclánělo" na public interface assembly, která to jen interně využívá. Pokud by se to dávalo do knihovny, je to samozřejmě potřeba změnit na public.

Ano, dobrá připomínka. Ta třída teď podporuje jen přílohy ze souboru, chtělo by to zobecnit, buď tím interface nebo přímo v té implementaci udělat např. třídy FileEmbeddedMailObject, StreamEmbeddedMailObject apod.

nahlásit spamnahlásit spam 0 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:  
Heslo:  

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback