Сегодня предлагаю взглянуть на то, как с помощью библиотеки MimeKit можно легко реализовать парсинг eml файлов на С#, на примере простейшего консольного ридера eml. Полный код проекта есть на GitHub, а сейчас, давайте разберем, что происходит.
Подготовка
Для парсинга определим для себя три класса:
- User, для хранения отправителя и получателя писем (email регистронезависим, по этому приводим к нижнему регистру):
record User
{
private string email;
public string Email
{
get => email;
set => email = value.ToLower();
}
public string Name { get; set; }
}
- Attachment, для хранения вложений из писем:
record Attachment
{
public string FileName { get; set; }
public string FullName { get; set; }
}
- Message, который будет отвечать за хранение спаршенного из eml сообщения и за его вывод в консоль:
record Message
{
public User From { get; set; } = new();
public HashSet<User> To { get; set; } = new();
public string Subject { get; set; } = string.Empty;
public HashSet<Attachment> Attachments { get; set; } = new();
public string Content { get; set; } = string.Empty;
public override string ToString()
{
var sb = new StringBuilder();
sb.AppendLine($"Тема: {Subject}");
sb.AppendLine( $"От:");
sb.AppendLine($"\t{From.Name} {From.Email}");
sb.AppendLine("Для:");
sb.AppendLine(string.Join("", To.Select(u=>$"\n\t{u.Name} {u.Email}")));
sb.AppendLine("Письмо:");
sb.AppendLine(string.Join("\n\t", HtmlUtilities.ConvertToPlainText(Content).Split("\n")));
if (Attachments.Any())
{
sb.AppendLine("Вложения: ");
sb.AppendLine(string.Join("", Attachments.Select(u => $"\n\t{u.FileName}")));
}
return sb.ToString();
}
}
Для красивого вывода Html содержимого письма в консоль я переопределил метод ToString() и воспользовался методом ConvertToPlainText из пространства имен HtmlUtilities из библиотеки ReadSharp.
Program.cs
Код парсера:
using var stream = File.OpenRead("test.eml");
var mimeMessage = MimeMessage.Load(stream);
var message = new Message();
message.Subject = mimeMessage.Subject;
message.From = new User
{
Email = mimeMessage.From.Mailboxes.First().Address,
Name = mimeMessage.From.Mailboxes.First().Name
};
foreach(var to in mimeMessage.To.Mailboxes)
{
message.To.Add(new User
{
Email = to.Address,
Name = to.Name
});
}
message.Content = mimeMessage.HtmlBody;
foreach (var attachment in mimeMessage.Attachments)
{
var fileName = "attached-message.eml";
if (attachment is MessagePart messagePart)
{
fileName = attachment.ContentDisposition?.FileName;
if (string.IsNullOrEmpty(fileName))
using (var fileStream = File.Create(fileName))
messagePart.Message.WriteTo(stream);
}
else
{
var part = attachment as MimePart;
fileName = part.FileName;
using (var fileStream = File.Create(fileName))
part.Content.DecodeTo(fileStream);
}
message.Attachments.Add(new Attachment
{
FileName = fileName,
FullName = new FileInfo(fileName).FullName,
});
}
Console.WriteLine(message);
Теперь разберем по частям, что же там происходит:
using var stream = File.OpenRead("test.eml");
var mimeMessage = MimeMessage.Load(stream);
Для начала создаем экземпляр класса MimeMessage. Это основной класс, вокруг которого будет построена вся дальнейшая работа. Конструктор класса принимает поток по этому можно передать в него поток чтения нашего eml файла.
var message = new Message();
message.Subject = mimeMessage.Subject;
message.From = new User
{
Email = mimeMessage.From.Mailboxes.First().Address,
Name = mimeMessage.From.Mailboxes.First().Name
};
foreach(var to in mimeMessage.To.Mailboxes)
{
message.To.Add(new User
{
Email = to.Address,
Name = to.Name
});
}
Далее мы создаем экземпляр класса Message, описанного ранее, и наполняем его значениями. Если Subject передается как есть, то поле From требует некоторых пояснений. Дело в том, что поля To и From в MimeMessage представляют собой объекты класса InternetAddressList. У письма может быть множество получателей, по которым можно проитерироваться с помощью поля Mailboxes и для каждого получить его имя и email адрес. Мне не доводилось видеть письма, у которых было бы более одного отправителя, по этому вместо итерирования был просто взят первый результат.
foreach (var attachment in mimeMessage.Attachments)
{
var fileName = "attached-message.eml";
if (attachment is MessagePart messagePart)
{
fileName = attachment.ContentDisposition?.FileName;
if (string.IsNullOrEmpty(fileName))
using (var fileStream = File.Create(fileName))
messagePart.Message.WriteTo(stream);
}
else
{
var part = attachment as MimePart;
fileName = part.FileName;
using (var fileStream = File.Create(fileName))
part.Content.DecodeTo(fileStream);
}
message.Attachments.Add(new Attachment
{
FileName = fileName,
FullName = new FileInfo(fileName).FullName,
});
}
Осталось разобраться с вложениями и всем остальным, что по каким-то причинам оказалось в сообщении. Для простого енумерирования по вложениям воспользуемся полем Attachments и грязно побрасаем все вложения прям рядом с eml файлом.
Результат работы программы на моей тестовой eml-ке:
Заключение
На этом на сегодня все. Проект ни в какой мере не претендует на полноценное решение, а заметка на единственно верное мнение. Целью было указать одно из возможных направлений для решения задачи парсинга eml файлов на C#.