- PVSM.RU - https://www.pvsm.ru -

ASP.NET MVC Урок A. Уведомление и рассылка

Цель урока Разобраться в отправлении писем и подтверждающих смс. MailNotify, использование конфигурационного файла. Рассылка через создание отдельного потока.

SmtpClient и MailNotify

При разработке сайта мы рано или поздно сталкиваемся с взаимодействием с электронной почтой, будь то активация пользователя, напоминание или сброс пароль, или создание рассылки.
Определимся, что нам для этого нужно:

  • Класс, который будет рассылать письма
  • Конфигурация smtp берется из IConfig
  • Ошибки отправки письма протоколируются
  • Наличие параметра, выключающего работу почты, дабы при работе с боевой базой клиентов не разослать какой-то треш.


Создадим статический класс, назовем его MailSender (/Tools/Mail/MailSender.cs):

public static class MailSender
    {
        private static IConfig _config;

        public static IConfig Config
        {
            get
            {
                if (_config == null)
                {
                    _config = (DependencyResolver.Current).GetService<IConfig>();

                }
                return _config;
            }
        }

        private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();

        public static void SendMail(string email, string subject, string body, MailAddress mailAddress = null)
        {

            try
            {
                if (Config.EnableMail)
                {
                    if (mailAddress == null)
                    {
                        mailAddress = new MailAddress(Config.MailSetting.SmtpReply, Config.MailSetting.SmtpUser);
                    }
                    MailMessage message = new MailMessage(
                        mailAddress,
                        new MailAddress(email))
                                              {
                                                  Subject = subject,
                                                  BodyEncoding = Encoding.UTF8,
                                                  Body = body,
                                                  IsBodyHtml = true,
                                                  SubjectEncoding = Encoding.UTF8
                                              };
                    SmtpClient client = new SmtpClient
                                            {
                                                Host = Config.MailSetting.SmtpServer,
                                                Port = Config.MailSetting.SmtpPort,
                                                UseDefaultCredentials = false,
                                                EnableSsl = Config.MailSetting.EnableSsl,
                                                Credentials =
                                                    new NetworkCredential(Config.MailSetting.SmtpUserName,
                                                                          Config.MailSetting.SmtpPassword),
                                                DeliveryMethod = SmtpDeliveryMethod.Network
                                            };
                    client.Send(message);
                }
                else
                {
                    logger.Debug("Email : {0} {1} t Subject: {2} {3} Body: {4}", email, Environment.NewLine, subject, Environment.NewLine, body);
                }
            }
            catch (Exception ex)
            {
                logger.Error("Mail send exception", ex.Message);
            }
        }
    }

Рассмотрим подробнее:

  • По необходимости, статически инициализируется IConfig из DependencyResolver
  • Если установлен флаг EnableMain, то начинаем работу с почтой, иначе просто письмо запишем в лог-файл
  • Если MailAddress не указан, то он инициализируется по данным из конфига
  • SmtpClient инициализируется по данным из конфига
  • Тело письма – html
  • Кодировка – UTF8
  • Если при отправке произошла ошибка, то запишем Exception.Message в лог (тут можно и больше информации собирать, но пока нет необходимости).

Рассмотрим рассылку писем по шаблону. Создадим класс (тоже статический) NotifyMail (/Tools/Mail/NotifyMail.cs):

public static class NotifyMail
    {
        private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();

        private static IConfig _config;

        public static IConfig Config
        {
            get
            {
                if (_config == null)
                {
                    _config = (DependencyResolver.Current).GetService<IConfig>();

                }
                return _config;
            }
        }

        public static void SendNotify(string templateName, string email,
            Func<string, string> subject,
            Func<string, string> body)
        {
            var template = Config.MailTemplates.FirstOrDefault(p => string.Compare(p.Name, templateName, true) == 0);
            if (template == null)
            {
                logger.Error("Can't find template (" + templateName + ")");
            }
            else
            {
                MailSender.SendMail(email,
                    subject.Invoke(template.Subject),
                    body.Invoke(template.Template));
            }
        }
    }

Аналогично получаем конфиг. При рассылке мы указываем для неё, и дальше используем Func<string,string> для формирования темы и тела письма.

Уведомим пользователя о регистрации, используя шаблон Register из Web.config:

<add name="Register" subject="Регистрация на {0}" template="Здравствуйте! <br/><br/> Перейдите по ссылке  <a href='http://{1}/User/Activate/{0}'>http://{1}/User/Activate/{0}</a>, чтобы подтвертить свой почтовый ящик.<br/>-----<br/>С уважением, команда <a href='http://{1}'>{1}</a>" />

Заметим, как необходимо экранировать html-теги, чтобы правильно сделать шаблон. Нужно учитывать зависимость между шаблоном для string.Format() и количеством параметров. В UserController.cs при регистрации добавим (/Areas/Default/Controllers/UserController.cs:Register):

Repository.CreateUser(user);

                NotifyMail.SendNotify("Register", user.Email,
                    subject => string.Format(subject, HostName),
                    body => string.Format(body, "", HostName));

                return RedirectToAction("Index");

HostName мы добавили в инициализации BaseController (/Controllers/BaseController.cs):

       public static string HostName = string.Empty;
protected override void Initialize(System.Web.Routing.RequestContext requestContext)
        {
            if (requestContext.HttpContext.Request.Url != null)
            {
                HostName = requestContext.HttpContext.Request.Url.Authority;
            } …

Регистрируемся, и на нашу почту приходит письмо:

ASP.NET MVC Урок A. Уведомление и рассылка

Более сложный случай

Всё это хорошо, но если нам необходимо рассылку с кучей акционных предложений, то данный формат нам не подойдет. Во-первых, сложно подобный шаблон задавать в Web.config, во-вторых, количество параметров не известно. Как и обычные html-шаблоны, шаблон письма было бы чудесно задать во View. Что ж, рассмотрим библиотеку ActionMailer (http://nuget.org/packages/ActionMailer [1]):

PM> Install-Package ActionMailer
Successfully installed 'ActionMailer 0.7.4'.
Successfully added 'ActionMailer 0.7.4' to LessonProject.Model.

Отнаследуем MailController от MailerBase:

public class MailController : MailerBase
    {
        public EmailResult Subscription(string message, string email)
        {
            To.Add(email);
            Subject = "Рассылка";
            MessageEncoding = Encoding.UTF8;
            return Email("Subscription", message);
        }

    }

Добавим Subscription.html.cshtml View (/Areas/Default/Views/Mail/Subscription.html.cshtml):

@model string
@{
    Layout = null;
}
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
   
</head>
<body>
    <div>
        <h1>@Model</h1>
    </div>
</body>
</html>

Добавляем в Web.config конфигурацию для работы с почтой (Web.config):

<system.net>
    <mailSettings>
      <smtp deliveryMethod="Network" from="lxndrpetrov@gmail.com">
        <network host="smtp.gmail.com" port="587" userName="lxndrpetrov" password="******" enableSsl="true" />
      </smtp>
    </mailSettings>
  </system.net>

И создаем в UserController.cs тестовый метод (/Areas/Default/Controllers/UserController.cs):

[Authorize]
        public ActionResult SubscriptionTest()
        {
            var mailController = new MailController();
           
            var email = mailController.Subscription("Привет, мир!", CurrentUser.Email);
            email.Deliver();
            return Content("OK");
        }

Запускаем:

localhost/User/SubscriptionTest [2] — и получаем на почту письмо.

Рассмотрим пример получения текста письма в строку. Для этого понадобится StreamReader (/Areas/Default/Controllers/UserController.cs):

[Authorize]
        public ActionResult SubscriptionShow()
        {
            var mailController = new MailController();
            var email = mailController.Subscription("Привет, мир!", CurrentUser.Email);

            using (var reader = new StreamReader(email.Mail.AlternateViews[0].ContentStream))
            {
                var content = reader.ReadToEnd();
                return Content(content);
            }
            return null;
        }

В content уже есть сформированная страница. Запускаем:
localhost/User/SubscriptionShow [3]

SmsNotify

В этой главе рассмотрим взаимодействие с помощью смс, а не только почты. Но есть ньюанс – доступ к рассылке предоставляется отдельными сервисами, и тут мы рассмотрим только основные принципынаписания модуля для работы с SMS-провайдерами на примере работы с unisender.ru.
Создадим класс настроек по типу MailSetting (/Global/Config/SmsSetting.cs):

public class SmsSetting : ConfigurationSection
    {
        [ConfigurationProperty("apiKey", IsRequired = true)]
        public string APIKey
        {
            get
            {
                return this["apiKey"] as string;
            }
            set
            {
                this["apiKey"] = value;
            }
        }

        [ConfigurationProperty("sender", IsRequired = true)]
        public string Sender
        {
            get
            {
                return this["sender"] as string;
            }
            set
            {
                this["sender"] = value;
            }
        }

        [ConfigurationProperty("templateUri", IsRequired = true)]
        public string TemplateUri
        {
            get
            {
                return this["templateUri"] as string;
            }
            set
            {
                this["templateUri"] = value;
            }
        }
    } 

Зададим в Web.Config (Web.config):

<configSections>
…
   <section name="smsConfig" type="LessonProject.Global.Config.SmsSetting, LessonProject" />
  </configSections>
…
<smsConfig
    apiKey="*******"
    sender="Daddy"
    templateUri="http://api.unisender.com/ru/api/sendSms"
  />
</configuration>

Создадим класс SmsSender (/Tools/Sms/SmsSender.cs):

public static class SmsSender
    {
        private static IConfig _config;

        public static IConfig Config
        {
            get
            {
                if (_config == null)
                {
                    _config = (DependencyResolver.Current).GetService<IConfig>();

                }
                return _config;
            }
        }

        private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();

        public static string SendSms(string phone, string text)
        {
            if (!string.IsNullOrWhiteSpace(Config.SmsSetting.APIKey))
            {
                return GetRequest(phone, Config.SmsSetting.Sender, text);
            }
            else
            {
                logger.Debug("Sms t Phone: {0} Body: {1}", phone, text);
                return "Success";
            }
        }

        private static string GetRequest(string phone, string sender, string text)
        {
            try
            {
                HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(Config.SmsSetting.TemplateUri);
                /// important, otherwise the service can't desirialse your request properly
                webRequest.ContentType = "application/x-www-form-urlencoded";
                webRequest.Method = "POST";
                webRequest.KeepAlive = false;
                webRequest.PreAuthenticate = false;

                string postData = "format=json&api_key=" + Config.SmsSetting.APIKey + "&phone=" + phone
                    + "&sender=" + sender + "&text=" + HttpUtility.UrlEncode(text);
                var ascii = new ASCIIEncoding();
                byte[] byteArray = ascii.GetBytes(postData);
                webRequest.ContentLength = byteArray.Length;
                Stream dataStream = webRequest.GetRequestStream();
                dataStream.Write(byteArray, 0, byteArray.Length);
                dataStream.Close();

                WebResponse webResponse = webRequest.GetResponse();

                Stream responceStream = webResponse.GetResponseStream();
                Encoding enc = System.Text.Encoding.UTF8;
                StreamReader loResponseStream = new
                        StreamReader(webResponse.GetResponseStream(), enc);

                string Response = loResponseStream.ReadToEnd();
                return Response;
            }
            catch (Exception ex)
            {
                logger.ErrorException("Ошибка при отправке SMS", ex);
                return "Ошибка при отправке SMS";
            }
        }
}

Результат приходит типа:

{"result":{"currency":"RUB","price":"0.49","sms_id":"1316886153.2_79859667475"}}

Его можно разобрать и проанализировать.
В следующем уроке мы рассмотрим, как работать с json.

Отдельный поток

Если мы рассылаем электронную почту большому количеству людей, то обработка может занять много времени. Для этого я пользуюсь следующим принципом:

  • Создаем отдельный поток, который проверяет, если ли исходящие письма готовые к отправке
  • При создании рассылки создаются письма и записываются в БД
  • Поток проверяет состояние БД на наличие писем
  • Письма извлекаются из БД последовательно (письмо может удалиться, может только обнулить содержимое письма (чтоб сэкономить размер БД).
  • Письмо отправляется.
  • Возвращается к проверке.

Отдельный поток запускается в Application_Start. Таймер устанавливается на повторение через 1 минуту:

public class MvcApplication : System.Web.HttpApplication
    {
        private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();

        private Thread mailThread { get; set; }

        protected void Application_Start()
        {
            var adminArea = new AdminAreaRegistration();
            var adminAreaContext = new AreaRegistrationContext(adminArea.AreaName, RouteTable.Routes);
            adminArea.RegisterArea(adminAreaContext);

            var defaultArea = new DefaultAreaRegistration();
            var defaultAreaContext = new AreaRegistrationContext(defaultArea.AreaName, RouteTable.Routes);
            defaultArea.RegisterArea(defaultAreaContext);

            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            mailThread = new Thread(new ThreadStart(ThreadFunc));
            mailThread.Start();
        }

        private static void ThreadFunc()
        {
            while (true)
            {
                try
                {
                    var mailThread = new Thread(new ThreadStart(MailThread));
                    mailThread.Start();
                    logger.Info("Wait for end mail thread");
                    mailThread.Join();
                    logger.Info("Sleep 60 seconds");
                }
                catch (Exception ex)
                {
                    logger.ErrorException("Thread period error", ex);
                }
                Thread.Sleep(60000);
            }
        }

        private static void MailThread()
        {
            var repository = DependencyResolver.Current.GetService<IRepository>();
            while (MailProcessor.SendNextMail(repository)) { }
        }
    }

Рассмотрим класс MailProcessor (но не будем его создавать):

public class MailProcessor
    {
        public static bool SendNextMail(IRepository repository)
        {
            var mail = repository.PopMailQueue();
            if (mail != null)
            {
                MailSender.SendMail(mail.Email, mail.Subject, mail.Body);
                return true;
            }
            return false;
        }
}

MailProcessor.SendNextMail(repository) – посылает следующее письмо, если писем нет – возвращает false
Поток MainThread дожидается выполнение MailThread и делает перекур на одну минуту. И далее. Если в БД новых писем нет – дальше курим одну минуту.

Все исходники находятся по адресу https://bitbucket.org/chernikov/lessons [4]

Автор: chernikov

Источник [5]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/asp-net-mvc/31665

Ссылки в тексте:

[1] http://nuget.org/packages/ActionMailer: http://nuget.org/packages/ActionMailer

[2] localhost/User/SubscriptionTest: http://localhost/User/SubscriptionTest

[3] localhost/User/SubscriptionShow: http://localhost/User/SubscriptionShow

[4] https://bitbucket.org/chernikov/lessons: https://bitbucket.org/chernikov/lessons

[5] Источник: http://habrahabr.ru/post/176075/