Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
rpanjwani opened this issue Jul 8, 2016 · 31 comments
Open

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

rpanjwani opened this issue Jul 8, 2016 · 31 comments
Labels
difficulty: hard fix is hard in difficulty status: work in progress Twilio or the community is in the process of implementing type: community enhancement feature request not on Twilio's roadmap

Comments

@rpanjwani
Copy link

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:

  • sendgrid-csharp Version: master (latest commit: [commit number])
  • .NET Version: 4.5.2
@thinkingserious
Copy link
Contributor

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

@thinkingserious thinkingserious added type: community enhancement feature request not on Twilio's roadmap status: help wanted requesting help from the community labels Jul 8, 2016
@rpanjwani
Copy link
Author

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
Copy link
Contributor

@rpanjwani,

Have you tried this? andrewdavey/postal#130 (comment)

@rpanjwani
Copy link
Author

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
Copy link
Contributor

@rpanjwani,

Please let us know how it goes. Thanks!

@APM3
Copy link

APM3 commented Aug 5, 2016

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
Copy link
Contributor

This is awesome @APM3!

Could you please email us at [email protected] with your T-shirt size and mailing address?

@kvishalv
Copy link

kvishalv commented Jun 1, 2017

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
Copy link
Contributor

Thanks for the suggestion @kvishalv!

@da1rren
Copy link

da1rren commented Aug 3, 2017

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
Copy link

da1rren commented Aug 3, 2017

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
Copy link
Contributor

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

@da1rren
Copy link

da1rren commented Aug 7, 2017

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
Copy link
Contributor

Thanks for the update @da1rren, that sounds good.

@thinkingserious thinkingserious added difficulty: hard fix is hard in difficulty hacktoberfest labels Sep 30, 2017
@TrendyTim
Copy link

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
Copy link
Contributor

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
Copy link

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

@thinkingserious
Copy link
Contributor

Not yet @electricessence.

Were you considering a PR? :)

@electricessence
Copy link

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

@electricessence
Copy link

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
Copy link

electricessence commented Jan 3, 2018

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
Copy link
Contributor

Hi @electricessence,

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

@thinkingserious thinkingserious added status: work in progress Twilio or the community is in the process of implementing and removed status: help wanted requesting help from the community labels Mar 1, 2018
@jmevel
Copy link

jmevel commented Aug 31, 2018

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
Copy link
Contributor

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
Copy link

jmevel commented Sep 6, 2018

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
Copy link
Contributor

Thanks @jmevel!

@da1rren
Copy link

da1rren commented Sep 14, 2018

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
Copy link
Contributor

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
Copy link

da1rren commented Sep 21, 2018

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

@da1rren da1rren mentioned this issue Sep 25, 2018
6 tasks
@Mek7
Copy link

Mek7 commented Nov 11, 2020

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
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
difficulty: hard fix is hard in difficulty status: work in progress Twilio or the community is in the process of implementing type: community enhancement feature request not on Twilio's roadmap
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants