Scott Hanselman

NuGet Package of the Week #2 - MvcMailer sends mails with ASP.NET MVC Razor Views and Scaffolding

March 16, 2011 Comment on this post [20] Posted in ASP.NET | ASP.NET MVC | NuGet | NuGetPOW | VS2010
Sponsored By

Have you implemented the NuGet Action Plan? Get on it, it'll take only 5 minutes: NuGet Action Plan - Upgrade to 1.1, Setup Automatic Updates, Get NuGet Package Explorer.

The Backstory: I was thinking since the NuGet .NET package management site is starting to fill up that I should start looking for gems (no pun intended) in there. You know, really useful stuff that folks might otherwise not find. I'll look for mostly open source projects, ones I think are really useful. I'll look at how they built their NuGet packages, if there's anything interesting about the way the designed the out of the box experience (and anything they could do to make it better) as well as what the package itself does.

MvcMailer sends mails with ASP.NET MVC Razor Views and Scaffolding

I just love the idea behind this NuGet package. This is effectively a port/reimagining of the Rails ActionMailer with an ASP.NET twist. It's wonderful for a number of reasons.

First, because (and this is an ingredient for all great pieces of software) we've all had this idea but never implemented it! I've been talking about writing this for six months. Of course, Talk is Cheap, Show Me The Code. I'm thrilled I never wrote it because it wouldn't have been this good.

Second, because the way that the author, Sohan, has implemented this really builds on existing technologies in a very "LEGO" way. Great open source apps often build on other great ones cleanly. He's really avoided duplication by separating concerns and focused on just the new functionality MvcMailer adds.

clip_image002

There comes a time in every project when you need to email a user. Traditionally it (almost always sucks). As Sohan points out, it usually looks like this:

StringBuilder mailBody = new StringBuilder();
mailBody.Append("<html><head><style type=\"text\css\">...</style></head>");
mailBody.Append("<body>")
mailBody.AppendFormat("Hi {0}<br/>", user.FirstName);
...
... XX lines of similar Appending unless it its done!
...
mailBody.Append("</body></html>");

If you're special, maybe you put a template text file somewhere and do a .Replace("{token}") but it still sucks.

Additionally, you want the ability to send not only rich HTML email but also plain text (or more likely, multipart) emails. Fortunately, with ASP.NET MVC we are already creating nice HTML template for output the user, just over HTTP. Why can't we do the same with email?

How MvcMailer Works and Why It's Cool

The MvcMailer NuGet Package is clever on a number of levels, truly. I mention this so that we (you and I, Dear Reader) might learn together. It's full of little gems and clever best-practices that we can use in our own NuGet packages.

In his install.ps1 - that's the PowerShell script that runs when you install a package - he has a little ReadMe in the form of a series of Write-Host commands. This is a simple, clever and effective way to get my attention. And it did!

Write-Host ---------------------------READ ME---------------------------------------------------
Write-Host
Write-Host Your default Mailer Scaffolder is set to $mailerScaffolder
Write-Host
Write-Host You can generate your Mailers and Views using the following Scaffolder Command
Write-Host
Write-Host "PM> Scaffold Mailer UserMailer Welcome,GoodBye"
Write-Host
Write-Host Edit the smtp configuration at web.config file before you send an email
Write-Host
Write-Host You can find more at: https://github.com/smsohan/MvcMailer/wiki/MvcMailer-Step-by-Step-Guide
Write-Host
Write-Host -------------------------------------------------------------------------------------

He also makes use of the version specific lib folders. There's a 40 folder underneath lib. That's because this package only works on .NET 4 and NuGet knows it because of the named lib folders. You can target Silverlight, etc with these folders.

The MvcMailer makes use of Steve Sanderson's excellent (and prescient) scaffolding system. Steve took the basic MvcScaffolding prototype and turned it into two packages, the MvcScaffolding one, and a base package called T4Scaffolding that isn't MVC specific. You can Get-Scaffolder and Set-Scaffolder and generate whatever you like, and he does.

If I type Get-Scaffolder, I see where MvcMailer has plugged in:

PM> Get-Scaffolder

Name Description Package
---- ----------- -------
Mailer.Aspx Scaffold ... MvcMailer 1.1
Mailer.Razor Scaffold ... MvcMailer 1.1
T4Scaffolding.CustomScaffolder Creates a... T4Scaffolding 0.9.7
T4Scaffolding.CustomTemplate Allows yo... T4Scaffolding 0.9.7
T4Scaffolding.EFDbContext Makes an ... T4Scaffolding 0.9.7
T4Scaffolding.EFRepository Creates a... T4Scaffolding 0.9.7

Now I can Scaffold out a Welcome and GoodBye mailer (or whatever, like Change Password, etc.

PM> Scaffold Mailer UserMailer Welcome,GoodBye
Added MvcMailer output 'Mailers\IUserMailer.cs'
Added MvcMailer output 'Mailers\UserMailer.cs'
Added MyScaffolder output 'Views\UserMailer\_Layout.cshtml'
Added MyScaffolder output 'Views\UserMailer\Welcome.cshtml'
Added MyScaffolder output 'Views\UserMailer\GoodBye.cshtml'

Ah, but I need both HTML and Text versions, so I'll do  it again -WithText:

PM> Scaffold Mailer UserMailer Welcome,GoodBye -WithText
Mailers\IUserMailer.cs already exists! Skipping...
Mailers\UserMailer.cs already exists! Skipping...
Views\UserMailer\_Layout.cshtml already exists! Skipping...
Views\UserMailer\Welcome.cshtml already exists! Skipping...
Views\UserMailer\GoodBye.cshtml already exists! Skipping...
Added MyScaffolder output 'Views\UserMailer\_Layout.text.cshtml' Added MyScaffolder output 'Views\UserMailer\Welcome.text.cshtml' Added MyScaffolder output 'Views\UserMailer\GoodBye.text.cshtml'

Very cool. Now I've got templates nicely organized as if they were Views for both HTML and Text for Welcome and GoodBye. I can take the generated code for my UserMail and extend it. For example, if I want to send some data to my mailer (I likely do) I'll change it to look like this:

public virtual MailMessage Welcome(string firstName, string email)
{
var mailMessage = new MailMessage{Subject = "Welcome"};

mailMessage.To.Add(email);
ViewBag.FirstName = firstName;
PopulateBody(mailMessage, viewName: "Welcome");

return mailMessage;
}

Then I can call this from a regular controller in response to an action. This could certainly be done on one line if you like that.

var mailer = new UserMailer();
var msg = mailer.Welcome(firstName: "Scott", email: "scottha@microsoft.com");
msg.Send();

I like named parameters.

NOTE: The Send() method is an extension method, and you need to make sure you have using Mvc.Mailer in your namespaces. This slowed me down a smidge until I figured it out.

Now, I don't want to setup my SMTP mail server, so I'll change the web.config to write emails out to a temp folder:

<system.net>
<mailSettings>
<!-- Method#1: Configure smtp server credentials -->
<!--<smtp from="some-email@gmail.com">
<network enableSsl="true" host="smtp.gmail.com" port="587" userName="some-email@gmail.com" password="valid-password" />
</smtp>-->
<!-- Method#2: Dump emails to a local directory -->
<smtp from="some-email@gmail.com" deliveryMethod="SpecifiedPickupDirectory">
<network host="localhost" />
<specifiedPickupDirectory pickupDirectoryLocation="c:\temp\"/>
</smtp>
</mailSettings>
</system.net>

When I execute /Home/SendWelcomeEmail, I've got a nice email sitting in my temp folder (note the Outlook Icon):

MvcMailerTempEmail

And here's my email. Note the user data passed in, in the form of my email and first name.

Welcome - Message (HTML)  (32)

I just love that this package exists and that it's such a great example of open source building on existing projects and plugging into existing conventions cleanly. It's also a testament to Steve's extensibility points  in T4Scaffolding and Andrew Nurse and friends in Razor as neither team had an idea this project was happening! Kudos to Sohan for this useful and polished project. I hope the community appreciates his work and supports him with bug fixes, improvements and more! Thank him in the comments and follow his project on GitHub!

One Other Clever Thing

Here's one other clever thing that Sohan does (borrowing from Steve Sanderson) He has to determine what your preferred View Engine is, so he counts the number of ASPX files and the number of Razor files and figures the one you have the most of is your preference.

### Copied from MvcScaffolding
function CountSolutionFilesByExtension($extension) {
$files = (Get-Project).DTE.Solution `
| ?{ $_.FileName } `
| %{ [System.IO.Path]::GetDirectoryName($_.FileName) } `
| %{ [System.IO.Directory]::EnumerateFiles($_, "*." + $extension, [System.IO.SearchOption]::AllDirectories) }
($files | Measure-Object).Count
}

function InferPreferredViewEngine() {
# Assume you want Razor except if you already have some ASPX views and no Razor ones
if ((CountSolutionFilesByExtension aspx) -eq 0) { return "razor" }
if (((CountSolutionFilesByExtension cshtml) -gt 0) -or ((CountSolutionFilesByExtension vbhtml) -gt 0)) { return "razor" }
return "aspx"
}

# Infer which view engine you're using based on the files in your project
### End copied

$mailerScaffolder = if ([string](InferPreferredViewEngine) -eq 'aspx') { "Mailer.Aspx" } else { "Mailer.Razor" }
Set-DefaultScaffolder -Name Mailer -Scaffolder $mailerScaffolder -SolutionWide -DoNotOverwriteExistingSetting

Very cool.

Related Links

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Hosting By
Hosted in an Azure App Service
March 16, 2011 5:26
I think that Postal is much more elegant
http://aboutcode.net/2010/11/17/going-postal-generating-email-with-aspnet-mvc-view-engines.html

I think that Postal is much more elegant
http://aboutcode.net/2010/11/17/going-postal-generating-email-with-aspnet-mvc-view-engines.html

Offtopic: @Scott: Posting from Firefox 4 still gives me 404. Have removed all cookies for Hanselman.com and MyOpenID. Same result.
Edit: Couldn't post from Chrome either. Posting with IE9
March 16, 2011 6:11
Why do you think it's more elegant? This one includes scaffolding for jump starting, supports multipart HTML/text automatically and puts the views in their own space. Plus, they seem extremely similar...what's the tiebreaker for you?
March 16, 2011 6:38
I am still trying to figure out how to receive mails in IIS 7+. http://stackoverflow.com/questions/2903543/iis-7-5-receive-emails any idea?
March 16, 2011 9:16
Thanks Scott! I appreciate this. Hope this helps the community.
March 16, 2011 16:59
Here is another package that we have been using that we really like:
http://nuget.org/List/Packages/ActionMailer

Link to developers blog that contains a little more info about the project:
http://geeksharp.com/2011/01/26/actionmailer-net-email-templates-for-the-mvc-crowd/

March 16, 2011 17:09
@Scott: you are right. I jumped too soon into conclusions without looking closely at MvcMailer.

In fact I love that MvcMailer can embed images or LinkedResource inside emails so easily.
March 16, 2011 18:20
I saw the mvcconf presentation on Postal and thought that was a very smart solution to emailing. I've not had a chance to look into MvcMailer so I'd be very interested to hear the +s and -s of both.

From what I gathered Postal supports html and text views, and also (recently) allows embedded images/attachments. Should I switch?
March 16, 2011 20:11
What I love about this project is how it handles the most problematic aspects of e-mail in a simple manner. I always struggled to get multipart e-mails working properly and never bothered getting embedded images working. The scaffolding is a bonus!
March 17, 2011 2:39
I was excited to try this out but it isnt working for me. Digging through the source code isnt yielding anything enlightening yet.
Anyone else seen something like this:

PM> Scaffold Mailer UserMailer Welcome,ResetPassword

Invoke-Scaffolder : A positional parameter cannot be found that accepts argument 'UserMailer'.
At line:1 char:9
+ Scaffold <<<< Mailer UserMailer Welcome,ResetPassword
+ CategoryInfo : InvalidArgument: (:) [Invoke-Scaffolder], ParameterBindingException
+ FullyQualifiedErrorId : PositionalParameterNotFound,T4Scaffolding.Cmdlets.InvokeScaffolderCmdlet

March 17, 2011 2:53
Ahh who knows why, but an Uninstall/reinstall of the MvcMailer package fixed the problem for me. Perhaps it was the order in which the dependent packages were installed. Nuget makes me feel like I'm still working with RPMs :)
March 17, 2011 3:23
This can happen if your installation failed for sone reason. You can check get-scaffolder command to see if u have MvcMailer scaffolder installed.
March 17, 2011 23:31
This is my kind of stuff - awesome stuff and a great learning resource.
March 18, 2011 8:14
@Gregor, thanks!
March 23, 2011 16:34
It's not always appropriate to send emails from the Presentation Layer when the business logic regarding what to include in the email may live in a different tier (e.g. a Workflow or WCF Service). Is there any way to take advantage of the same goodness while also better separating concerns?
March 23, 2011 16:48
I answered my own question - no (sort of). Whatever it is that sends the email needs access to HTTPContext

See the section - 'Email Sending from a Background Process':

https://github.com/smsohan/MvcMailer/wiki/MvcMailer-Step-by-Step-Guide
March 24, 2011 21:18
hmmm way cool but ...

I really want my emails to be generated in my business tier so that my other system processes like services and console applications can use the same code base for generating emails. Great idea wrong tier for the responsibility.
March 25, 2011 19:04
Pretty cool, but wouldn't it be even more scalable to simply build an e-mail provider that can take in any URL, parse the html from it, then fire it off?

Simple example: EmailProvider.Send("me@address.com", "http://www.google.com");
March 27, 2011 9:29
Ryan - Scalable? No, but possibly more useful in non-UI environments, like Mike says. You could modify this to to remove that dependency. I should talk to Sohan about this.

lmf232s - Thanks for sharing ActionMailer!


September 06, 2011 11:11
At our company we just create the (partial)view and run Render(Partial)ToString on it on the fly, right before sending it.

No package, no scaffolding, just a view and 20 lines of code. Might be a bit slower performance wise.



November 28, 2011 19:40
thanks for the article, however I think it could be more detailed in its description.

Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.