Цель урока Разобраться в отправлении писем и подтверждающих смс. 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;
} …
Регистрируемся, и на нашу почту приходит письмо:
Более сложный случай
Всё это хорошо, но если нам необходимо рассылку с кучей акционных предложений, то данный формат нам не подойдет. Во-первых, сложно подобный шаблон задавать в Web.config, во-вторых, количество параметров не известно. Как и обычные html-шаблоны, шаблон письма было бы чудесно задать во View. Что ж, рассмотрим библиотеку ActionMailer (http://nuget.org/packages/ActionMailer):
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 — и получаем на почту письмо.
Рассмотрим пример получения текста письма в строку. Для этого понадобится 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
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
Автор: chernikov