Sending MHT file using C# Mail Message - c#

Has anyone got any sample code to send MHT using SmtpClient and MailMessage in C#?
I have found tantalising references to using Alternate Views but I cannot figure out how to make this work with my SSRS generated MHT.
If anyone has any working code they are willing to share I would be greatly appreciative.

So it appears that .NET MailMessage does not support this out of the box. I have found a couple of approaches that can be used with varying degrees of success.
MHTML -> HTML Decoder
David Benko wrote https://github.com/DavidBenko/MHTML-to-HTML-Decoding-in-C-Sharp/blob/master/MHTMLParser.cs and while it works, I was unable to make the images appear when I viewed the resulting email. The HTML looked right, it just did not work in Outlook 2010.
Convert MimeKit MimeMessage to .NET Mail Message
As Jeffrey points out this is a start https://github.com/jstedfast/MimeKit/issues/140. I had problems again with the images not being displayed in the resulting message.
Use MimeKit & MailKit
I ended up using https://github.com/jstedfast/MimeKit to do the job. This can be achieved as follows:
MimeMessage messageMimeKit = MimeMessage.Load(Request.MapPath("~/Blah/378921.mht"));
messageMimeKit.From.Add(new MailboxAddress("Dev", "developer#gmail.com"));
messageMimeKit.To.Add(new MailboxAddress("Homer", "homer#gmail.com"));
messageMimeKit.Subject = "Another subject line";
using (var client = new MailKit.Net.Smtp.SmtpClient())
{
client.Connect("smtp.gmail.com", 465, true);
client.Authenticate("homer#gmail.com", "*****");
client.Send(messageMimeKit);
client.Disconnect(true);
}

Using System.Net.Http.Formatting
My approach leverages the ReadAsMultipartAsync extension methods on HttpContent found in the System.Net.Http.Formatting library. (part of the Microsoft.AspNet.WebApi.Client nuget package)
The only tricky bit is getting the correct content type out of the file. In order for the ReadAsMultipartAsync method to work, it needs the ContentType header on the stream content set to the correct type ("multipart/related"), including the correct boundary parameter on the content type value:
multipart/related;
boundary="----=_NextPart_01C35DB7.4B204430"
Please note, since my solution was only written for sending content from SSRS reports exported in the MTHML format, my code assumes the multipart content has only one text/html part with the remaining parts being images, but the technique is sound and could be adjusted to handle multipart content in a more generic way.
const string ContentTypePrefix = "Content-Type: ";
AlternateView htmlView = null;
var linkedResources = new List<LinkedResource>();
var content = File.ReadAllText("report.mhtml");
var startIndex = content.IndexOf(ContentTypePrefix, StringComparison.OrdinalIgnoreCase) + ContentTypePrefix.Length;
var endIndex = content.IndexOf("\"", startIndex, StringComparison.Ordinal) + 1;
var contentType = content.Substring(startIndex, endIndex);
using (var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(content)))
{
using (var stream = new StreamContent(memoryStream))
{
stream.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);
if (!stream.IsMimeMultipartContent())
{
throw new Exception("Not correct format.");
}
var parts = stream.ReadAsMultipartAsync().ConfigureAwait(false).GetAwaiter().GetResult();
foreach (var part in parts.Contents)
{
part.Headers.ContentType.CharSet = part.Headers.ContentType.CharSet?.Replace("\"", string.Empty); // Needed since the SSRS report defaults the charset to \"utf-8\", instead of utf-8 and the code fails to find the correct encoder
var compressedContent = part.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult();
if (!part.Headers.ContentType.MediaType.Contains("image"))
{
var encoding = Encoding.GetEncoding(part.Headers.ContentType.CharSet);
var partContent = encoding.GetString(Convert.FromBase64String(compressedContent));
htmlView = AlternateView.CreateAlternateViewFromString(partContent, encoding, part.Headers.ContentType.MediaType);
}
else
{
linkedResources.Add(new LinkedResource(new MemoryStream(Convert.FromBase64String(compressedContent)), part.Headers.ContentType.MediaType)
{
ContentId = part.Headers.ContentDisposition.FileName.Replace("\"", string.Empty)
});
}
}
}
}
foreach (var linkedResource in linkedResources)
{
htmlView?.LinkedResources.Add(linkedResource);
}
using (var message = new MailMessage("from#mail.com", "to#mail.com", "Subject", string.Empty))
{
message.BodyEncoding = Encoding.UTF8;
message.AlternateViews.Add(htmlView);
using (var client = new SmtpClient("smtpserver.com", 25))
{
client.Send(message);
}
}

Related

Send images using sendgrid C#

I am using sendgrid to send images in mails, but I am not sure what's wrong with my code.
Here is my code:
string imgPath= Server.MapPath(#"~/Images/logo-img.png");
StringBuilder sb = new StringBuilder();
sb.Append("Hi There! I am using sendgrid to send images");
sb.Append("<a href='someurl'><img src='cid:myImage.png> Login in</a>");
LinkedResource lr = new LinkedResource(imgPath, "image/png");
lr.ContentId = "myImage";
AlternateView AV = AlternateView.CreateAlternateViewFromString(sb.ToString(), null, "image/png");
AV.LinkedResources.Add(lr);
var from = new EmailAddress("abc#bac.com");
var subject = "Hi There!";
var to = new EmailAddress("xyz#xyz.com");
var plainTextContent = "Hello";
var htmlContent = sb.ToString();
var msg = MailHelper.CreateSingleEmail(from, to, subject, plainTextContent, htmlContent);
msg.SetFrom(from, "abc");
var sendingResult = SendMail(msg);
You put quote in wrong place:
sb.Append("<a href='someurl'><img src='cid:myImage.png'> Login in</a>");
I don't know what the LinkedResource or AlternateView classes do, but they don't appear to be part of the Twilio SendGrid C# library.
There is an example in the library docs for sending attachments, but it doesn't show how to address the content within the email. But it's close. The key is that you need to load the file you want to attach and transform it to base64, then use the AddAttachment method to add it as an inline attachment to your mail, passing the content ID (cid) that you can refer to in the body of the message. Something like:
string imgPath= Server.MapPath(#"~/Images/logo-img.png");
StringBuilder sb = new StringBuilder();
sb.Append("Hi There! I am using sendgrid to send images");
sb.Append("<a href='someurl'><img src='cid:myImage'> Login in</a>");
var bytes = File.ReadAllBytes(imgPath);
var file = Convert.ToBase64String(bytes);
var from = new EmailAddress("abc#bac.com");
var subject = "Hi There!";
var to = new EmailAddress("xyz#xyz.com");
var plainTextContent = "Hello";
var htmlContent = sb.ToString();
var msg = MailHelper.CreateSingleEmail(from, to, subject, plainTextContent, htmlContent);
msg.SetFrom(from, "abc");
msg.AddAttachment("logo-img.png", file, "image/png", "inline", "myImage");
var sendingResult = SendMail(msg);
In this updated code the important parts are loading the file as base64:
var bytes = File.ReadAllBytes(imgPath);
var file = Convert.ToBase64String(bytes);
and attaching the file to the message:
msg.AddAttachment("logo-img.png", file, "image/png", "inline", "myImage");
The AddAttachment method takes 5 arguements: the filename, the base64 content of the file, the mime type, the content disposition (for referring to images in the HTML you should use "inline"), and the cid. Note also that the cid is the entire string you should use to refer to the image, so if the image cid is "myImage" then you should refer to it as cid:myImage, like:
sb.Append("<a href='someurl'><img src='cid:myImage'> Login in</a>");

MimeMessage causing recipient address required error in Gmail Apis possibly due base64url encoding

Edit 1: After playing around further it appears that the space is not the culprit but possibly the base64url encode method is. I extracted the Raw string for both plain text and mime message and compared them in the the online tool. In case of plain text they both are same but in the case of mime message they are completely different however if I decode them back to string they are identical. I don't know a whole lot about encoding decoding so any help will be appreciated.
Edit end.
I am trying to compose email in c# using mimekit and send it with google.apis.gmail.v1. but it throws error "Recipient address required[400]. Here is the code.
var mailMessage = new MailMessage
{
Subject = "test subject",
Body = " <h1>test body</h1>"
};
mailMessage.IsBodyHtml = true;
mailMessage.To.Add("testemail");
MimeMessage mm = MimeMessage.CreateFromMailMessage(mailMessage);
var gmailMessage = new Google.Apis.Gmail.v1.Data.Message
{
Raw = Base64UrlEncode(mm.ToString())
};
service.Users.Messages.Send(gmailMessage, "me").execute();
// Encoding method
public static string Base64UrlEncode(string text)
{
byte[] bytes = Encoding.UTF8.GetBytes(text);
return Convert.ToBase64String(bytes)
.Replace('+', '-')
.Replace('/', '_')
.Replace("=", "");
}
The above code throws error however if I compose email using plain text like below:
string plainText = "To:testemail\r\n" +
"Subject: test subject\r\n" +
"Content-Type: text/html; charset=us-ascii\r\n\r\n" +
"<h1>Test body<h1>";
if I encode and pass this plain text to Raw property of gmail message this works flawlessly.
to debug the issue I decoded the plain text and mime message using an online tool and the only difference I notice is space before To header value like this:
// plain text:
To:testemail
//mime message:
To: testemail
notice the space after "To:". To confirm my suspicion I removed the space from mime message and encoded it again using online tool and passed the encoded string to Raw property and it works without error. So figured a hacky solution and tried removing the space using regex like this:
var corrected = Regex.Replace(mm.ToString(), #"To: ", "To:");
But for some reason that doesn't work which doesn't make sense to me. According to the documentation Raw property takes RFC 2822 formatted and base64url encoded string and from what I could find RFC 2822 doesn't allow space after the header type. Any help will be appreciated :)
For reference here is both strings decoded from raw one works and other doesn't;
//plain text that works:
To:test#test.com
Subject: Test subject
Content-Type: text/html; charset=us-ascii
<h1>Test body <h1>
// mime message that throw error
To: test#test.com
Subject: test message
Date: Date
MIME-Version: 1.0
Content-Type: text/html; charset=us-ascii
<h1>test body</h1>
The correct solution is something more like this:
var mailMessage = new MailMessage
{
Subject = "test subject",
Body = " <h1>test body</h1>"
};
mailMessage.IsBodyHtml = true;
mailMessage.To.Add("testemail");
MimeMessage mm = MimeMessage.CreateFromMailMessage(mailMessage);
byte[] rawMimeData;
using (var memory = new MemoryStream ()) {
mm.WriteTo(memory);
rawMimeData = memory.ToArray();
}
var gmailMessage = new Google.Apis.Gmail.v1.Data.Message
{
Raw = Base64UrlEncode(rawMimeData)
};
service.Users.Messages.Send(gmailMessage, "me").execute();
// Encoding method
public static string Base64UrlEncode(byte[] bytes)
{
return Convert.ToBase64String(bytes)
.Replace('+', '-')
.Replace('/', '_')
.Replace("=", "");
}
Finally Solved it!! For some reason that ToString method on MimeMessage adds carriage return and new line (\r\n) at the start and end of the string representation of mime message like so "\r\n-------string---------\r\n" which apparently chokes the gmail api so I just used the trim method like so mm.ToString().Trim('\r', '\n')
and voilĂ  It works flawlessly. Not sure its a bug or a expected behavior but Jstedfast if you ever get a chance to read this post please provide your feedback.

S/Mime with attachment sign using MailSystem.Net

I'm new to s/mime and need to digitally sign email with xml attachment, but unfortunately this email has a wrong hash value (according to response of external system). I digged into the code of the library and found that it creates a sign for base64-encoded body part, is it correct or the signature should be computed for xml attachment content?
Also here is some more issues:
Lots of headers/parameters are owerritten by library: for ex. ContentType parameters, some headers (like X-Mailer) and many others
It creates an empty boundary for Content-Type: text/plain, though I haven't any text except attachment
Here is my code:
public static void Sign(X509Certificate2 clientCert, string from, string to, string subject, string attachementPath)
{
Message message = new Message();
message.From = new Address(from);
message.To.Add(to);
message.ContentType.MimeType = "multipart/signed";
message.ContentType.Parameters.Add("protocol", "\"application/pkcs7-signature\"");
message.ContentTransferEncoding = ContentTransferEncoding.SevenBits;
message.AddHeaderField("MIME-Version", "1.0");
message.Subject = subject;
var mimePart = new MimePart(attachementPath, false);
mimePart.ContentTransferEncoding = ContentTransferEncoding.Base64;
mimePart.Charset = "windows-1251";
mimePart.ContentType.MimeType = "text/xml";
message.Attachments.Add(mimePart);
message.BuildMimePartTree();
CmsSigner signer = new CmsSigner(clientCert);
signer.IncludeOption = X509IncludeOption.EndCertOnly;
message.SmimeAttachSignatureBy(signer);
}

Cyrillic symbols in HttpClient POST request for upload filename

In one of my .NET applications I've go a method which uploads file to the site via HttpClient. Here is implementation
using (var clientHandler = new HttpClientHandler
{
CookieContainer = cookieContainer,
UseDefaultCredentials = true
})
{
using (var client = new HttpClient(clientHandler))
{
client.BaseAddress = requestAddress;
client.DefaultRequestHeaders.Accept.Clear();
using (var content = new MultipartFormDataContent())
{
var streamContent = new StreamContent(new MemoryStream(fileData));
streamContent.Headers.ContentDisposition = ContentDispositionHeaderValue.Parse("form-data");
streamContent.Headers.ContentDisposition.Parameters.Add(new NameValueHeaderValue("name", "contentFile"));
streamContent.Headers.ContentDisposition.Parameters.Add(new NameValueHeaderValue("filename", "\"" + fileName + "\""));
streamContent.Headers.ContentType = new MediaTypeHeaderValue(contentType);
content.Add(streamContent);
HttpResponseMessage response = await client.PostAsync("/Files/UploadFile", content);
if (response.IsSuccessStatusCode)
{
return true;
}
return false;
}
}
}
Method work fine. But when I pass Cyrillic symbols in fileName property generated post request filename has corrupted symbols like ????1.docx for exmaple where ? replaces the Cyrillic symbol. Is there any way to send Cyrillic symbols without corruption?
I believe filename is very limited on what you can do in terms of code page. I guess it only supports ASCII (not 100% sure). There is a better header you can use called filename* which is bit hard to google for since google will just remove the * and you get all the ordinary filename back.
Long story short you need to use this:
$"filename*=UTF-8''{fileName}"
You also might need to do some encoding of filename with regard to space, etc. You can google a bit more on that + check this SO post.
P.S. Some older browsers might not like it, you need to check your requirements.

C# encoding Shift-JIS vs. utf8 html agility pack

i have a problem. My goal is to save some Text from a (Japanese Shift-JS encoded)html into a utf8 encoded text file.
But i don't really know how to encode the text.. The HtmlNode object is encoded in Shift-JS. But after i used the ToString() Method, the content is corrupted.
My method so far looks like this:
public String getPage(String url)
{
String content = "";
HtmlDocument page = new HtmlWeb(){AutoDetectEncoding = true}.Load(url);
HtmlNode anchor = page.DocumentNode.SelectSingleNode("//div[contains(#class, 'article-def')]");
if (anchor != null)
{
content = anchor.InnerHtml.ToString();
}
return content;
}
I tried
Console.WriteLine(page.Encoding.EncodingName.ToString());
and got: Japanese Shift-JIS
But converting the html into a String produces the error. I thought there should be a way, but since documentation for html-agility-pack is sparse and i couldn't really find a solution via google, i'm here too get some hints.
Well, AutoDetectEncoding doesn't really work like you'd expect it to. From what i found from looking at the source code of the AgilityPack, the property is only used when loading a local file from disk, not from an url.
So there's three options. One would be to just set the Encoding
OverrideEncoding = Encoding.GetEncoding("shift-jis")
If you know the encoding will always be the same that's the easiest fix.
Or you could download the file locally and load it the same way you do now but instead of the url you'd pass the file path.
using (var client=new WebClient())
{
client.DownloadFile(url, "20130519-OYT1T00606.htm");
}
var htmlWeb = new HtmlWeb(){AutoDetectEncoding = true};
var file = new FileInfo("20130519-OYT1T00606.htm");
HtmlDocument page = htmlWeb.Load(file.FullName);
Or you can detect the encoding from your content like this:
byte[] pageBytes;
using (var client = new WebClient())
{
pageBytes = client.DownloadData(url);
}
HtmlDocument page = new HtmlDocument();
using (var ms = new MemoryStream(pageBytes))
{
page.Load(ms);
var metaContentType = page.DocumentNode.SelectSingleNode("//meta[#http-equiv='Content-Type']").GetAttributeValue("content", "");
var contentType = new System.Net.Mime.ContentType(metaContentType);
ms.Position = 0;
page.Load(ms, Encoding.GetEncoding(contentType.CharSet));
}
And finally, if the page you are querying returns the content-Type in the response you can look here for how to get the encoding.
Your code would of course need a few more null checks than mine does. ;)

Categories

Resources