jstedfast/MailKit

In linux saving a message does not use CRLF

Closed this issue · 2 comments

Describe the bug
We have a software that download Messages, then save with WriteToAsync() in a file stream. Running the code under linux save the file in eml format not using CRLF but using only LF. This generates a .eml file that some software (like outlook) does not render correctly if you have unicode.

The obvious solution is using directly stream

Platform (please complete the following information):

  • OS: Linux
  • .NET Runtime: .NET 8
  • .NET Framework: .NET 8
  • MailKit Version: Latest

To Reproduce
Take the program.cs attached. It scans an imbox and allow saving to disk the file. You send an email with unicode char, then save. You will see that saving with stream or passing from stream generates two different .eml file. In windows they are identical, in linux the one you save from WriteToAsync uses LF.

Expected behavior
Saving an .eml should always use CRLF, we expect saving from a message to produce the very same result that saving from a stream.

Code Snippets
You have a complete program.cs attached.

// See https://aka.ms/new-console-template for more information
using MailKit.Net.Imap;
using MailKit.Security;
using MailKit;
using MimeKit;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.IO;

class Program
{
    static async Task Main(string[] args)
    {
        var password = ReadPasswordFromFile("imappassword.txt");

        var config = new
        {
            DefaultSender = "xxx@xxxx.it",
            ImapPort = 993, // IMAP port is usually 993 for SSL
            ImapHost = "imap.xxxxxxx.com", 
            RequireCredentials = true,
            UserName = "xxx@xxxx.it",
            Password = password, // Use the password read from the file
            UseSsl = true,      
            CheckCertificateRevocation = false,
            SecurityProtocol = 1,
            UseOffice365Smtp = false,
            UseAnonymous = false
        };

        var token = new CancellationToken();

        var client = new ImapClient();
        SecureSocketOptions secureSocketOptions = SecureSocketOptions.SslOnConnect;
        await client.ConnectAsync(config.ImapHost, config.ImapPort, secureSocketOptions, token);
        client.AuthenticationMechanisms.Remove("XOAUTH2");
        var passwordRead = string.IsNullOrWhiteSpace(config.UserName) ? config.DefaultSender : config.UserName;
        await client.AuthenticateAsync(passwordRead, config.Password, token);

        // Open the folder INBOX
        var inbox = client.GetFolder("INBOX");
        await inbox.OpenAsync(FolderAccess.ReadOnly, token);

        // Print all emails with subject, sender, and unique identifier
        foreach (var summary in await inbox.FetchAsync(0, -1, MessageSummaryItems.UniqueId | MessageSummaryItems.Envelope, token))
        {
            Console.WriteLine($"Subject: {summary.Envelope.Subject}");
            Console.WriteLine($"Sender: {summary.Envelope.From}");
            Console.WriteLine($"Unique ID: {summary.UniqueId}");
            Console.WriteLine();
        }

        // Ask the user to select an email to download
        Console.WriteLine("Enter the unique ID of the email you want to download:");
        var input = Console.ReadLine();
        if (UniqueId.TryParse(input, out var uniqueId))
        {
            // Option 1: Download the entire message
            var message = await inbox.GetMessageAsync(uniqueId, token);
            Console.WriteLine($"Downloaded email with subject: {message.Subject}");

            // Save the message to a file in the current directory
            var filePath = Path.Combine(Directory.GetCurrentDirectory(), $"{uniqueId}.eml");
            await using (var stream = File.Create(filePath))
            {
                await message.WriteToAsync(stream, token);
            }
            Console.WriteLine($"Email saved to {filePath}");

            // Option 2: Use GetStreamAsync to download the message
            var streamFilePath = Path.Combine(Directory.GetCurrentDirectory(), $"{uniqueId}_stream.eml");
            await using (var stream = File.Create(streamFilePath))
            {
                var directStream = await inbox.GetStreamAsync(uniqueId, token);
                await directStream.CopyToAsync(stream, token);
            }
            Console.WriteLine($"Email saved to {streamFilePath} using GetStreamAsync");
        }
        else
        {
            Console.WriteLine("Invalid unique ID.");
        }

        await client.DisconnectAsync(true, token);
    }

    static string ReadPasswordFromFile(string fileName)
    {
        var currentDirectory = Directory.GetCurrentDirectory();
        while (currentDirectory != null)
        {
            var filePath = Path.Combine(currentDirectory, fileName);
            if (File.Exists(filePath))
            {
                return File.ReadAllText(filePath).Trim();
            }
            currentDirectory = Directory.GetParent(currentDirectory)?.FullName;
        }
        throw new FileNotFoundException($"The file {fileName} was not found in any parent directory.");
    }
}

This is actually intentional because email messages on Linux from most clients are saved in UNIX format (i.e. LF-only).

You can override this by creating a custom FormatOptions and then passing that set of options to the MimeMessage.WriteTo(FormatOptions, Stream, CancellationToken) method.

The way I'd recommend doing this is:

var formatOptions = FormatOptions.Default.Clone ();
formatOptions.NewLineFormat = NewLineFormat.Dos;

// ...

await message.WriteToAsync(formatOptions, stream, token);

Thanks, actually I solved using the GetStream to download the message, so I'm downloading the original message stream from the server, I think that it can be the best option.