jstedfast / MailKit

A cross-platform .NET library for IMAP, POP3, and SMTP.

Home Page:http://www.mimekit.net

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Example how to use GetStreamAsync method for ImapFolder

aldayneko opened this issue · comments

Hello,

Is there any example how to use GetStreamAsync on ImapFolder properly ?
Task<Stream> GetStreamAsync(UniqueId uid, BodyPart part, int offset, int count, CancellationToken cancellationToken = default(CancellationToken), ITransferProgress progress = null);
What I'm trying to achieve with this method is to get attachment as a stream and return this stream as a part of FileStreamResult from web api endpoint.

At this moment there are two unclear things for me:

  1. Method requires count parameter. Is it attachment size ? How to get it correctly from message ?
  2. Is it necessary to call Close method on ImapFolder ? If so, how to do it correctly ? Does Close method dispose all opened streams ? I'm asking because if I simple call Close method my endpoint still continues to read data from stream when returning FileStreamResult

Thanks in advance

The first thing that should be noted is that GetStream/Async() methods do not decode any MIME - they simply get raw message data.

If that's what you want, then great - if you want the (e.g.) base64 decoded content, then you should use GetBodyPartAsync() and then decode the content.

Assuming that you actually want what GetStream/Async() returns, then:

Method requires count parameter. Is it attachment size ? How to get it correctly from message ?

If you want the complete body part stream, then you'd want to use offset = 0 and count = bodyPart.Octets or count = int.MaxValue.

Normally, these GetStream/Async() methods that take int offset and int count arguments are meant for incremental streaming of the MIME content, not grabbing the whole thing.

If you want to grab the whole body part stream, then you want GetStream(UniqueId uid, string section, CancellationToken cancellationToken = default(CancellationToken), ITransferProgress progress = null); where section would be bodyPart.PartSpecifier or some variation of that (which requires reading rfc3501 to learn about TEXT, HEADERS, and MIME section extensions)

For convenience, I'm adding overloads of the GetStream/Async() methods that do not take offset or count arguments which will be available in the next release (in another week or so).

Is it necessary to call Close method on ImapFolder ?

It is not necessary to call the Close() method.

If so, how to do it correctly ?

IMAP only allows 1 folder to be "open" at a time. Whenever you open a folder, any previously opened folders are automatically closed.

There's really no need to manually Close() a folder unless you want to get back to a "no folders are open" state, which, I can't really see much value in that.

Does Close method dispose all opened streams ?

No. You are responsible for disposing any streams returned from methods like GetStream() or GetStreamAsync(). They will typically be MemoryStream (or potentially MimeKit.IO.MemoryBlockStream).

I'm asking because if I simple call Close method my endpoint still continues to read data from stream when returning FileStreamResult

Correct, because you need to dispose that stream and the Close() method will have no effect on any streams you have references to.

Here's an example I am adding to the documentation for the GetStream() method that takes a string section argument:

public static void CacheBodyParts (string baseDirectory)
{
	using (var client = new ImapClient ()) {
		client.Connect ("imap.gmail.com", 993, SecureSocketOptions.SslOnConnect);

		client.Authenticate ("username", "password");

		client.Inbox.Open (FolderAccess.ReadOnly);

		// search for messages where the Subject header contains either "MimeKit" or "MailKit"
		var query = SearchQuery.SubjectContains ("MimeKit").Or (SearchQuery.SubjectContains ("MailKit"));
		var uids = client.Inbox.Search (query);

		// fetch summary information for the search results (we will want the UID and the BODYSTRUCTURE
		// of each message so that we can extract the text body and the attachments)
		var items = client.Inbox.Fetch (uids, MessageSummaryItems.UniqueId | MessageSummaryItems.BodyStructure);

		foreach (var item in items) {
			// determine a directory to save stuff in
			var directory = Path.Combine (baseDirectory, item.UniqueId.ToString ());

			// create the directory
			Directory.CreateDirectory (directory);

			// IMessageSummary.TextBody is a convenience property that finds the 'text/plain' body part for us
			var bodyPart = item.TextBody;

			if (bodyPart != null) {
				// cache the raw 'text/plain' MIME part for later use
				using (var stream = client.Inbox.GetStream (item.UniqueId, bodyPart.PartSpecifier)) {
					var path = path.Combine (directory, bodyPart.PartSpecifier);

					using (var output = File.Create (path))
						stream.CopyTo (output);
				}
			}

			// IMessageSummary.HtmlBody is a convenience property that finds the 'text/html' body part for us
			bodyPart = item.HtmlBody;

			if (bodyPart != null) {
				// cache the raw 'text/html' MIME part for later use
				using (var stream = client.Inbox.GetStream (item.UniqueId, bodyPart.PartSpecifier)) {
					var path = path.Combine (directory, bodyPart.PartSpecifier);

					using (var output = File.Create (path))
						stream.CopyTo (output);
				}
			}

			// now iterate over all of the attachments and save them to disk
			foreach (var attachment in item.Attachments) {
				// cache the raw MIME attachment just like we did with the body
				using (var stream = client.Inbox.GetStream (item.UniqueId, attachment.PartSpecifier)) {
					var path = path.Combine (directory, attachment.PartSpecifier);

					using (var output = File.Create (path))
						stream.CopyTo (output);
				}
			}
		}

		client.Disconnect (true);
	}
}

Thanks a lot for the response
But how to get attachment as a stream if I need decoded content ?
As I can see there is an option to call MimeContent.DecodeTo() method, but it means all data will be buffered into memory stream which is not good from resources management perspective.
Are there any options not to write decoded data to stream but to read decoded data as a stream ?

But how to get attachment as a stream if I need decoded content ?

foreach (var attachment in item.Attachments) {
    // Figure out a filename to use to save the part.
    string fileName = attachment.FileName;

    if (string.IsNullOrEmpty (fileName)) {
        if (!MimeTypes.TryGetExtension (attachment.ContentType.MimeType, out string extension))
            extension = ".dat";

        fileName = Guid.NewGuid ().ToString () + extension;
    }

    // If all we want is the content (rather than the entire MIME part including the headers), then
    // we want the ".TEXT" section of the part.
    using (var content = client.Inbox.GetStream (item.UniqueId, attachment.PartSpecifier + ".TEXT")) {
#if USE_MIME_CONTENT
        ContentEncoding encoding;

        if (string.IsNullOrEmpty (attachment.ContentTransferENcoding) || !MimeUtils.TryParse (attachment.ContentTransferEncoding, out encoding))
            encoding = ContentEncoding.Default;

        using (var mimeContent = new MimeContent (content, encoding)) {
            using (var stream = File.Create (fileName))
                mimeContent.DecodeTo (stream);
        }
#else
        using (var stream = File.Create (fileName)) {
            using (var filtered = new FilteredStream (stream)) {
                if (!string.IsNullOrEmpty (attachment.ContentTransferEncoding)) {
                    var decoder = DecoderFilter.Create (attachment.ContentTransferEncoding);
                    filtered.Add (filter);
                }

                content.CopyTo (filtered);
                filtered.Flush ();
            }
        }
#endif
    }
}

As I can see there is an option to call MimeContent.DecodeTo() method, but it means all data will be buffered into memory stream which is not good from resources management perspective.

MimeContent.DecodeTo() doesn't load everything into memory, it incrementally decodes the stream as it reads it and writes each chunk to the output stream.

In the above example, the difference between the USE_MIME_CONTENT option and the low-level FilteredStream option is just that MimeContent is a lot simpler to deal with.

Are there any options not to write decoded data to stream but to read decoded data as a stream ?

No. MailKit doesn't have the context to know what the content transfer encoding is.