-
Notifications
You must be signed in to change notification settings - Fork 177
MvcMailer Step by Step Guide
MvcMailer provides you with an ActionMailer style email sending NuGet Package for ASP.Net MVC 3/4. So, you can produce professional looking emails composed of your MVC master pages and views with ViewBag.
Because you want to:
- Write clean code to send emails instead of spaghetti code
- Reuse the power of master pages, views and data
- Easily write unit test for email sending code
- Send multi-part emails
- Do some or all the above painlessly.
Now that you are convinced, please install the package!
PM> install-package MvcMailer
PM> Install-Package MvcScaffolding -Version 1.0.8-vs2013 -Pre
...
PM> install-package MvcMailer-vs2013 -pre
See more details as to why there are two options here and here.
Scaffold is your friend that produces your mailer with master pages and views. Run the following command in your Package Manager console:
PM> Scaffold Mailer.Razor UserMailer Welcome,PasswordReset
You should see the following:
Added MyScaffolder output 'Mailers\IUserMailer.cs'
Added MyScaffolder output 'Mailers\UserMailer.cs'
Added MyScaffolder output 'Views\UserMailer\_Layout.cshtml'
Added MyScaffolder output 'Views\UserMailer\Welcome.cshtml'
Added MyScaffolder output 'Views\UserMailer\PasswordReset.cshtml'
Mailers\IUserMailer.cs defines your mailer interface with two methods - Welcome and PasswordReset. Mailers\UserMailer.cs is your Mailer class that implements IUserMailer and extends MailerBase. MailerBase extends ControllerBase so that your mailer is just like your controller.
Your already got meaningful code in UserMailer. For example, take a look at the following Welcome() method:
public virtual MvcMailMessage Welcome()
{
//ViewBag.Data = someObject;
return Populate(x => {
x.ViewName = "Welcome";
x.To.Add("[email protected]");
x.Subject = "Welcome";
});
}
Typically, you will edit this method body and pass models to your views, as you do in your controllers. You can use ViewData, ViewBag and strongly typed Models with your views and master pages. Views\UserMailer contains your master page and email views.
However, if you need more mailers, just use the scaffold command as follows:
PM> Scaffold Mailer.Razor CommentMailer CommentPosted,Liked
PM> Scaffold Mailer.Razor ReportMailer ReportProduced,ReportSent,ReportLoading
When you install MvcMailer it automatically sets the Scaffolder called Mailer to one of Mailer.Razor or Mailer.Aspx depending on the project files. So, if you are using Aspx/Razor view engine, your scaffolder will produce aspx and razor views respectively.
When scaffolding, you can provide a switch -NoInterface if you don't like the interface for your mailers. For example the following one will not create IMyMailer,
PM> Scaffold Mailer.Razor MyMailer Welcome -NoInterface
MvcMailer already added the following smtp configuration section in your Web.config file. Open web.config and you will see the following:
<!-- Method#1: Configure smtp server credentials -->
<smtp from="[email protected]">
<network enableSsl="true" host="smtp.gmail.com" port="587" userName="[email protected]" password="valid-password" />
</smtp>
You can use the credentials from a Gmail account or use your favorite SMTP client - just specify the values in the config file and you are good to go.
In case you want to drop the emails in a local folder, just uncomment Method 2 and comment out method 1.
<!-- Method#2: Dump emails to a local directory -->
<smtp from="[email protected]" deliveryMethod="SpecifiedPickupDirectory">
<specifiedPickupDirectory pickupDirectoryLocation="<email_directory_path>"/>
</smtp>
In case you are new to the .Net Mail library, this configuration is used by the System.Net.Mail and there is nothing specific to MvcMailer.
You will edit your mailer method and pass useful data to views. For example, you can do the following:
public virtual MvcMailMessage Welcome()
{
ViewBag.Name = "Sohan";
return Populate(x =>{
x.viewName = "Welcome";
x.To.Add("[email protected]");
});
}
From your Mailer, you can use the following ways to pass data to view:
- Using ViewBag
ViewBag.Name = "Sohan";
ViewBag.Comment = myComment;
- Using ViewData
ViewData["Name"] = "Sohan";
ViewData["Comment"] = myComment;
- Using Strongly Typed Model
var comment = new Comment {From = me, To = you, Message = "Great Work!"};
ViewData = new ViewDataDictionary(comment);
- Using Strongly Typed Model with default HtmlHelper use @Html.DisplayFor(model => model.Property) in your view
var comment = new Comment {From = me, To = you, Message = "Great Work!"};
ViewData.Model = comment
Edit Views/UserMailer/_Layout.cshtml to write your email master page.
<html>
<head></head>
<body>
<h1>MvcMailer</h1>
@RenderBody()
<br />
---
<br />
Thank you!
</body>
</html>
Edit Views/UserMailer/Welcome.cshtml to write your email content that goes inside the master page:
Hello @ViewBag.Name:<br />
Welcome to MvcMailer and enjoy your time!<br />
MvcMailer scaffolder defaults to your project's preferred view engine. So, if you're using ASPX, you will see .Master and .ASPX views instead of the .cshtml ones - but the flavor should be same!
# This will set Mailer scaffolder to Razor
PM> Set-DefaultScaffolder -Name Mailer -Scaffolder Mailer.Razor
# This will set Mailer scaffolder to Aspx
PM> Set-DefaultScaffolder -Name Mailer -Scaffolder Mailer.Aspx
If you do not intend to change your default Mailer, you can invoke one of the following to produce your desired view files:
# This will produce Razor views
PM> Scaffold Mailer.Razor UserMailer Welcome,GoodBye
# This will produce Aspx views
PM> Scaffold Mailer.Aspx UserMailer Welcome,GoodBye
Unlike the relative URLs in your web app, your email recipients will need to get absolute URLs for links and images. You can use the Url.Abs extension method from MvcMailer as shown below:
Please <a href="@Url.Abs(Url.Action("Index", "Home"))">Visit Us</a> to find more.
Add a Reference to IUserMailer
using System.Web;
using System.Web.Mvc;
using MvcMailer_Example.Mailers;
using Mvc.Mailer;
namespace MvcMailer_Example.Controllers
{
public class HomeController : Controller
{
private IUserMailer _userMailer = new UserMailer();
public IUserMailer UserMailer
{
get{return _userMailer;}
set{_userMailer = value;}
}
...
Use the Reference to Send Email:
//This is important, for the Send() extension method
using Mvc.Mailer;
...
public ActionResult SendWelcomeMessage()
{
UserMailer.Welcome().Send(); //Send() extension method: using Mvc.Mailer
return RedirectToAction("Index");
}
Now, you know the basics of sending emails using MvcMailer. However, you could get a lot more done with MvcMailer. Keep reading if you are interested!
If you have a mailer method like the following:
public virtual MvcMailMessage Welcome()
{
ViewBag.Name = "Sohan";
return Populate(x =>{
x.Subject = "Welcome to MvcMailer";
x.To.Add("[email protected]");
x.viewName = "Welcome";
});
}
You can write a unit test code like this:
//Test using NUnit and Moq
using System.Linq;
using NUnit.Framework;
using Moq;
using MvcMailer_Example.Mailers;
using System.Net.Mail;
namespace MvcMailer_Example.Tests.Mailers
{
[TestFixture]
public class UserMailerTest
{
private Mock<UserMailer> _userMailerMock;
[SetUp]
public void Setup()
{
//setup the mock
_userMailerMock = new Mock<UserMailer>();
//CallBase will ensure it calls real implementations other than the mocked out methods
_userMailerMock.CallBase = true;
}
[Test]
public void Test_WelcomeMessage()
{
//Arrange: Moq out the PopulateBody method
_userMailerMock.Setup(mailer => mailer.PopulateBody(It.IsAny<MvcMailMessage>(), "Welcome", It.IsAny<string>(), null));
//Act
var mailMessage = _userMailerMock.Object.Welcome();
//Assert
_userMailerMock.VerifyAll();
Assert.AreEqual("Sohan", _userMailerMock.Object.ViewBag.Name);
Assert.AreEqual("Welcome to MvcMailer", mailMessage.Subject);
Assert.AreEqual("[email protected]", mailMessage.To.First().ToString());
}
}
}
Since the scaffold generates the interface for you, its as easy as testing model repositories. Say, you have the following controller action that sends an Email:
public ActionResult SendWelcomeMessage()
{
UserMailer.Welcome().Send();
return RedirectToAction("Index");
}
You can write the following test for this:
using System.Net.Mail;
using System.Web.Mvc;
using Moq;
using Mvc.Mailer;
using MvcMailer_Example.Controllers;
using MvcMailer_Example.Mailers;
using NUnit.Framework;
namespace MvcMailer_Example.Tests.Controllers
{
[TestFixture]
public class HomeControllerTests
{
private Mock<IUserMailer> _userMailerMock;
private HomeController _homeController;
[SetUp]
public void Setup()
{
_homeController = new HomeController();
_userMailerMock = new Mock<IUserMailer>();
_homeController.UserMailer = _userMailerMock.Object;
MailerBase.IsTestModeEnabled = true;
}
[TearDown]
public void TearDown()
{
TestSmtpClient.SentMails.Clear();
}
[Test]
public void Test_SendWelcomeMessage()
{
//Arrange
var mailMessage = new MailMessage();
_userMailerMock.Setup(userMailer => userMailer.Welcome()).Returns(mailMessage);
//Act
var actionResult = _homeController.SendWelcomeMessage();
//Assert
_userMailerMock.VerifyAll();
Assert.AreEqual(1, TestSmtpClient.SentMails.Count);
Assert.AreEqual(mailMessage, TestSmtpClient.SentMails[0]);
var routeValues = (actionResult as RedirectToRouteResult).RouteValues;
Assert.AreEqual(routeValues["action"], "Index");
}
}
}
Just add your attachments to your MailMessage object. For example, you can do the following:
public virtual MvcMailMessage Welcome(string attachmentPath)
{
return Populate(x => {
x.viewName = "Welcome";
x.Attachments.Add(new Attachment(attachmentPath));
});
}
You can send both text/plain and text/html parts for a single email. Just add your text and html views like the following:
Views
|--- UserMailer
|--- _Layout.cshtml => email master page for text/html
|--- Welcome.cshtml => email content for text/html
|--- _Layout.text.cshtml => email master page for text/plain
|--- Welcome.text.cshtml => email content for text/plain
MvcMailer will look for both text and html versions. In case it finds both, it will send a multi-part email containing both parts. Otherwise, it will decide based on what is passed to UserMailer.IsBodyHtml property.
You can use the -WithText switch to Scaffold both html and plain text view files using the following.
PM> scaffold Mailer MyMailer Hello -WithText
Added MvcMailer output 'Mailers\IMyMailer.cs'
Added MvcMailer output 'Mailers\MyMailer.cs'
Added MyScaffolder output 'Views\MyMailer\_Layout.cshtml'
Added MyScaffolder output 'Views\MyMailer\Hello.cshtml'
Added MyScaffolder output 'Views\MyMailer\_Layout.text.cshtml'
Added MyScaffolder output 'Views\MyMailer\Hello.text.cshtml'
You can simply use the SendAsync extension method for MailMessage:
using Mvc.Mailer;
...
public ActionResult SendWelcomeMessage()
{
UserMailer.Welcome().SendAsync();
return RedirectToAction("Index");
}
If you use SendAsyc, you can write the following code to test it:
[TearDown]
public void TearDown()
{
TestSmtpClient.SentMails.Clear();
TestSmtpClient.WasLastCallAsync = false;
}
[Test]
public void Test_SendWelcomeMessage_sends_async()
{
//Arrange
var mailMessage = new MailMessage();
_userMailerMock.Setup(userMailer => userMailer.Welcome()).Returns(mailMessage);
//Act
var actionResult = _homeController.SendWelcomeMessage();
//Assert
_userMailerMock.VerifyAll();
Assert.IsTrue(TestSmtpClient.WasLastCallAsync);
}
You may want to handle events related to Asynchronous Emails. For example, you might need to take action when an asynchronous email sending is successful or not. Here's an example for you:
var client = new SmtpClientWrapper();
client.SendCompleted += (sender, e) =>
{
if (e.Error != null || e.Cancelled)
{
// Handle Error
}
//Use e.UserState
};
new MyMailer().Welcome().SendAsync("user state object", client);
Sometimes you want to embed an image or other resources directly inline with the email. This is better for cases when you want the recipients to see the images and other resources while offline. MvcMailer makes is simpler for you. Here is an example:
@Html.InlineImage("logo", "Company Logo")
Here, cid:logo will refer to the resource with Id logo. To set this resource, in your mailer do the following:
var resources = new Dictionary<string, string>();
resources["logo"] = logoPath;
PopulateBody(mailMessage, "WelcomeMessage", resources);
Do you need to send emails from a background process? Yes, you're right. You don't want to block your request/response cycle for that notification email to be sent. Instead, what you want is a background process that does it for you, even if it's sent after a short delay. Here's what you can do:
- Save your email related data into a database.
- Create a REST/SOAP web service that sends out the emails. This will ensure your Mailer has access to the HttpContext, which is essential for the core ASP.NET MVC framework to work properly. For example, to find your views, produce URLs, and perform authentication/authorization.
- Create a simple App that calls the web service. This could be a windows service app or an executable app running under Windows Scheduled task.
A future version of MvcMailer is likely to have support for this. But it is hard because of two reasons:
- MailMessage is not Serializable out of the box and has a lot of complex fields and associations.
- The core ASP.NET framework still needs HttpContext :(
- Email sending from a background process
- VB Code example
For Visual Studio 2013 issues with T4Scaffolding, Use the pre-release version at https://www.nuget.org/packages/MvcMailer-vs2013/
See https://github.com/smsohan/MvcMailer/issues/37 http://stackoverflow.com/questions/10241797/error-scaffolding-with-mvcmailer-in-mvc-4
for issues and solutions that seem fairly common.