November Happy Hour will be moved to Thursday December 5th.
November Happy Hour will be moved to Thursday December 5th.
Hi Andy
Two comments:
private Stream GetLogoStream(ContentReference logoReference)
{
var logoFile = _contentLoader.Get<ImageData>(logoReference, language: null);
var blob = logoFile.BinaryData;
using (var blobStream = blob.OpenRead())
{
var memoryStream = new MemoryStream();
blobStream.CopyTo(memoryStream);
memoryStream.Position = 0;
return memoryStream;
}
}
Then you can call this GetLogoStream method when constructing your logo attachment, as it accepts a Stream.
These are all the templates to send out various emails for different actions on the website - there is also a HtmlHelpExtensions class that was already existing that I haven't seen if its being used or not. It seems to have some of the things you've suggested - but this project is over 500 files and spans multiple developers and contractors.
I'm completely lost in this and new to Optimizely, I understand this is a lot to ask but I really appreciate any help in solving this. Basically my hail mary attempt at finishing this.
This is the HtmlHelperExtension class that was existing but was not the piece the old developer built specifically for this SendGrid task (which was initially meant for MailGun and worked with that but the switch to SendGrid caused issues).
public static class HtmlHelperExtensions
{
public static IHtmlString WebCustomersEmailLink(this HtmlHelper htmlHelper, string inlineStyles)
{
var email = SiteSettingsHelper.Get(x => x.EmailCustomerSupportEmailAddress);
var tag = htmlHelper.Anchor($"mailto:{email}", null, null, email);
if (!string.IsNullOrEmpty(inlineStyles))
{
tag.Attributes["style"] = inlineStyles;
}
return new MvcHtmlString(tag.ToString());
}
public static IHtmlString SiteName(this HtmlHelper htmlHelper)
{
return new MvcHtmlString(SiteDefinition.Current.SiteUrl.Host);
}
public static IHtmlString InlineFile(this HtmlHelper htmlHelper, string path)
{
using (var stream = File.OpenRead(HostingEnvironment.MapPath(path)))
{
return htmlHelper.AddInlineAttachment(stream, Path.GetFileName(path));
}
}
public static IHtmlString InlineFile(this HtmlHelper htmlHelper, ContentReference mediaReference)
{
if (!ServiceLocator.Current.GetInstance<IContentLoader>().TryGet<MediaData>(mediaReference, out var fileData))
{
return MvcHtmlString.Empty;
}
using (var stream = fileData.BinaryData.OpenRead())
{
return htmlHelper.AddInlineAttachment(stream, fileData.Name);
}
}
private static MvcHtmlString AddInlineAttachment(this HtmlHelper htmlHelper, Stream fileStream, string fileName)
{
var length = fileStream.Length;
var binaryData = new byte[length];
fileStream.Read(binaryData, 0, (int)length);
var attachments = (ICollection<Attachment>)htmlHelper.ViewContext.TempData[TemplateCompiler.AttachmentsViewDataProperty];
attachments.Add(new Attachment(binaryData, fileName, true));
return new MvcHtmlString("cid:" + fileName);
}
}
This class compiles everything and builds the actual emails.
public static class TemplateCompiler
{
public const string AttachmentsViewDataProperty = "Attachments";
public static string Compile(string templatePath, object model, out ICollection<Attachment> attachments)
{
return DoRender(templatePath, model, out attachments);
}
private static string DoRender(string viewPath, object viewModel, out ICollection<Attachment> attachments)
{
var sb = new System.Text.StringBuilder();
using (var writer = new StringWriter(sb))
{
var controllerContext = CreateControllerContext();
var razorViewResult = ViewEngines.Engines.FindView(controllerContext, viewPath, string.Empty);
if (razorViewResult.View == null)
{
throw new InvalidOperationException($"Cannot find view {viewPath}.");
}
attachments = new List<Attachment>();
var tempData = new TempDataDictionary
{
[AttachmentsViewDataProperty] = attachments
};
var viewContext = new ViewContext(
controllerContext,
razorViewResult.View,
new ViewDataDictionary(viewModel),
tempData,
writer);
razorViewResult.View.Render(viewContext, writer);
return writer.ToString();
}
}
private static ControllerContext CreateControllerContext()
{
var routeData = new RouteData();
routeData.Values.Add("controller", nameof(FakeController));
// The url below doesn't matter.
return new ControllerContext(
new HttpContextWrapper(
new HttpContext(
new HttpRequest(null, "https://localhost", null),
new HttpResponse(null))),
routeData,
new FakeController());
}
private class FakeController : Controller
{
}
}
I'm going to go through and insert the several relevant classes that are referenced in the MailGunEmailService class - which is the service that is supposed to handle the SendGrid
Attachment
public class Attachment
{
public Attachment(byte[] content, string fileName, bool inline = false)
{
Content = content;
FileName = fileName;
Inline = inline;
}
public string FileName { get; }
public byte[] Content { get; }
public bool Inline { get; }
public override string ToString()
{
return $"{FileName}, {Content.Length} bytes";
}
public static Attachment FromFile(string filePath)
{
return new Attachment(File.ReadAllBytes(filePath), Path.GetFileName(filePath));
}
}
MailAddress
public class MailAddress
{
public MailAddress(string address)
{
Address = address;
}
public MailAddress(string address, string displayName)
{
Address = address;
DisplayName = displayName;
}
private string DisplayName { get; }
private string Address { get; }
public override string ToString() => string.IsNullOrEmpty(DisplayName)
? Address
: $"\"{DisplayName}\" <{Address}>";
/// <summary>
/// Parses the list of emails to an array of <see cref="MailAddress"/> objects.
/// </summary>
/// <param name="emails">The comma- or semicolon- separated list.</param>
/// <returns>The list of emails or <c>null</c> in case original string is <c>null</c> or empty.</returns>
public static MailAddress[] FromString(string emails)
{
if (string.IsNullOrEmpty(emails))
{
return Array.Empty<MailAddress>();
}
var split = emails.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries);
return split.Select(s => new MailAddress(s.Trim())).ToArray();
}
}
MailGunEmailService - where everything ties into
public class MailGunEmailService : IEmailService
{
private static Logger logger = LogManager.GetCurrentClassLogger();
public void Send(
MailAddress from,
ICollection<MailAddress> to,
ICollection<MailAddress> cc,
ICollection<MailAddress> bcc,
string subject,
string body,
ICollection<Attachment> attachments)
{
var smtpClient = new SmtpClient();
var message = new MailMessage();
message.From = new System.Net.Mail.MailAddress(from.ToString());
//var request = new RestRequest();
if (to?.Count > 0)
foreach (var emailTo in to)
message.To.Add(new System.Net.Mail.MailAddress(emailTo.ToString()));
if (cc?.Count > 0)
foreach (var carbonCopy in cc)
message.CC.Add(new System.Net.Mail.MailAddress(carbonCopy.ToString()));
if (bcc?.Count > 0)
foreach (var blindCarbonCopy in bcc)
message.Bcc.Add(new System.Net.Mail.MailAddress(blindCarbonCopy.ToString()));
message.Subject = subject;
message.Body = body;
message.IsBodyHtml = true;
// Need to get the path(s) to the file(s) and add to email using System.Net.Mail.Attachments()
if (attachments != null)
{
foreach (var attachment in attachments.Where(a => !a.Inline))
message.Attachments.Add(new System.Net.Mail.Attachment(attachment.FileName));
foreach (var attachment in attachments.Where(a => a.Inline))
{
var att = new System.Net.Mail.Attachment(attachment.FileName);
att.ContentId = attachment.FileName;
att.ContentDisposition.Inline = true;
att.ContentDisposition.DispositionType = DispositionTypeNames.Inline;
message.Attachments.Add(att);
}
}
smtpClient.Send(message);
}
}
Hi Mike
First off, Mailgun has a template renderer on its own. This means you can put your template there and just send your parameters to their API. Then you can remove all your renderer code. That's what I always prefer.
It looks like you are hardcoding the email content in a view file. If you use the InlineFile(this HtmlHelper htmlHelper, ContentReference mediaReference), then the data is loaded from the blob file (wherever that is stored). That is the recommended way.
The Mailgun class you have should already be neutral enough to also support Sendgrid, since it is just a SMTP client wrapper. What kind of issues do you have with Sendgrid?
I can confirm now the issue happened when switching from MailGun (using the API) to the SendGrid (using SMTP). The requirement is to use SendGrid SMTP to send and not the Rest API method. I think the issue is that we never had any SMTP version of this working, whether it was MailGun or SendGrid. The code below is the last working version of what we had but its using the API - I'm almost sure what I need is to have this same MailGunEmailService class have the stuff to send those attachments (that are from blob storage). This class and methods get called and work for sending but the issue of inline and attachments is really the only issue with all of this. Whatever missing pieces or things I need to add/modify to reconstruct this class to account for any attachments (including inline).
This is some code I wrote - before I realized it was the wrong way to save these files like this and that the files are actually coming from Optimizely. This actually did work for inline attachments but it's obviously not the right way to do it. Which is why I'm going back to fix before we launch it. Let me know if this makes anything clearer, I really appreciate your time.
internal class MailGunEmailService : IEmailService
{
private static Logger logger = LogManager.GetCurrentClassLogger();
private readonly string path = "~/App_Data/attachments/";
public void Send(
MailAddress from,
ICollection<MailAddress> to,
ICollection<MailAddress> cc,
ICollection<MailAddress> bcc,
string subject,
string body,
ICollection<Attachment> attachments)
{
var smtpClient = new SmtpClient();
var message = new MailMessage();
message.From = new System.Net.Mail.MailAddress(from.ToString());
//var request = new RestRequest();
if (to != null && to.Count > 0)
foreach (var emailTo in to)
message.To.Add(new System.Net.Mail.MailAddress(emailTo.ToString()));
if (cc != null && cc.Count > 0)
foreach (var carbonCopy in cc)
message.CC.Add(new System.Net.Mail.MailAddress(carbonCopy.ToString()));
if (bcc != null && bcc.Count > 0)
foreach (var blindCarbonCopy in bcc)
message.Bcc.Add(new System.Net.Mail.MailAddress(blindCarbonCopy.ToString()));
message.Subject = subject;
message.Body = body;
message.IsBodyHtml = true;
// Attachments currently do not work
// Need to get the path(s) to the file(s) and add to email using System.Net.Mail.Attachments()
if (attachments != null)
{
foreach (var attachment in attachments.Where(a => !a.Inline))
message.Attachments.Add(new System.Net.Mail.Attachment(attachment.FileName));
foreach (var attachment in attachments.Where(a => a.Inline))
{
if (!Directory.Exists(HostingEnvironment.MapPath(path)))
Directory.CreateDirectory(HostingEnvironment.MapPath(path) ?? string.Empty);
var attachmentPath = Path.Combine(HostingEnvironment.MapPath(path) ?? string.Empty,
attachment.FileName);
File.WriteAllBytes(attachmentPath, attachment.Content);
var att = new System.Net.Mail.Attachment(attachmentPath);
att.ContentDisposition.Inline = true;
att.ContentId = attachment.FileName;
att.ContentDisposition.DispositionType = DispositionTypeNames.Inline;
message.Attachments.Add(att);
}
}
smtpClient.Send(message);
}
}
Are the attachments added to the email, but just not shown in the body? Or are they not added at all?
If they are added, but not shown, a simple thing you can try is to add <> around your file name in the ContentId value. Like this:
att.ContentId = $”<{attachment.FileName}>”;
I have an email provider (SendGrid) I use to automatically trigger emails based on different functions on the CMS / Commerce site. I have a View as a template and some classes that correctly compile and send the emails but the issue is with attachments (and specifically in this case: inline). This specific one has the logo that's set in the backend end and stored in App_Data and attached to the email inline, but there is a specification we need to use the System.Net.Mail.Attachment object instead of any API. I've struggled seeing how to dynamically get the path of this (and all other email attachments to work with the same code). It gets the attachment file's name, content correct but in terms of actually attaching it to the email template, I've been working on this awhile and can't find something to fix this yet.
This is the email service errors in the ForEach block.
Where the uploaded files are located
Exception shown when trying to attach
Line showing how these email attachments are referenced inline