Attachments

MapiMessage.Attachments is the raw model: one MapiAttachment per attachment storage or MIME part. GetAttachmentsFlattened() is the adapter for code that wants files.

using XRay.Email.Msg;

var message = MapiMessage.Load(path, token);

var options = new AttachmentFlattenOptions
{
    MaxDepth = 2,                                   // also list attachments inside embedded messages
    EmbeddedFormat = EmbeddedMessageFormat.Auto,    // .msg for an MSG source, .eml for an EML source
    IncludeInline = true,
    IncludeHidden = true,
};

foreach (var attachment in message.GetAttachmentsFlattened(options, token))
{
    Console.WriteLine($"{attachment.Path}: {attachment.Name} {attachment.ContentType} {attachment.Size} bytes");

    if (!attachment.HasContent) continue;           // an OLE storage attachment reads as empty

    await using var file = File.Create(Path.Combine(folder, attachment.Name));
    await attachment.CopyToAsync(file, token);
}

What a MessageAttachment gives you

Member Content
Name A file name that is safe on every platform and unique within the list, case-insensitively (a.png, a (2).png). Capped at 120 characters.
ContentType The declared type, else one sniffed from image bytes, else one derived from the file name. An embedded message is application/vnd.ms-outlook or message/rfc822.
Size, GetSize(token) The size in bytes. For an embedded message, the size of its serialisation.
ContentId The content id without angle brackets, or null.
IsInline, IsHidden Inline when it has a content id that the HTML body references as cid:, or is hidden, or is flagged as an MHTML reference; or when its Content-Location appears in the HTML body. IsHidden is PR_ATTACHMENT_HIDDEN.
IsEmbeddedMessage, EmbeddedMessage Whether it is a message, and that message.
Depth, Path Where it sits when embedded messages are flattened.
Owner, Source The message it belongs to and the underlying MapiAttachment.
OpenRead(), CopyTo(), CopyToAsync() Read it or copy it to a stream.

Embedded messages

An embedded message is named from its subject and serialised on demand, as MSG or as EML. With MaxDepth 0 it is one entry. Each further level also lists the attachments inside embedded messages, depth first, right after the embedded message's own entry, up to a cap of 32 levels. IncludeEmbeddedMessages = false leaves the embedded messages themselves out of the list. The attachments inside them are still listed when MaxDepth reaches them.

Limits

An attachment that is an OLE storage rather than a file (an embedded Office object, for example) reads as empty, and HasContent is false.