sendgrid / sendgrid-csharp

The Official Twilio SendGrid C#, .NetStandard, .NetCore API Library
https://sendgrid.com
MIT License
1.08k stars 584 forks source link

Provide a helper to convert MailMessage to SendGrid.Mail #266

Open rpanjwani opened 8 years ago

rpanjwani commented 8 years ago

Issue Summary

It's just ridiculous that I have to manually map my existing MailMessage to SendGrid's Mail. I am currently using Postal which generates a MailMessage from an MVC view, but now I can't easily convert my MailMessage object to SendGrid.Mail. I have to manually go through each property and I have no idea how it will work.

Steps to Reproduce

No steps, it's pretty obvious.

Technical details:

thinkingserious commented 8 years ago

I have added this to our backlog for consideration, in the mean time I suggest you take a look at this: https://sendgrid.com/docs/Classroom/Send/v3_Mail_Send/how_to_migrate_from_v2_to_v3_mail_send.html

rpanjwani commented 8 years ago

thanks @thinkingserious , but it doesn't really help me in converting .net MailMessage object to SendGrid Mail object. I still have to go through and read the .net documentation on each portion of the email such as content disposition, content type, etc. etc. and convert and map each of the properties to that of SendGrid. Just opens up simple emailing to a lot of bugs in the future.

thinkingserious commented 8 years ago

@rpanjwani,

Have you tried this? https://github.com/andrewdavey/postal/issues/130#issuecomment-163363032

rpanjwani commented 8 years ago

I think this is using SendGrid as an smtp relay, whereas I am trying to use the web api v3. I was under the impression that azure doesn't allow smtp directly - you have to setup a relay server. I can certainly give it a go - that way I don't have to mess with the API at all.

thinkingserious commented 8 years ago

@rpanjwani,

Please let us know how it goes. Thanks!

APM3 commented 8 years ago

You have probably solved this by now, but in case you haven't or someone else wanders in here, then this might at least help get it started:

using System;
using System.IO;
using System.Net.Mail;

namespace SendGrid.Helpers.Mail
{
    public static partial class MailMessageExtensions
    {
        public static Email GetSendGridAddress(this MailAddress address)
        {
            // SendGrid Server-Side API is currently bugged, and messes up when the name has a comma or a semicolon in it
            return String.IsNullOrWhiteSpace(address.DisplayName) ?
                new Email(address.Address) :
                new Email(address.Address, address.DisplayName.Replace(",", "").Replace(";", ""));
        }

        public static Attachment GetSendGridAttachment(this System.Net.Mail.Attachment attachment)
        {
            using (var stream = new MemoryStream())
            {
                try
                {
                    attachment.ContentStream.CopyTo(stream);
                    return new Attachment()
                    {
                        Disposition = "attachment",
                        Type = attachment.ContentType.MediaType,
                        Filename = attachment.Name,
                        ContentId = attachment.ContentId,
                        Content = Convert.ToBase64String(stream.ToArray())
                    };
                }
                finally
                {
                    stream.Close();
                }
            }
        }

        public static Mail GetSendGridMessage(this MailMessage message)
        {
            var msg = new Mail();

            msg.From = message.From.GetSendGridAddress();
            if (message.ReplyToList.Count > 0)
            {
                msg.ReplyTo = message.ReplyToList[0].GetSendGridAddress();
            }

            var p = new Personalization();
            foreach (var a in message.To)
            {
                p.AddTo(a.GetSendGridAddress());
            }
            foreach (var a in message.CC)
            {
                p.AddCc(a.GetSendGridAddress());
            }
            foreach (var a in message.Bcc)
            {
                p.AddBcc(a.GetSendGridAddress());
            }
            msg.AddPersonalization(p);

            if (!String.IsNullOrWhiteSpace(message.Subject))
            {
                msg.Subject = message.Subject;
            }
            if (!String.IsNullOrWhiteSpace(message.Body))
            {
                if (message.IsBodyHtml)
                {
                    var c = new Content();
                    c.Type = "text/html";
                    if (!message.Body.StartsWith("<html"))
                    {
                        c.Value = "<html><body>" + message.Body + "</body></html>";
                    }
                    else
                    {
                        c.Value = message.Body;
                    }
                    msg.AddContent(c);
                }
                else
                {
                    msg.AddContent(new Content("text/plain", message.Body));
                }
            }

            foreach (var attachment in message.Attachments)
            {
                msg.AddAttachment(attachment.GetSendGridAttachment());
            }

            return msg;
        }
    }
}

now you can get it simply by doing: var sgMail = mailMessage.GetSendGridMessage();

thinkingserious commented 8 years ago

This is awesome @APM3!

Could you please email us at dx@sendgrid.com with your T-shirt size and mailing address?

kvishalv commented 7 years ago

Cool extension. Please also add a loop for your replacements from MailMessage .. a.ka substitution tags in Sendgrid

       var output = new StringBuilder(EmailBody);
        foreach (Dictionary<string, string> _replacement in replacements)
        {
            foreach (KeyValuePair<string, string> kvp in _replacement)
            {
                output.Replace(kvp.Key, kvp.Value);
            }
        }

EmailBody = output.ToString();

thinkingserious commented 7 years ago

Thanks for the suggestion @kvishalv!

da1rren commented 7 years ago

I have updated and cleaned up the above extension method. To reflect the latest API (9.6.0). I should add I haven't test this yet but should do before close of business today and will amend this post with any fixes.

    public static partial class MailMessageExtensions
    {
        public static EmailAddress GetSendGridAddress(this MailAddress address)
        {
            return String.IsNullOrWhiteSpace(address.DisplayName) ?
                new EmailAddress(address.Address) :
                new EmailAddress(address.Address, address.DisplayName.Replace(",", "").Replace(";", ""));
        }

        public static SendGrid.Helpers.Mail.Attachment GetSendGridAttachment(this System.Net.Mail.Attachment attachment)
        {
            using (var stream = new MemoryStream())
            {
                attachment.ContentStream.CopyTo(stream);
                return new SendGrid.Helpers.Mail.Attachment()
                {
                    Disposition = "attachment",
                    Type = attachment.ContentType.MediaType,
                    Filename = attachment.Name,
                    ContentId = attachment.ContentId,
                    Content = Convert.ToBase64String(stream.ToArray())
                };
            }
        }

        public static SendGridMessage GetSendGridMessage(this MailMessage message)
        {
            var sendgridMessage = new SendGridMessage();

            sendgridMessage.From = GetSendGridAddress(message.From);

            if (message.ReplyToList.Any())
            {
                sendgridMessage.ReplyTo = message.ReplyToList.First().GetSendGridAddress();
            }

            if(message.To.Any())
            {
                var tos = message.To.Select(x => x.GetSendGridAddress()).ToList();
                sendgridMessage.AddTos(tos);
            }

            if (message.CC.Any())
            {
                var cc = message.CC.Select(x => x.GetSendGridAddress()).ToList();
                sendgridMessage.AddCcs(cc);
            }

            if(message.Bcc.Any())
            {
                var bcc = message.Bcc.Select(x => x.GetSendGridAddress()).ToList();
                sendgridMessage.AddBccs(bcc);
            }

            if (!string.IsNullOrWhiteSpace(message.Subject))
            {
                sendgridMessage.Subject = message.Subject;
            }

            if (!string.IsNullOrWhiteSpace(message.Body))
            {
                var content = message.Body;

                if (message.IsBodyHtml)
                {

                    if (content.StartsWith("<html"))
                    {
                        content = message.Body;
                    }
                    else
                    {
                        content = $"<html><body>{message.Body}</body></html>";
                    }

                    sendgridMessage.AddContent("text/html", content);
                }
                else
                {
                    sendgridMessage.AddContent("text/plain", content);
                }
            }

            if(message.Attachments.Any())
            {
                sendgridMessage.Attachments = new System.Collections.Generic.List<SendGrid.Helpers.Mail.Attachment>();
                sendgridMessage.Attachments.AddRange(message.Attachments.Select(x => GetSendGridAttachment(x)));
            }

            return sendgridMessage;
        }
    }
da1rren commented 7 years ago

Furthermore if you believe this would make a useful feature. I would be open to making this extension into a PR with associated tests etc.

thinkingserious commented 7 years ago

I think this would be a great addition as a helper! Thank you!

da1rren commented 7 years ago

Upon investigation the System.Net.Mail assembly wasn't inlcuded in the .NET standard v1.3. However it appears that it has been ported to v2.0. Once the v2.0 standard is released I will provide a pull request.

thinkingserious commented 7 years ago

Thanks for the update @da1rren, that sounds good.

TrendyTim commented 7 years ago

Thank you to @APM3 and @da1rren that helped me with a speedy migration from SMTP to API.

I needed to add support for headers in the conversion (especially the X-SMTPAPI header which the webapi doesn't extract from the message so needs special treatment) so i thought i'd contribute my additions, and threw in priority support as well (not that i think we ever really used in in our system, but the calling class supported it so why not).

I converted from vb.net to c# to match the posted code so please forgive any noncompilabilities/faux pas

foreach (void hdr_loopVariable in message.Headers.AllKeys) {
    hdr = hdr_loopVariable;
    if (hdr == "X-SMTPAPI") {
              //deserailize X-SMTPAPI JSON from format {"unique_args":{"Arg1":"Value1"}}
        dynamic xSmtpApiDict = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, Dictionary<string, string>>>(message.Headers.Item(hdr));
        Dictionary<string, string> uniqueArgs =null;
        if (xSmtpApiDict.TryGetValue("unique_args", uniqueArgs)) {
            foreach (KeyValuePair<string, string> de in uniqueArgs) {
                sendgridMessage.AddCustomArg(de.Key, de.Value);
            }
        }
    } else {
              //add normal header
        sendgridMessage.AddHeader(hdr, message.Headers.Item(hdr));
    }
}

if (message.Priority != Net.Mail.MailPriority.Normal) {
    string xpri = "3";
    string imp = "Normal";

    switch (message.Priority) {
        case Net.Mail.MailPriority.High:
            xpri = "1";
            imp = "High";
            break;
        case Net.Mail.MailPriority.Low:
            xpri = "5";
            imp = "Low";
            break;
        default:
                      //use defaults
            break;
    }
    sendgridMessage.AddHeader("X-Priority", xpri);
    sendgridMessage.AddHeader("X-MSMail-Priority", imp);
    sendgridMessage.AddHeader("Importance", imp);
    //"X-Priority" (values: 1 to 5- from the highest[1] to lowest[5]),
    //"X-MSMail-Priority" (values: High, Normal, Or Low),
    //"Importance" (values: High, Normal, Or Low).
}

Not the greatest, I'm sure i could probably pre-process the string to just get one dictionary instead of the nested dictionaries, but i can't know what everyone put in the X-SMTPAPI header, and at this stage i only cared about keeping unique_args to support the webhooks.

Hope that helps someone.

thinkingserious commented 7 years ago

Hi @TrendyTim,

Would you mind adding your example and perhaps a synthesis of the solutions in this thread to our TROUBLESHOOTING.md for hacktoberfest?

With Best Regards,

Elmer

electricessence commented 6 years ago

So nothing like this has been added to >9.6.0 yet?

thinkingserious commented 6 years ago

Not yet @electricessence.

Were you considering a PR? :)

electricessence commented 6 years ago

@thinkingserious, I'm just surprised it hasn't happened yet. I could easily add what @da1rren posted earlier.

electricessence commented 6 years ago

So just for giggles, I forked and tried to update the code. Issues I ran into: StyleCop doesn't seem to properly respect the copyright setting? It's strange. And just a note, the copyright in the stylecop,json needs fixing. Then, because of how the dependencies are arranged, I admittedly am not versed enough using the VS Community 2017 to add dependencies for both 4.5.2 and .NET Standard 1.3. :(

electricessence commented 6 years ago

Here's what I was working on to add SendGrid as an "EmailProcessor" injectable interface/code. https://github.com/electricessence/NetMail It has the code from above.

thinkingserious commented 6 years ago

Hi @electricessence,

I will take care of the dependencies and StyleCop issues. Thanks!

jmevel commented 6 years ago

Hello. I see this is still a "work in progress" since March 2nd. Do I need to create a custom implementation of this in my project or could we expect it to arrive soon in a near-coming version of SendGrid?

Thank you

thinkingserious commented 6 years ago

Hello @jmevel,

So far, there has not been a PR submitted, but I marked it as work in progress because two people on this thread offered to submit a PR.

Beyond that, it may be a while before we implement this internally as it's not too high on our backlog right now.

My suggestion, if this is time sensitive, is for you to create a custom implementation, or if you are up to it, create a PR here.

With Best Regards,

Elmer

jmevel commented 6 years ago

Hello,

Thanks for your reply @thinkingserious. My task on writing an email component at my job has been postponed for the moment.

When time comes I will create a custom implementation then and I'll see if this could lead me to a PR in SendGrid repo or not...

Regards

thinkingserious commented 6 years ago

Thanks @jmevel!

da1rren commented 6 years ago

The problem with creating a pull request is simply that .net core 1.0 does not contain System.Net.Mail.MailMessage. Which is the minimum version of .net core the SendGrid client currently supports.

I don't know of a nice way of including the helper method. Until send grid drops support for .net core 1.0 & 1.1. Aside from some nasty if def stuff.

thinkingserious commented 6 years ago

Hi @da1rren,

Thanks for bringing your expertise to the conversation! Perhaps, we just have to get nasty in this case, as I don't see us dropping support for 1.0 & 1.1 :)

With Best Regards,

Elmer

da1rren commented 6 years ago

I have a plan. Will fork and take a look at the weekend. It might not be nearly as bad as I would think.

Mek7 commented 3 years ago

Hi, I found some code in pull request #746 and was able to use that to make migrating from SMTP to SendGrid API much easier. It works, however, if you use AlternateViews in the old MailMessage object (to send both HTML and plain version in the same e-mail), it is not migrated to the SendGridMessage object. I had to modify the ToSendGridMessage method as follows:

        /// <summary>
        /// Converts a System.Net.Mail.MailMessage to a SendGrid message.
        /// </summary>
        /// <param name="message">The MailMessage to be converted</param>
        /// <returns>Returns a <see cref="SendGridMessage"/> with the properties from the MailMessage</returns>
        public static SendGridMessage ToSendGridMessage(this MailMessage message)
        {
            var sendGridMessage = new SendGridMessage();

            sendGridMessage.From = ToSendGridAddress(message.From);

            if (message.ReplyToList.Any())
            {
                if (message.ReplyToList.Count > 1)
                {
                    throw new ArgumentException("Sendgrid only supports one reply to address.");
                }

                sendGridMessage.ReplyTo = message.ReplyToList.Single().ToSendGridAddress();
            }

            if (message.To.Any())
            {
                var tos = message.To.Select(ToSendGridAddress).ToList();
                sendGridMessage.AddTos(tos);
            }

            if (message.CC.Any())
            {
                var cc = message.CC.Select(ToSendGridAddress).ToList();
                sendGridMessage.AddCcs(cc);
            }

            if (message.Bcc.Any())
            {
                var bcc = message.Bcc.Select(ToSendGridAddress).ToList();
                sendGridMessage.AddBccs(bcc);
            }

            if (!string.IsNullOrWhiteSpace(message.Subject))
            {
                sendGridMessage.Subject = message.Subject;
            }

            if (message.Headers.Count > 0)
            {
                var headers = message.Headers.AllKeys.ToDictionary(x => x, x => message.Headers[x]);
                sendGridMessage.AddHeaders(headers);
            }

            if (!string.IsNullOrWhiteSpace(message.Body))
            {
                var htmlAlternateView = message.AlternateViews.FirstOrDefault(av => av.ContentType.MediaType == "text/html");

                // if we have HTML alternate view, use it as body
                if (htmlAlternateView != null)
                {
                    using (StreamReader reader = new StreamReader(htmlAlternateView.ContentStream))
                    {
                        htmlAlternateView.ContentStream.Seek(0, SeekOrigin.Begin);
                        sendGridMessage.AddContent("text/html", reader.ReadToEnd());
                    }
                }

                var content = message.Body;

                // if body is html, only add it if alternate view is not present (and was not already added as body)
                if (message.IsBodyHtml && htmlAlternateView == null)
                {
                    content = content.Contains("<html")
                        ? message.Body
                        : $"<html><body>{message.Body}</body></html>";

                    sendGridMessage.AddContent("text/html", content);
                }
                else if (!message.IsBodyHtml)
                {
                    sendGridMessage.AddContent("text/plain", content);
                }
            }

            if (message.Attachments.Any())
            {
                sendGridMessage.Attachments = new List<SendGrid.Helpers.Mail.Attachment>();
                sendGridMessage.Attachments.AddRange(message.Attachments.Select(ToSendGridAttachment));
            }

            return sendGridMessage;
        }

It simply uses AlternateView of type text/html if it finds it (and adds it into the Contents of resulting SendGridMessage. If there is no AlternateView, then the body is represented as HTML or Plain depending on the IsBodyHtml flag of original MailMessage. This works for us, hope it will be helpful for someone else too. For complete code (other methods unchanged) refer to the above mentioned pull request.

sdavie-richards commented 2 years ago

Just came here to say that this was a huge help, thanks.