Skip to content

MvcMailer Step by Step Guide

online-java-training edited this page Nov 9, 2015 · 62 revisions

Introduction

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.

Why MvcMailer?

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!

Install MvcMailer NuGet Package

If you are NOT using VS 2013, open your package manager and type:

PM> install-package MvcMailer

Or if you are using VS 2013, do both of these:

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 Your Mailer

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

Configure SMTP Client

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.

Send Email

Edit Your Mailer

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]");
        });
}

Pass Data to Mailer Views

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 Your Master Page

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 Your View

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

Support for ASPX Views

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!

Forcing the Scaffolder to use Razor or ASPX

# 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

Absolute URL Using Url.Abs

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.

Send Mail from Controller

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!

Unit Test Your Mailers

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());
        }

    }
}

Unit Test A Controller That Uses Mailer

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");
        }
    }
}

Send Email with Attachments

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

Send Multi-part Emails

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'

Send Email Asynchronously

You can simply use the SendAsync extension method for MailMessage:

using Mvc.Mailer;
...

public ActionResult SendWelcomeMessage()
{
    UserMailer.Welcome().SendAsync();
    return RedirectToAction("Index");
}

Test Asynchronous Email Sending

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

Handle Events for Asynchronous Email

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);

Embed Image or LinkedResource Inside Email

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:

In Your View

@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:

In Your Mailer

var resources = new Dictionary<string, string>();
resources["logo"] = logoPath;
PopulateBody(mailMessage, "WelcomeMessage", resources);

Email Sending from a Background Process

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 :(

Upcoming Features

  • Email sending from a background process
  • VB Code example

Troubleshooting

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.

Online MVC 4 Training