I have been looking through many answers on this I just can't quite seem to understand how to do it though, even if its glaringly obvious.
I want to write values from a table in my database to a text file and attach the text file to an email without storing it locally. I am using Sendgrid API to manage sending the email and I was attempting to use MemoryStream to store the data.
Below is what I have attempted
private MemoryStream WriteToTextFile(IEnumerable<Location> locations)
{
MemoryStream memoryStream = new MemoryStream();
using (StreamWriter streamWriter = new StreamWriter(memoryStream))
{
foreach (var location in locations)
{
streamWriter.WriteLine($"{location.Time},{location.Location},{ location.LocationAccuracy},{ location.IsAlertRaised}");
}
streamWriter.Flush();
}
return memoryStream;
}
Then my attempt to attach it to the email
memoryStream.Seek(0, SeekOrigin.Begin);
var message = MailHelper.CreateSingleEmail(from, to, subject, plainTextContent, htmlContent);
message.AddAttachment(memoryStream, fileName);
var response = await _client.SendEmailAsync(message);
memoryStream.Dispose();
The error is straightforward "cannot convert from 'System.IO.MemoryStream' to 'string'
The solution I was attempting was based on a question asked here.
AddAttachment from MemoryStream
There are two problems here.
The using block in the first sample will call streamWriter.Dispose() before returning the stream. The Dispose() operation will close your MemoryStream, leaving it unusable.
The example code from your link seems to be using a different, possibly older, version of the SendGrid API. Looking at the current API, the only overload from the AddAttachment() family that accepts a stream looks like this:
public async Task AddAttachmentAsync(string filename, Stream contentStream, string type = null, string disposition = null, string content_id = null, CancellationToken cancellationToken = default(CancellationToken))
All of the other options required a base64content string instead. You can also just read the code for the method in the API link above for a good example of how to convert that stream to base64.
Related
I've been searching Google/SO for the past 3 days without success and hoping someone can help or point me in the right direction.
We have a method (with a bunch to overloads) that sends emails. I'm tasked with saving the email into a database.
Question
How can I extract the contents of an attachment into a byte[] in order to save it to the database?
I've read a lot of samples that saves attachments to disk, but I want to avoid saving to disk then reading that into memory (maybe that's why I haven't found anything because it's not possible, doubt it).
So I think what your asking is how to convert the stream to bytes then you will not have to save it to disk. You can use this extension method to do that.
public static class StreamExtensions
{
public static byte[] ReadAllBytes(this Stream instream)
{
if (instream is MemoryStream)
return ((MemoryStream) instream).ToArray();
using (var memoryStream = new MemoryStream())
{
instream.CopyTo(memoryStream);
return memoryStream.ToArray();
}
}
}
see
https://stackoverflow.com/a/33611922/237109
with that method you can do this
System.Net.Mail.MailMessage m = new MailMessage();
foreach (var element in m.Attachments)
{
byte[] bytes = element.ContentStream.ReadAllBytes()
element.ContentType // you will want to save this as well so you can convert it to a file when you need to pull it back out of the database.
}
I have located my smime.p7m from my email message, I read it as stream and try to decrypt it using MimeKit, but it failed with Operation is not valid due to the current state of the object.
using (MemoryStream ms = new MemoryStream(data)) {
CryptographyContext.Register(typeof(WindowsSecureMimeContext));
ApplicationPkcs7Mime p7m = new ApplicationPkcs7Mime(SecureMimeType.EnvelopedData, ms);
var ctx = new WindowsSecureMimeContext(StoreLocation.CurrentUser);
p7m.Verify(ctx, out MimeEntity output);
}
Following the example on https://github.com/jstedfast/MimeKit doesn't help either. Anyone familiar with MimeKit could chime in?
EDIT:
After decrypting the p7m, am I supposed to use the MimeParser to parse the content? I got the following from the decryption:
Content-Type: application/x-pkcs7-mime; name=smime.p7m; smime-type=signed-data
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=smime.p7m
MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCAJIAEWUNvbnRl
bnQtVHlwZTogdGV4dC9wbGFpbjsNCgljaGFyc2V0PSJ1cy1hc2NpaSINCkNvbnRlbnQtVHJhbnNm
ZXItRW5jb2Rpbmc6IDdiaXQNCg0KdGVzdA0KAAAAAAAAoIImTTCCBaIwggOKoAMCAQICBguC3JQz
...more...
But when parsing with MimeParser,
System.FormatException: Failed to parse message headers.
at MimeKit.MimeParser.ParseMessage(Byte* inbuf, CancellationToken cancellationToken)
at MimeKit.MimeParser.ParseMessage(CancellationToken cancellationToken)
UPDATE:
Ah, so it turns, calling Decrypt only gives me the SignedData, I need to then call Verify to pull the original data... this is kind of misleading, I thought Verify would simply verify it... which is why I didn't bother calling it, since I don't really need to verify it... Perhaps it should be call Decode instead? That's what I was trying to do originally, ((MimePart) signedData).Content.DecodeTo(...).
So in the end, I had to do something like this to extract the data.
CryptographyContext.Register(typeof(WindowsSecureMimeContext));
ApplicationPkcs7Mime p7m = new ApplicationPkcs7Mime(SecureMimeType.EnvelopedData, ms);
var ctx = new WindowsSecureMimeContext(StoreLocation.CurrentUser);
if (p7m != null && p7m.SecureMimeType == SecureMimeType.EnvelopedData)
{
// the top-level MIME part of the message is encrypted using S/MIME
p7m = p7m.Decrypt() as ApplicationPkcs7Mime;
}
if (p7m != null && p7m.SecureMimeType == SecureMimeType.SignedData)
{
p7m.Verify(out MimeEntity original); // THE REAL DECRYPTED DATA
using (MemoryStream dump = new MemoryStream())
{
original.WriteTo(dump);
decrypted = dump.GetBuffer();
}
}
You are getting an InvalidOperationException because you are calling Verify() on a EncryptedData.
You need to call Decrypt().
Verify() is for SignedData.
I'm trying to download a nodeServices generated pdf file that is in the form of byte array. here is my original code:
[HttpGet]
[Route("[action]/{appId}")]
public async Task<IActionResult> Pdf(Guid appId, [FromServices] INodeServices nodeServices)
{
// generateHtml(appId) is a function where my model is converted to html.
// then nodeservices will generate the pdf for me as byte[].
var result = await nodeServices.InvokeAsync<byte[]>("./pdf",
await generateHtml(appId));
HttpContext.Response.ContentType = "application/pdf";
HttpContext.Response.Headers.Add("x-filename", "myFile.pdf");
HttpContext.Response.Headers.Add("Access-Control-Expose-Headers", "x-filename");
HttpContext.Response.Body.Write(result, 0, result.Length);
return new ContentResult();
}
This code was working fine, it will show the pdf file in the browser, eg. chrome, and when I try to download it, I get "failed, network error".
I've searched here and there, I saw some suggestions to return File instead:
return File(result, "application/pdf");
that didn't work either, another thing was adding "Content-Disposition" header:
HttpContext.Response.Headers.Add("Content-Disposition", string.Format("inline;filename={0}", "myFile.pdf"));
others suggested to use FileStreamResult, no good either.
I realized that the problem could be about my generated file (byte[]) does not have a path or a link of his own, so I saved the bytes to my server, then got the file again by its path, then to memory stream, and finally return a file containing the memory stream:
var result = await nodeServices.InvokeAsync<byte[]>("./pdf", await generateHtml(appId));
var tempfilepath = Path.Combine(_environment.WebRootPath, $"temp/{appId}.pdf");
System.IO.File.WriteAllBytes(tempfilepath, result);
var memory = new MemoryStream();
using (var stream = new FileStream(tempfilepath, FileMode.Open))
{
await stream.CopyToAsync(memory);
}
memory.Position = 0;
return File(memory, "application/pdf", Path.GetFileName(tempfilepath));
which worked! it showed the file in the browser, and I could download it, but, I did not want any files to be stored on my server, my question is, can't I just download the file without the need to store it?
You can still return the FileContentResult without converting the byte array to a stream. There is an overload of the File() method which takes fileContents as a byte array and the contentType as a string.
So you can refactor to something like:
public async Task<IActionResult> Pdf(Guid appId, [FromServices] INodeServices nodeServices)
{
var result = await nodeServices.InvokeAsync<byte[]>("./pdf",
await generateHtml(appId));
return File(result, "application/pdf","myFile.pdf");
}
I have a function I use for aggregating streams from a zip archive.
private void ExtractMiscellaneousFiles()
{
foreach (var miscellaneousFileName in _fileData.MiscellaneousFileNames)
{
var fileEntry = _archive.GetEntry(miscellaneousFileName);
if (fileEntry == null)
{
throw new ZipArchiveMissingFileException("Couldn't find " + miscellaneousFileName);
}
var stream = fileEntry.Open();
OtherFileStreams.Add(miscellaneousFileName, (DeflateStream) stream);
}
}
This works well in most cases. However, if I have a zip within a zip, I get an excpetion on casting the stream to a DeflateStream:
System.InvalidCastException: Unable to cast object of type 'System.IO.Compression.SubReadStream' to type 'System.IO.Compression.DeflateStream'.
I am unable to find Microsoft documentation for a SubReadStream. I would like my zip within a zip as a DeflateStream. Is this possible? If so how?
UPDATE
Still no success. I attempted #Sunshine's suggestion of copying the stream using the following code:
private void ExtractMiscellaneousFiles()
{
_logger.Log("Extracting misc files...");
foreach (var miscellaneousFileName in _fileData.MiscellaneousFileNames)
{
_logger.Log($"Opening misc file stream for {miscellaneousFileName}");
var fileEntry = _archive.GetEntry(miscellaneousFileName);
if (fileEntry == null)
{
throw new ZipArchiveMissingFileException("Couldn't find " + miscellaneousFileName);
}
var openStream = fileEntry.Open();
var deflateStream = openStream;
if (!(deflateStream is DeflateStream))
{
var memoryStream = new MemoryStream();
deflateStream.CopyTo(memoryStream);
memoryStream.Position = 0;
deflateStream = new DeflateStream(memoryStream, CompressionLevel.NoCompression, true);
}
OtherFileStreams.Add(miscellaneousFileName, (DeflateStream)deflateStream);
}
}
But I get a
System.NotSupportedException: Stream does not support reading.
I inspected deflateStream.CanRead and it is true.
I've discovered this happens not just on zips, but on files that are in the zip but are not compressed (because too small, for example). Surely there's a way to deal with this; surely someone has encountered this before. I'm opening a bounty on this question.
Here's the .NET source for SubReadStream, thanks to #Quantic.
The return type of ZipArchiveEntry.Open() is Stream. An abstract type, in practice it can be a DeflateStream (you'd be happy), a SubReadStream (boo) or a WrappedStream (boo). Woe be you if they decide to improve the class some day and use a ZopfliStream (boo). The workaround is not good, you are trying to deflate data that is not compressed (boo).
Too many boos.
Only good solution is to change the type of your OtherFileStreams member. We can't see it, smells like a List<DeflateStream>. It needs to be a List<Stream>.
So it looks like the when storing a zip file inside another zip it doesn't deflate the zip but rather just inlines the content of the zip with the rest of the files with some information that these entries are part of a sub zip file. Which makes sense because applying compression to something that is already compressed is a waste of time.
This zip file is marked as CompressionMethodValues.Stored in the archive, which causes .NET to just return the original stream it read instead to wrapping it in a DeflateStream.
Source here: https://github.com/dotnet/corefx/blob/master/src/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs#L670
You could pass the stream into a ZipArchive, if it's not a DeflateStream (if you are interested in the file inside)
var stream = entry.Open();
if (!(stream is DeflateStream))
{
var subArchive = new ZipArchive(stream);
}
Or you can copy the stream to a FileStream (if you want to save it to disk)
var stream = entry.Open();
if (!(stream is DeflateStream))
{
var fs = File.Create(Path.GetTempFileName());
stream.CopyTo(fs);
fs.Close();
}
Or copy to any stream you are interested in using.
Note: This is also how .NET 4.6 behaves
The SendGrid API docs specify you can add attachments from a Stream. The example it gives uses a FileStream object.
I have some blobs in Azure Storage which I'd like to email as attachments. To achieve this I'm trying to use a MemoryStream:
var getBlob = blobContainer.GetBlobReferenceFromServer(fileUploadLink.Name);
if(getBlob != null)
{
// Get file as a stream
MemoryStream memoryStream = new MemoryStream();
getBlob.DownloadToStream(memoryStream);
emailMessage.AddAttachment(memoryStream, fileUploadLink.Name);
}
emailTransport.Deliver(emailMessage);
It sends fine but when the email arrives, the attachment appears to be there but it's actually empty. Looking at the email source, there is no content for the attachment.
Is using a MemoryStream a known limitation when using the SendGrid C# API to send attachments? Or should I be approaching this in some other way?
You probably just need to reset the stream position back to 0 after you call DownloadToStream:
var getBlob = blobContainer.GetBlobReferenceFromServer(fileUploadLink.Name);
if (getBlob != null)
{
var memoryStream = new MemoryStream();
getBlob.DownloadToStream(memoryStream);
memoryStream.Seek(0,SeekOrigin.Begin); // Reset stream back to beginning
emailMessage.AddAttachment(memoryStream, fileUploadLink.Name);
}
emailTransport.Deliver(emailMessage);
You might want to check who cleans up the stream as well and if they don't you should dispose of it after you've called Deliver().
According to their API, they have implemented void AddAttachment(Stream stream, String name).
You are probably using a MemoryStream which you have written to before. I suggest resetting the position inside the stream to the beginning, like:
memoryStream.Seek(0, SeekOrigin.Begin);
I ended up with the following which fixed the issue for me:
fileByteArray = new byte[getBlob.Properties.Length];
getBlob.DownloadToByteArray(fileByteArray, 0);
attachmentFileStream = new MemoryStream(fileByteArray);
emailMessage.AddAttachment(attachmentFileStream, fileUploadLink.Name);
The thread is a bit old, but I use a varient with NReco PDF converter:
private async Task SendGridasyncBid(string from, string to, string displayName, string subject, **byte[] PDFBody**, string TxtBody, string HtmlBody)
{
...
var myStream = new System.IO.MemoryStream(**PDFBody**);
myStream.Seek(0, SeekOrigin.Begin);
myMessage.AddAttachment(myStream, "NewBid.pdf");
...
}
convert the html to pdf and return it instead of writing it for download...
private byte[] getHTML(newBidViewModel model)
{
string strHtml = ...;
HtmlToPdfConverter pdfConverter = new HtmlToPdfConverter();
pdfConverter.CustomWkHtmlArgs = "--page-size Letter";
var pdfBytes = pdfConverter.GeneratePdf(strHtml);
return **pdfBytes**;
}
I am not sure how efficient this is, but it is working for me and I hope it helps someone else get their attachments figured out.