Home .NET ASP.NET MVC Lesson A. Notification and mailing

ASP.NET MVC Lesson A. Notification and mailing

by admin

Lesson Objective To understand sending emails and confirmation texts. MailNotify, using a configuration file. Mailing through the creation of a separate thread.

SmtpClient and MailNotify

When designing a website, sooner or later we are faced with interacting with email, whether it’s activating a user, reminding or resetting a password, or creating a mailing list.
Let’s define what we need to do this :

  • The class that will send out emails
  • The smtp configuration is taken from IConfig
  • Errors of sending an email are logged
  • Have a parameter that turns off mail operation, so that you don’t send out some crap when you’re working with a customer battlebase.

Let’s create a static class and call it 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);}}}

Let’s take a closer look at :

  • IConfig is statically initialized from DependencyResolver as needed
  • If the EnableMain flag is set, then we start working with the mail, otherwise we just write the mail to the log file
  • If MailAddress is not specified, it is initialized with the data from the config
  • SmtpClient is initialized according to the data in the configuration
  • Body of the email – html
  • Encoding – UTF8
  • If there is an error when sending, we put Exception.Message in the log (we can gather more information here, but we don’t need it yet).

Consider sending mails by template. Create a class (also static) 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));}}}

We get the config in the same way. When we send it out, we specify for it, and then we use Func<string, string> to form the subject and the body of the mail.
Notify the user about registration using the Register template from Web.config:

<add name="Register" subject="Register for {0}" template="Hello! <br/> <br/> Go to <a href='http://{1}/User/Activate/{0}'> http://{1}/User/Activate/{0}</a> to confirm your mailbox.<br/> -----<br/> Respectfully, team <a href='http://{1}'> {1}</a> "/>

Notice how the html tags need to be escaped in order to do the pattern correctly. We need to consider the dependence between the template for string.Format() and the number of parameters. In UserController.cs let’s add (/Areas/Default/Controllers/UserController.cs:Register) when registering:

Repository.CreateUser(user);NotifyMail.SendNotify("Register", user.Email, subject => string.Format(subject, HostName), body => string.Format(body, "", HostName));return RedirectToAction("Index");

HostName we added in the BaseController initialization (/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;} …

Sign up, and we get an email :
ASP.NET MVC Lesson A. Notification and mailing

More complicated case

This is all good, but if we need a newsletter with a bunch of promotional offers, this format won’t work for us. First of all, it’s hard to specify such a template in Web.config, and secondly, the number of parameters is unknown. Just like with the regular html templates, it would be nice to set up a letter template in View. Well, consider the ActionMailer library ( http://nuget.org/packages/ActionMailer ):

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

Let’s inherit MailController from MailerBase:

public class MailController : MailerBase{public EmailResult Subscription(string message, string email){To.Add(email);Subject = "Mailing List";MessageEncoding = Encoding.UTF8;return Email("Subscription", message);}}

Adding 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>

Add to Web.config the configuration for working with mail (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>

And create a test method in UserController.cs (/Areas/Default/Controllers/UserController.cs):

[Authorize]public ActionResult SubscriptionTest(){var mailController = new MailController();var email = mailController.Subscription("Hello, world!", CurrentUser.Email);email.Deliver();return Content("OK");}

Run :
localhost/User/SubscriptionTest – and we get an email.
Let’s look at an example of getting the text of a letter into a string. You will need StreamReader (/Areas/Default/Controllers/UserController.cs):

[Authorize]public ActionResult SubscriptionShow(){var mailController = new MailController();var email = mailController.Subscription("Hello, world!", CurrentUser.Email);using (var reader = new StreamReader(email.Mail.AlternateViews[0].ContentStream)){var content = reader.ReadToEnd();return Content(content);}return null;}

The content already has a generated page. Run :
localhost/User/SubscriptionShow

SmsNotify

In this chapter we’ll take a look at the interaction with sms, not just mails. But there is a nuance – the access to the mailing is provided by separate services, and here we’ll consider only the basic principles of writing a module for working with SMS-providers on the example of working with unisender.ru.
Let’s create a settings class like 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;}}}

Set in Web.Config (Web.config):

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

Create the SmsSender class (/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 properlywebRequest.ContentType = "application/x-www-form-urlencoded";webRequest.Method = "POST";webRequest.KeepAlive = false;webRequest.PreAuthenticate = false;string postData = "format=jsonapi_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 = newStreamReader(webResponse.GetResponseStream(), enc);string Response = loResponseStream.ReadToEnd();return Response;}catch (Exception ex){logger.ErrorException("Error sending SMS", ex);return "Error sending SMS";}}}

The result comes in like :

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

It can be taken apart and analyzed.
In the next lesson, we’ll look at how to work with json.

Separate flow

If we send out emails to a large number of people, the processing can take a long time. I use the following principle for this :

  • Create a separate thread that checks if outgoing mail is ready to be sent
  • When creating a mailing list, the e-mails are created and written to the database
  • The thread checks the state of the database for the mails
  • Emails are fetched from the database sequentially (an email can be deleted, it can only zero the contents of an email (to save the size of the database).
  • Mail is sent.
  • Returns to check.

A separate thread is started in Application_Start. The timer is set to repeat after 1 minute :

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

Consider the MailProcessor class (but we won’t create it):

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) – sends the next mail, if there is no mail, returns false
MainThread is waiting for execution MailThread and takes a break for one minute. And then. If there are no new emails in the database, then we smoke for one minute.
All the sources are at https://bitbucket.org/chernikov/lessons

You may also like