Mandar Email con View .cshtml¶
Mandar un email con un template .cshtml
.
Crear directorio ~/Views/TemplateEmails
Crear archivo ~/Views/TemplateEmails/_LayoutEmail.cshtml
@{
Layout = null;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>@ViewBag.Title - My ASP.NET Application</title>
</head>
<body>
<div>
@RenderBody()
</div>
</body>
</html>
Este sera el Layout por defecto para los emails. Luego una View para un email, sera algo asi:
@{
ViewBag.Title = "Contact";
Layout = "~/Views/TemplateEmails/_LayoutEmail.cshtml";
}
<h2>Contact</h2>
<!-- resto del html, todo igual a las Views con Razor, con using, model, etc. -->
Editar ~/Global.asax.cs/
y en el metodo Application_Start
añadir:
// Añadir ~/Views/TemplateEmails a razor engine.
RazorViewEngine razorEngine = ViewEngines.Engines.OfType<RazorViewEngine>().FirstOrDefault();
razorEngine.ViewLocationFormats = razorEngine.ViewLocationFormats.Concat(
new string[] { "~/Views/TemplateEmails/{0}.cshtml" }
).ToArray();
// Partials en ~/Views/TemplateEmails
razorEngine.PartialViewLocationFormats = razorEngine.PartialViewLocationFormats.Concat(
new string[] { "~/Views/TemplateEmails/{0}.cshtml" }
).ToArray();
Para el ejemplo creo el directorio ~/Core
y dentro creo el archivo ViewRenderer.cs
con el
siguiente código de https://gist.github.com/HarveyWilliams/0405edd6719c16171329
using System;
using System.Web;
using System.Web.Mvc;
using System.IO;
using System.Web.Routing;
namespace WebApplication1.Core
{
/// <summary>
/// Class that renders MVC views to a string using the
/// standard MVC View Engine to render the view.
///
/// Requires that ASP.NET HttpContext is present to
/// work, but works outside of the context of MVC
///
/// Particularly useful for rendering CSHTML for emails.
///
/// Code extracted from:
/// https://github.com/RickStrahl/WestwindToolkit/blob/master/Westwind.Web.Mvc/Utils/ViewRenderer.cs
/// </summary>
public class ViewRenderer
{
/// <summary>
/// Required Controller Context
/// </summary>
protected ControllerContext Context { get; set; }
/// <summary>
/// Initializes the ViewRenderer with a Context.
/// </summary>
/// <param name="controllerContext">
/// If you are running within the context of an ASP.NET MVC request pass in
/// the controller's context.
/// Only leave out the context if no context is otherwise available.
/// </param>
public ViewRenderer(ControllerContext controllerContext = null)
{
// Create a known controller from HttpContext if no context is passed
if (controllerContext == null)
{
if (HttpContext.Current != null)
controllerContext = CreateController<EmptyController>().ControllerContext;
else
throw new InvalidOperationException(
"ViewRenderer must run in the context of an ASP.NET " +
"Application and requires HttpContext.Current to be present.");
}
Context = controllerContext;
}
/// <summary>
/// Renders a full MVC view to a string. Will render with the full MVC
/// View engine including running _ViewStart and merging into _Layout
/// </summary>
/// <param name="viewPath">
/// The path to the view to render. Either in same controller, shared by
/// name or as fully qualified ~/ path including extension
/// </param>
/// <param name="model">The model to render the view with</param>
/// <returns>String of the rendered view or null on error</returns>
public string RenderViewToString(string viewPath, object model = null)
{
return RenderViewToStringInternal(viewPath, model, false);
}
/// <summary>
/// Renders a full MVC view to a writer. Will render with the full MVC
/// View engine including running _ViewStart and merging into _Layout
/// </summary>
/// <param name="viewPath">
/// The path to the view to render. Either in same controller, shared by
/// name or as fully qualified ~/ path including extension
/// </param>
/// <param name="model">The model to render the view with</param>
/// <returns>String of the rendered view or null on error</returns>
public void RenderView(string viewPath, object model, TextWriter writer)
{
RenderViewToWriterInternal(viewPath, writer, model, false);
}
/// <summary>
/// Renders a partial MVC view to string. Use this method to render
/// a partial view that doesn't merge with _Layout and doesn't fire
/// _ViewStart.
/// </summary>
/// <param name="viewPath">
/// The path to the view to render. Either in same controller, shared by
/// name or as fully qualified ~/ path including extension
/// </param>
/// <param name="model">The model to pass to the viewRenderer</param>
/// <returns>String of the rendered view or null on error</returns>
public string RenderPartialViewToString(string viewPath, object model = null)
{
return RenderViewToStringInternal(viewPath, model, true);
}
/// <summary>
/// Renders a partial MVC view to given Writer. Use this method to render
/// a partial view that doesn't merge with _Layout and doesn't fire
/// _ViewStart.
/// </summary>
/// <param name="viewPath">
/// The path to the view to render. Either in same controller, shared by
/// name or as fully qualified ~/ path including extension
/// </param>
/// <param name="model">The model to pass to the viewRenderer</param>
/// <param name="writer">Writer to render the view to</param>
public void RenderPartialView(string viewPath, object model, TextWriter writer)
{
RenderViewToWriterInternal(viewPath, writer, model, true);
}
/// <summary>
/// Renders a partial MVC view to string. Use this method to render
/// a partial view that doesn't merge with _Layout and doesn't fire
/// _ViewStart.
/// </summary>
/// <param name="viewPath">
/// The path to the view to render. Either in same controller, shared by
/// name or as fully qualified ~/ path including extension
/// </param>
/// <param name="model">The model to pass to the viewRenderer</param>
/// <param name="controllerContext">Active Controller context</param>
/// <returns>String of the rendered view or null on error</returns>
public static string RenderView(string viewPath, object model = null,
ControllerContext controllerContext = null)
{
ViewRenderer renderer = new ViewRenderer(controllerContext);
return renderer.RenderViewToString(viewPath, model);
}
/// <summary>
/// Renders a partial MVC view to the given writer. Use this method to render
/// a partial view that doesn't merge with _Layout and doesn't fire
/// _ViewStart.
/// </summary>
/// <param name="viewPath">
/// The path to the view to render. Either in same controller, shared by
/// name or as fully qualified ~/ path including extension
/// </param>
/// <param name="model">The model to pass to the viewRenderer</param>
/// <param name="writer">Writer to render the view to</param>
/// <param name="controllerContext">Active Controller context</param>
/// <returns>String of the rendered view or null on error</returns>
public static void RenderView(string viewPath, TextWriter writer, object model,
ControllerContext controllerContext)
{
ViewRenderer renderer = new ViewRenderer(controllerContext);
renderer.RenderView(viewPath, model, writer);
}
/// <summary>
/// Renders a partial MVC view to string. Use this method to render
/// a partial view that doesn't merge with _Layout and doesn't fire
/// _ViewStart.
/// </summary>
/// <param name="viewPath">
/// The path to the view to render. Either in same controller, shared by
/// name or as fully qualified ~/ path including extension
/// </param>
/// <param name="model">The model to pass to the viewRenderer</param>
/// <param name="controllerContext">Active Controller context</param>
/// <param name="errorMessage">optional out parameter that captures an error message instead of throwing</param>
/// <returns>String of the rendered view or null on error</returns>
public static string RenderView(string viewPath, object model,
ControllerContext controllerContext,
out string errorMessage)
{
errorMessage = null;
try
{
ViewRenderer renderer = new ViewRenderer(controllerContext);
return renderer.RenderViewToString(viewPath, model);
}
catch (Exception ex)
{
errorMessage = ex.GetBaseException().Message;
}
return null;
}
/// <summary>
/// Renders a partial MVC view to the given writer. Use this method to render
/// a partial view that doesn't merge with _Layout and doesn't fire
/// _ViewStart.
/// </summary>
/// <param name="viewPath">
/// The path to the view to render. Either in same controller, shared by
/// name or as fully qualified ~/ path including extension
/// </param>
/// <param name="model">The model to pass to the viewRenderer</param>
/// <param name="controllerContext">Active Controller context</param>
/// <param name="writer">Writer to render the view to</param>
/// <param name="errorMessage">optional out parameter that captures an error message instead of throwing</param>
/// <returns>String of the rendered view or null on error</returns>
public static void RenderView(string viewPath, object model, TextWriter writer,
ControllerContext controllerContext,
out string errorMessage)
{
errorMessage = null;
try
{
ViewRenderer renderer = new ViewRenderer(controllerContext);
renderer.RenderView(viewPath, model, writer);
}
catch (Exception ex)
{
errorMessage = ex.GetBaseException().Message;
}
}
/// <summary>
/// Renders a partial MVC view to string. Use this method to render
/// a partial view that doesn't merge with _Layout and doesn't fire
/// _ViewStart.
/// </summary>
/// <param name="viewPath">
/// The path to the view to render. Either in same controller, shared by
/// name or as fully qualified ~/ path including extension
/// </param>
/// <param name="model">The model to pass to the viewRenderer</param>
/// <param name="controllerContext">Active controller context</param>
/// <returns>String of the rendered view or null on error</returns>
public static string RenderPartialView(string viewPath, object model = null,
ControllerContext controllerContext = null)
{
ViewRenderer renderer = new ViewRenderer(controllerContext);
return renderer.RenderPartialViewToString(viewPath, model);
}
/// <summary>
/// Renders a partial MVC view to string. Use this method to render
/// a partial view that doesn't merge with _Layout and doesn't fire
/// _ViewStart.
/// </summary>
/// <param name="viewPath">
/// The path to the view to render. Either in same controller, shared by
/// name or as fully qualified ~/ path including extension
/// </param>
/// <param name="model">The model to pass to the viewRenderer</param>
/// <param name="controllerContext">Active controller context</param>
/// <param name="writer">Text writer to render view to</param>
/// <param name="errorMessage">optional output parameter to receive an error message on failure</param>
public static void RenderPartialView(string viewPath, TextWriter writer, object model = null,
ControllerContext controllerContext = null)
{
ViewRenderer renderer = new ViewRenderer(controllerContext);
renderer.RenderPartialView(viewPath, model, writer);
}
/// <summary>
/// Internal method that handles rendering of either partial or
/// or full views.
/// </summary>
/// <param name="viewPath">
/// The path to the view to render. Either in same controller, shared by
/// name or as fully qualified ~/ path including extension
/// </param>
/// <param name="model">Model to render the view with</param>
/// <param name="partial">Determines whether to render a full or partial view</param>
/// <param name="writer">Text writer to render view to</param>
protected void RenderViewToWriterInternal(string viewPath, TextWriter writer, object model = null, bool partial = false)
{
// first find the ViewEngine for this view
ViewEngineResult viewEngineResult = null;
if (partial)
viewEngineResult = ViewEngines.Engines.FindPartialView(Context, viewPath);
else
viewEngineResult = ViewEngines.Engines.FindView(Context, viewPath, null);
if (viewEngineResult == null)
throw new FileNotFoundException();
// get the view and attach the model to view data
var view = viewEngineResult.View;
Context.Controller.ViewData.Model = model;
var ctx = new ViewContext(Context, view,
Context.Controller.ViewData,
Context.Controller.TempData,
writer);
view.Render(ctx, writer);
}
/// <summary>
/// Internal method that handles rendering of either partial or
/// or full views.
/// </summary>
/// <param name="viewPath">
/// The path to the view to render. Either in same controller, shared by
/// name or as fully qualified ~/ path including extension
/// </param>
/// <param name="model">Model to render the view with</param>
/// <param name="partial">Determines whether to render a full or partial view</param>
/// <returns>String of the rendered view</returns>
private string RenderViewToStringInternal(string viewPath, object model,
bool partial = false)
{
// first find the ViewEngine for this view
ViewEngineResult viewEngineResult = null;
if (partial)
viewEngineResult = ViewEngines.Engines.FindPartialView(Context, viewPath);
else
viewEngineResult = ViewEngines.Engines.FindView(Context, viewPath, null);
if (viewEngineResult == null || viewEngineResult.View == null)
{
//throw new FileNotFoundException(Resources.ViewCouldNotBeFound);
throw new Exception("Can't find view.");
}
// get the view and attach the model to view data
var view = viewEngineResult.View;
Context.Controller.ViewData.Model = model;
string result = null;
using (var sw = new StringWriter())
{
var ctx = new ViewContext(Context, view,
Context.Controller.ViewData,
Context.Controller.TempData,
sw);
view.Render(ctx, sw);
result = sw.ToString();
}
return result;
}
/// <summary>
/// Creates an instance of an MVC controller from scratch
/// when no existing ControllerContext is present
/// </summary>
/// <typeparam name="T">Type of the controller to create</typeparam>
/// <returns>Controller for T</returns>
/// <exception cref="InvalidOperationException">thrown if HttpContext not available</exception>
public static T CreateController<T>(RouteData routeData = null, params object[] parameters)
where T : Controller, new()
{
// create a disconnected controller instance
T controller = (T)Activator.CreateInstance(typeof(T), parameters);
// get context wrapper from HttpContext if available
HttpContextBase wrapper = null;
if (HttpContext.Current != null)
wrapper = new HttpContextWrapper(System.Web.HttpContext.Current);
else
throw new InvalidOperationException(
"Can't create Controller Context if no active HttpContext instance is available.");
if (routeData == null)
routeData = new RouteData();
// add the controller routing if not existing
if (!routeData.Values.ContainsKey("controller") && !routeData.Values.ContainsKey("Controller"))
routeData.Values.Add("controller", controller.GetType().Name
.ToLower()
.Replace("controller", ""));
controller.ControllerContext = new ControllerContext(wrapper, routeData, controller);
return controller;
}
}
/// <summary>
/// Empty MVC Controller instance used to
/// instantiate and provide a new ControllerContext
/// for the ViewRenderer
/// </summary>
public class EmptyController : Controller
{
}
}
Dentro de ~/Core
creo SimpleEmail.cs
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Net;
using System.Net.Mail;
using System.Threading.Tasks;
namespace WebApplication1.Core.Emails
{
/// <summary>
/// Envía un email renderizando con Razor engine.
///
/// Ejemplo:
/// var template = "Hello";
/// var subject = "Email de prueba";
/// var from = new MailAddress("perico@example.com"); //Opcional || SMTPDefaultFrom
/// var to = new List<MailAddress> { new MailAddress("palote@example.com") };
/// var model = new Person { Username = "Perico de los Palotes", Email = "perico@example.com" }; // Opcional
/// SimpleEmail.Send(template, subject, from, to, model);
///
/// Require:
///
/// https://gist.github.com/HarveyWilliams/0405edd6719c16171329
///
/// Web.config en appSettings
///
/// <add key="SMTPDefaultFromName" value="My Company"/>
/// <add key="SMTPDefaultFromEmail" value="default@example.com"/>
/// <add key="SMTPHost" value="smtp.gmail.com"/>
/// <add key="SMTPEnableSsl" value="true"/>
/// <add key="SMTPUserName" value="username@gmail.com"/>
/// <add key="SMTPPassword" value="MI_PASSWORD"/>
/// <add key="SMTPPort" value="587" />
/// </summary>
public class SimpleEmail : IDisposable
{
/// <summary>
/// Template para el email.
/// </summary>
private string _template;
/// <summary>
/// Titulo del email.
/// </summary>
private string _subject;
/// <summary>
/// Cabecera FROM:
/// </summary>
private MailAddress _from;
/// <summary>
/// Lista de emails destinatarios.
/// </summary>
private List<MailAddress> _to;
/// <summary>
/// ¿El email sera enviado como HTML?
/// </summary>
private bool _isBodyHtml;
/// <summary>
/// Cuerpo del email.
/// </summary>
private string _body;
/// <summary>
/// model para la View.
/// </summary>
private object _model;
// SMTP
private MailMessage _mailMessage;
private NetworkCredential _networkCredential;
private SmtpClient _smtpClient;
/// <summary>
/// Envía un email asíncrono.
/// </summary>
/// <param name="template">Nombre del archivo en ~/Views/TemplateEmails/</param>
/// <param name="subject">Titulo del mensaje</param>
/// <param name="from">Cabeceras para From:</param>
/// <param name="to">Lista de emails de recepción</param>
/// <param name="model">model para el contexto</param>
/// <param name="isBodyHtml">¿Mandar mensaje como HTML?</param>
public static async Task SendAsync(string template, string subject, MailAddress from, List<MailAddress> to, object model = null, bool isBodyHtml = true)
{
SimpleEmail email = _getInstance(template, subject, from, to, model, isBodyHtml);
await email._smtpClient.SendMailAsync(email._mailMessage);
}
/// <summary>
/// Envía un email asíncrono.<br>
/// El campo From: lo obtendrá de SMTPDefaultFrom del archivo de configuración.
/// </summary>
/// <param name="template">Nombre del archivo en ~/Views/TemplateEmails/</param>
/// <param name="subject">Titulo del mensaje</param>
/// <param name="to">Lista de emails de recepción</param>
/// <param name="model">model para el contexto</param>
/// <param name="isBodyHtml">¿Mandar mensaje como HTML?</param>
public static async Task SendAsync(string template, string subject, List<MailAddress> to, object model = null, bool isBodyHtml = true)
{
SimpleEmail email = _getInstance(template, subject, null, to, model, isBodyHtml);
await email._smtpClient.SendMailAsync(email._mailMessage);
}
/// <summary>
/// Envía un email.
/// </summary>
/// <param name="template">Nombre del archivo en ~/Views/TemplateEmails/</param>
/// <param name="subject">Titulo del mensaje</param>
/// <param name="from">Cabeceras para From:</param>
/// <param name="to">Lista de emails de recepción</param>
/// <param name="model">model para el contexto</param>
/// <param name="isBodyHtml">¿Mandar mensaje como HTML?</param>
public static void Send(string template, string subject, MailAddress from, List<MailAddress> to, object model = null, bool isBodyHtml = true)
{
SimpleEmail email = _getInstance(template, subject, from, to, model, isBodyHtml);
email._smtpClient.Send(email._mailMessage);
}
/// <summary>
/// Envía un email.<br>
/// El campo From: lo obtendrá de SMTPDefaultFrom del archivo de configuración.
/// </summary>
/// <param name="template">Nombre del archivo en ~/Views/TemplateEmails/</param>
/// <param name="subject">Titulo del mensaje</param>
/// <param name="to">Lista de emails de recepción</param>
/// <param name="model">model para el contexto</param>
/// <param name="isBodyHtml">¿Mandar mensaje como HTML?</param>
public static void Send(string template, string subject, List<MailAddress> to, object model = null, bool isBodyHtml = true)
{
SimpleEmail email = _getInstance(template, subject, null, to, model, isBodyHtml);
email._smtpClient.Send(email._mailMessage);
}
/// <summary>
/// Solo es instanciable desde los métodos statics
/// </summary>
private SimpleEmail()
{
_mailMessage = new MailMessage();
_networkCredential = new NetworkCredential()
{
UserName = ConfigurationManager.AppSettings["SMTPUserName"],
Password = ConfigurationManager.AppSettings["SMTPPassword"]
};
_smtpClient = new SmtpClient()
{
Host = ConfigurationManager.AppSettings["SMTPHost"],
EnableSsl = Convert.ToBoolean(ConfigurationManager.AppSettings["SMTPEnableSsl"]),
UseDefaultCredentials = true,
Credentials = _networkCredential,
Port = int.Parse(ConfigurationManager.AppSettings["SMTPPort"])
};
}
/// <summary>
/// Obtener instancia.
/// </summary>
private static SimpleEmail _getInstance(string template, string subject, MailAddress from, List<MailAddress> to, object model, bool isBodyHtml)
{
from = from ?? new MailAddress(
ConfigurationManager.AppSettings["SMTPDefaultFromEmail"],
ConfigurationManager.AppSettings["SMTPDefaultFromName"]
);
SimpleEmail email = new SimpleEmail
{
_template = template,
_subject = subject,
_from = from,
_to = to,
_model = model
};
email._isBodyHtml = isBodyHtml;
email._render();
email._initialize();
return email;
}
/// <summary>
/// Inicializa las variables de clase.
/// </summary>
private void _initialize()
{
_mailMessage.From = _from;
_mailMessage.Subject = _subject;
_mailMessage.Body = _body;
_mailMessage.IsBodyHtml = _isBodyHtml;
foreach (var m in _to)
{
_mailMessage.To.Add(m);
}
}
/// <summary>
/// Renderiza el archivo con Razor.
/// </summary>
private string _render()
{
_body = ViewRenderer.RenderView(_template, _model);
return _body;
}
public void Dispose()
{
_smtpClient.Dispose();
_mailMessage.Dispose();
}
}
}
En el archivo Web.config
<appSettings>
<!-- ... --->
<add key="SMTPDefaultFromName" value="My Company"/>
<add key="SMTPDefaultFromEmail" value="default@example.com"/>
<add key="SMTPHost" value="smtp.gmail.com"/>
<add key="SMTPEnableSsl" value="true"/>
<add key="SMTPUserName" value="username@gmail.com"/>
<add key="SMTPPassword" value="MI_PASSWORD"/>
<add key="SMTPPort" value="587" />
</appSettings>