3

I am trying to write a small console app using C# on the top of .NET Core 2.2 framework.

The console app will make an HTTP-request to external API to get multiple images. I am able to make the request to the server and get the response. However, the server responds with a multipart response using MIMI messages.

I am able to parse the request and get the MIME-body for every message. But, I am unable to figure out how to create a file out of the content of the body.

Here is a sample of how the raw MIMI message begins with enter image description here

I tried writing the body as a string to the file but it did not work

string body = GetMimeBody(message);
File.WriteAllText("image_from_string" + MimeTypeMap.GetExtension(contentType), bytes);

I also tried to convert the string to byte[] like so but still did not work

byte[] bytes = Encoding.ASCII.GetBytes(body);
File.WriteAllBytes("image_from_ascii_bytes" + MimeTypeMap.GetExtension(contentType), bytes);

byte[] bytes = Encoding.Default.GetBytes(body);
File.WriteAllBytes("image_from_default_bytes" + MimeTypeMap.GetExtension(contentType), bytes);


byte[] bytes = Encoding.UTF8.GetBytes(body);
File.WriteAllBytes("image_from_utf8_bytes" + MimeTypeMap.GetExtension(contentType), bytes);

By "not working" I mean that the image does not open correctly. The photo viewer says "the image appears to be damaged or corrupted."

How can I correctly make a good image out of the message?

UPDATED

Here is the code along with the parsing parts

var responseContentType = response.Content.Headers.GetValues("Content-Type").FirstOrDefault();
string splitter = string.Format("--{0}", GetBoundary(responseContentType));
string content = await response.Content.ReadAsStringAsync();
var messages = content.Split(splitter, StringSplitOptions.RemoveEmptyEntries);

foreach (var message in messages)
{
    var mimiParts = message.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
    if (mimiParts.Length == 0)
    {
        continue;
    }

    string contentId = Str.GetValue("Content-ID", mimiParts, ':');
    string objectId = Str.GetValue("Object-ID", mimiParts, ':');
    string contentType = Str.GetValue("Content-Type", mimiParts, ':');

    if (string.IsNullOrWhiteSpace(contentId) || string.IsNullOrWhiteSpace(objectId) || string.IsNullOrWhiteSpace(contentType))
    {
        continue;
    }

    string body = mimiParts[mimiParts.Length - 1];

    var filename = string.Format("{0}_{1}{2}", contentId, objectId, MimeTypeMap.GetExtension(contentType));

    var decoded = System.Net.WebUtility.HtmlDecode(data);
    File.WriteAllText("image_from_html_decoded_bytes" + filename, decoded);
}

Here is the method that parses the message

public class Str
{
    public static string GetValue(string startWith, string[] lines, char splitter = '=')
    {
        foreach (var line in lines)
        {
            var value = line.Trim();

            if (!value.StartsWith(startWith, StringComparison.CurrentCultureIgnoreCase) || !line.Contains(splitter))
            {
                continue;
            }

            return value.Split(splitter)[1].Trim();
        }

        return string.Empty;
    }
}

Here is a screenshot showing the content of the mimiParts variable enter image description here

UPDATED 2

Based on the feedback below, I tried to use MimeKit packages instead of trying to parse the response myself. Below is how I tried to consume the response. However, I am still getting the same error as above. When writting the image file, I get image corrupted error.

var responseContentType = response.Content.Headers.GetValues("Content-Type").FirstOrDefault();

if (!ContentType.TryParse(responseContentType, out ContentType documentContentType))
{
    return;
}

var stream = await response.Content.ReadAsStreamAsync();

MimeEntity entity = MimeEntity.Load(documentContentType, stream);
Multipart messages = entity as Multipart;

if (messages == null)
{
    throw new Exception("Unable to cast entity to Multipart");
}

foreach (MimeEntity message in messages)
{
    string contentId = message.Headers["Content-ID"];
    string objectId = message.Headers["Object-ID"];
    string contentType = message.Headers["Content-Type"];

    if (string.IsNullOrWhiteSpace(contentId) || string.IsNullOrWhiteSpace(objectId) || string.IsNullOrWhiteSpace(contentType))
    {
        continue;
    }

    var filename = string.Format("{0}_{1}{2}", contentId, objectId, MimeTypeMap.GetExtension(contentType));

    message.WriteTo(filename);
}
Junior
  • 11,602
  • 27
  • 106
  • 212

2 Answers2

3

MimeEntity.WriteTo (file) will unfortunately include the MIME headers which is what is causing the corrupt error.

What you need to do is cast the MimeEntity to a MimePart and then save the decoded content using MimePart.Content.DecodeTo (stream):

var responseContentType = response.Content.Headers.GetValues("Content-Type").FirstOrDefault();

if (!ContentType.TryParse(responseContentType, out ContentType documentContentType))
{
    return;
}

var stream = await response.Content.ReadAsStreamAsync();

MimeEntity entity = MimeEntity.Load(documentContentType, stream);
Multipart multipart = entity as Multipart;

if (multipart == null)
{
    throw new Exception("Unable to cast entity to Multipart");
}

foreach (MimePart part in multipart.OfType<MimePart> ())
{
    string contentType = part.ContentType.MimeType;
    string contentId = part.ContentId;
    string objectId = part.Headers["Object-ID"];

    if (string.IsNullOrWhiteSpace(contentId) || string.IsNullOrWhiteSpace(objectId) || string.IsNullOrWhiteSpace(contentType))
    {
        continue;
    }

    var filename = string.Format("{0}_{1}{2}", contentId, objectId, MimeTypeMap.GetExtension(contentType));

    using (var output = File.Create (filename))
        part.Content.DecodeTo (output);
}
jstedfast
  • 35,744
  • 5
  • 97
  • 110
  • 1
    Well, what do you know. The maker of MimeKit advises on its function. If that's not the best support ever, I don't know what is. – Alexander Gräf Jan 29 '19 at 01:02
  • 1
    I try to pay attention to StackOverflow and answer MimeKit/MailKit questions whenever I see them :-) – jstedfast Jan 29 '19 at 12:56
  • @jstedfast I am wondering if there is a way to write the decoded content into a different stream (i.e, MemoyStream) so I can delegate the write-to-file process at a later time. I create a FileObject model-class (https://gist.github.com/CrestApps/ee07bc1a76766bbbbded5c1fe304ba5d) then I changed your code to this `var file = new FileObject(); file.Content = new MemoryStream(); message.Content.DecodeTo(file.Content);` but when I try to write file.Content into a file, the image becomes corrupt. How I can make a memory stream out of the MimePart so I can delegate the write process? – Junior Feb 02 '19 at 19:12
  • 2
    Remember to rewind your file.Content stream before writing it to a file or the file will be empty (aka “corrupt”). – jstedfast Feb 02 '19 at 19:54
  • Awesome! that was it. – Junior Feb 02 '19 at 23:10
2

MIME encoding is hard, and treating the bytes that the server sends as a string is already an error. Splitting it up at newlines will produce even more problems. Binary means that every value between 0x00 and 0xff is valid. But Unicode and ASCII have different ranges of valid bytes, and especially converting them is problematic. The .NET internal string class interprets each character as two bytes. The moment HttpContent.ReadAsStringAsync runs, it tries to interpret each single byte received from the server as a two-byte Unicode character. I'm pretty sure you won't be able to recover from that data loss.

  • Use a hex-editor like HxD to compare a good copy of the image to the one you are writing out from your application and look for differences. At least if you want to stick with your own code. But I'm sure that you'll still need to switch from string manipulation to Stream operations.
  • Use an already made MIME parsing library. One example is MimeKit. This will dramatically reduce your development time.

Just as a reference, here is how the first 10 bytes of a JPG should look like:

FF D8 FF E0 00 10 4A 46 49 46      ÿØÿà..JFIF
Alexander Gräf
  • 511
  • 1
  • 3
  • 10
  • Any idea how to loop through the MimeEntity children? I used `ContentType.Parse()` to get the content-type. Now, I created the `MimeEntity` like this `var stream = await response.Content.ReadAsStreamAsync(); MimeEntity messages = MimeEntity.Load(documentContentType, stream);` but how do I loop over the children aka messages? – Junior Jan 28 '19 at 18:56
  • @MikeA https://github.com/jstedfast/MimeKit/blob/master/Documentation/Examples/MultipartFormDataExample.cs – Alexander Gräf Jan 28 '19 at 19:10
  • @MikeA MimeMessage is the entire response from the server. It has properties like Attachments and BodyParts, and that's where your content should be. Each entry should be a specialization of the abstract MimeEntity. Although I don't have access to the data you're trying to parse, so I can only speculate. Just inspect the objects returned by MimeKit in the debugger. – Alexander Gräf Jan 28 '19 at 19:14
  • That example is what I am doing. I have looked in the debugger. I see a private property called "children" that contains all the objects I am looking for but unsure how to access that publicly. – Junior Jan 28 '19 at 19:32
  • @MikeA What's the type (class) of the object that has the private property "children"? – Alexander Gräf Jan 28 '19 at 19:38
  • @MikeA Here is an example on how to iterate the entities of the MimeMessage: http://www.mimekit.net/docs/html/T_MimeKit_MimeIterator.htm – Alexander Gräf Jan 28 '19 at 19:40
  • I am confused. The class `MimeEntity` has a private property called `children` which has all the files that I seem to need. Then I tried to create `MimeMessage` which seems to be required to construct `MimeIterator` object but that seems to be parsing the content incorrectly – Junior Jan 28 '19 at 20:09
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/187450/discussion-between-mike-a-and-alexander-graf). – Junior Jan 28 '19 at 22:06
  • MimeEntity is an abstract class. Please lookup the actual class the instance has in your debugger because it cannot be MimeEntity. Then we can lookup a way to access its contents. – Alexander Gräf Jan 28 '19 at 22:28
  • I updated my question (i.e Updated 2) I showed how I wrote the MimeEntity to the file. But still getting the same error as if I was parcing the message myself. – Junior Jan 28 '19 at 23:32