Frequently Asked Questions
This topic contains the following sections:
- Are MimeKit and MailKit completely free? Can I use them in my proprietary product(s)?
- Why do I get "NotSupportedException: No data is available for encoding ######."?
- Why does text show up garbled in my ASP.NET Core / .NET Core / .NET 5+ app?
- Why do I get a TypeLoadException when I try to create a new MimeMessage?
- Why do I get "An error occurred while attempting to establish an SSL or TLS connection." when I try to Connect?
- How do I debug SMTP, POP3 and/or IMAP errors?
- Why doesn't MailKit find some of my GMail POP3 or IMAP messages?
- How do I access GMail using MailKit?
- How can I log in to a GMail account using OAuth 2.0?
- How do I create a message with attachments?
- How do I get the main body of a message?
- How do I tell if a message has attachments?
- Why doesn't the MimeMessage class implement ISerializable so that I can serialize a message to disk and read it back later?
- How do I parse messages?
- How do I save messages?
- How do I save attachments?
- How do I get the email addresses in the From, To, and Cc headers?
- Why do attachments with Unicode filenames appear as "ATT0####.dat" in Outlook?
- How do I decrypt PGP messages that are embedded in the main message text?
- How do I reply to a message using MimeKit?
- How do I forward a message?
- How would I parse multipart/form-data from an HTTP web request?
- How do I get the number of unread messages in a folder?
- How do I get all of the unread messages in a folder?
- How do I search for messages delivered between two dates?
- What does "The ImapClient is currently busy processing a command." mean?
- Why do I get InvalidOperationException: "The folder is not currently open."?
- Why doesn't ImapFolder.MoveTo() move the message out of the source folder?
- How do I mark messages as read using IMAP?
- How do I re-synchronize the cache for an IMAP folder?
- How do I log in to a shared mailbox in Office365?
- Why doesn't the message show up in the "Sent Mail" folder after sending it?
- How do I send email to a SpecifiedPickupDirectory?
- How do I request a notification when the message is read by the user?
- How do I process a read receipt notification?
Are MimeKit and MailKit completely free? Can I use them in my proprietary product(s)?
Yes. MimeKit and MailKit are both completely free and open source. They are both covered under the MIT license.
Why do I get "NotSupportedException: No data is available for encoding ######."?
In .NET Core, Microsoft decided to split out the non-Unicode text encodings into a separate NuGet package called System.Text.Encoding.CodePages.
MimeKit already pulls in a reference to this NuGet package, so you shouldn't need to add a reference to it in your project. That said, you will still need to register the encoding provider. It is recommended that you add the following line of code to your program initialization (e.g. the beginning of your program's Main() method):
System.Text.Encoding.RegisterProvider (System.Text.CodePagesEncodingProvider.Instance);
Why does text show up garbled in my ASP.NET Core / .NET Core / .NET 5+ app?
.NET Core (and ASP.NET Core by extension) and .NET 5 (and later) only provide the Unicode encodings, ASCII and ISO-8859-1 by default. Other text encodings are not available to your application unless your application registers the encoding provider that provides all of the additional encodings.
First, add a package reference for the System.Text.Encoding.CodePages nuget package to your project and then register the additional text encodings using the following code snippet:
System.Text.Encoding.RegisterProvider (System.Text.CodePagesEncodingProvider.Instance);
Note: The above code snippet should be safe to call in .NET Framework versions >= 4.6 as well.
Why do I get a TypeLoadException when I try to create a new MimeMessage?
This only seems to happen in cases where the application is built for .NET Framework (v4.x) and seems to be most common for ASP.NET web applications that were built using Visual Studio 2019 (it is unclear whether this happens with Visual Studio 2022 as well).
The issue is that some (older?) versions of MSBuild do not correctly generate *.dll.config, app.config and/or web.config files with proper assembly version binding redirects.
If this problem is happening to you, make sure to use MimeKit and MailKit >= v4.0 which include MimeKit.dll.config and MailKit.dll.config.
The next step is to manually edit your application's app.config (or web.config) to add a binding redirect for System.Runtime.CompilerServices.Unsafe:
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
Why do I get "An error occurred while attempting to establish an SSL or TLS connection." when I try to Connect?
When you get an exception with that error message, it usually means that you are encountering one of the following scenarios:
-
The mail server does not support SSL on the specified port.
There are 2 different ways to use SSL/TLS encryption with mail servers.
The first way is to enable SSL/TLS encryption immediately upon connecting to the SMTP, POP3 or IMAP server. This method requires an "SSL port" because the standard port defined for the protocol is meant for plain-text communication.
The second way is via a STARTTLS command (aka STLS for POP3) that is optionally supported by the server.
Below is a table of the protocols supported by MailKit and the standard plain-text ports (which either do not support any SSL/TLS encryption at all or only via the STARTTLS command extension) and the SSL ports which require SSL/TLS encryption immediately upon a successful connection to the remote host.
Protocol
Standard Port
SSL Port
SMTP
25 or 587
465
POP3
110
995
IMAP
143
993
It is important to use the correct SecureSocketOptions for the port that you are connecting to.
If you are connecting to one of the standard ports above, you will need to use SecureSocketOptions.None, SecureSocketOptions.StartTls or SecureSocketOptions.StartTlsWhenAvailable.
If you are connecting to one of the SSL ports, you will need to use SecureSocketOptions.SslOnConnect.
You could also try using SecureSocketOptions.Auto which works by choosing the appropriate option to use by comparing the specified port to the ports in the above table.
-
The mail server that you are connecting to is using an expired (or otherwise untrusted) SSL certificate.
Often times, mail servers will use self-signed certificates instead of using a certificate that has been signed by a trusted Certificate Authority. Another potential pitfall is when locally installed anti-virus software replaces the certificate in order to scan web traffic for viruses.
When your system is unable to validate the mail server's certificate because it is not signed by a known and trusted Certificate Authority, the above error will occur.
You can work around this problem by supplying a custom RemoteServerCertificateValidationCallback and setting it on the client's ServerCertificateValidationCallback property.
In the simplest example, you could do something like this (although I would strongly recommend against it in production use):
C#using (var client = new SmtpClient ()) { client.ServerCertificateValidationCallback = (s,c,h,e) => true; client.Connect (hostName, port, SecureSocketOptions.Auto); // ... }A better solution might be to compare the certificate's common name, issuer, serial number, and fingerprint to known values to make sure that the certificate can be trusted. Take the following code snippet as an example of how to do this:
C#bool MyServerCertificateValidationCallback (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { if (sslPolicyErrors == SslPolicyErrors.None) return true; // Note: The following code casts to an X509Certificate2 because it's easier to get the // values for comparison, but it's possible to get them from an X509Certificate as well. if (certificate is X509Certificate2 certificate2) { var cn = certificate2.GetNameInfo (X509NameType.SimpleName, false); var fingerprint = certificate2.Thumbprint; var serial = certificate2.SerialNumber; var issuer = certificate2.Issuer; return cn == "imap.gmail.com" && issuer == "CN=GTS CA 1O1, O=Google Trust Services, C=US" && serial == "00BABE95B167C9ECAF08000000006065B6" && fingerprint == "E79A011EF55EEC72D2B7E391D193761372796836"; } return false; }The downside of the above example is that it requires hard-coding known values for "trusted" mail server certificates which can quickly become unwieldy to deal with if your program is meant to be used with a wide range of mail servers.
The best approach would be to prompt the user with a dialog explaining that the certificate is not trusted for the reasons enumerated by the SslPolicyErrors argument as well as potentially the errors provided in the X509Chain If the user wishes to accept the risks of trusting the certificate, your program could then return true.
For more details on writing a custom SSL certificate validation callback, it may be worth checking out the SslCertificateValidation.cs example.
-
A Certificate Authority CRL server for one or more of the certificates in the chain is temporarily unavailable.
Most Certificate Authorities are probably pretty good at keeping their CRL and/or OCSP servers up 24/7, but occasionally they do go down or are otherwise unreachable due to other network problems between you and the server. When this happens, it becomes impossible to check the revocation status of one or more of the certificates in the chain.
To ignore revocation checks, you can set the CheckCertificateRevocation property of the IMAP, POP3 or SMTP client to false before you connect:
C#using (var client = new SmtpClient ()) { client.CheckCertificateRevocation = false; client.Connect (hostName, port, SecureSocketOptions.Auto); // ... } -
The server does not support the same set of SSL/TLS protocols that the client is configured to use.
MailKit attempts to keep up with the latest security recommendations and so is continuously removing older SSL and TLS protocols that are no longer considered secure from the default configuration. This often means that MailKit's SMTP, POP3 and IMAP clients will fail to connect to servers that are still using older SSL and TLS protocols. Currently, the SSL and TLS protocols that are not supported by default are: SSL v2.0, SSL v3.0, TLS v1.0 and TLS v1.1.
You can override MailKit's default set of supported SSL and TLS protocols by setting the value of the SslProtocols property on your SMTP, POP3 or IMAP client.
For example:
C#using (var client = new SmtpClient ()) { // Allow SSLv3.0 and all versions of TLS client.SslProtocols = SslProtocols.Ssl3 | SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13; client.Connect ("smtp.gmail.com", 465, true); // ... }
How do I debug SMTP, POP3 and/or IMAP errors?
All of MailKit's client implementations have a constructor that takes a nifty IProtocolLogger interface for logging client/server communications. Out of the box, you can use the handy ProtocolLogger class. Here are some examples of how to use it:
// log to a file called 'imap.log'
var client = new ImapClient (new ProtocolLogger ("imap.log"));
// log to standard output (i.e. the console)
var client = new ImapClient (new ProtocolLogger (Console.OpenStandardOutput ()));
Note: When submitting a protocol log as part of a bug report, make sure to scrub any sensitive information including your authentication credentials. This information will generally be the base64 encoded blob immediately following an AUTHENTICATE or AUTH command (depending on the type of server) or the cleartext username and password strings in a LOGIN command. The only exception to this case is if you are authenticating with NTLM in which case I may need this information, but only if the bug/error is in the authentication step.
Why doesn't MailKit find some of my GMail POP3 or IMAP messages?
By default, GMail's POP3 and IMAP server does not behave like standard POP3 or IMAP servers and hides messages from clients using those protocols (as well as having other non-standard behavior).
If you want to configure your GMail POP3 settings to behave the way POP3 is intended to behave, you'll need to log in to your GMail account via your web browser and navigate to the Forwarding and POP/IMAP tab of your GMail Settings page and make the following changes in the POP3 Download section:
-
Enable POP for all mail (even if it has already been downloaded).
-
When messages are accessed with POP, keep GMail's copy in the Inbox.
How do I access GMail using MailKit?
As of September 30th, 2024, authentication using only a username and password is no longer supported by Google.
There are now only 2 options to choose from:
-
Use OAuth 2.0 to authenticate with GMail. This is the recommended approach.
-
Use an App Password to authenticate with GMail. This is a less secure option and is not recommended. It is only available for accounts that have 2-Step Verification enabled.
To use an App password, you will first need to turn on 2-Step Verification. Once 2-Step Verification is turned on, you can generate an App password.
Then, assuming that your GMail account is user@gmail.com, you would use the following code snippet to connect to GMail via IMAP:
using (var client = new ImapClient ()) {
client.Connect ("imap.gmail.com", 993, SecureSocketOptions.SslOnConnect);
client.Authenticate ("user@gmail.com", "app-specific-password");
// do stuff...
client.Disconnect (true);
}
Connecting via POP3 or SMTP is identical except for the host names and ports (and, of course, you'd use a Pop3Client or SmtpClient as appropriate).
How can I log in to a GMail account using OAuth 2.0?
The first thing you need to do is follow Google's instructions for obtaining OAuth 2.0 credentials for your application.
Or, as an alternative set of step-by-step instructions, you can follow the directions that I have written (complete with screenshots).
Once you've done that, the easiest way to obtain an access token is to use Google's Google.Apis.Auth library:
const string GMailAccount = "username@gmail.com";
var clientSecrets = new ClientSecrets {
ClientId = "XXX.apps.googleusercontent.com",
ClientSecret = "XXX"
};
var codeFlow = new GoogleAuthorizationCodeFlow (new GoogleAuthorizationCodeFlow.Initializer {
// Cache tokens in ~/.local/share/google-filedatastore/CredentialCacheFolder on Linux/Mac
DataStore = new FileDataStore ("CredentialCacheFolder", false),
Scopes = new [] { "https://mail.google.com/" },
ClientSecrets = clientSecrets
});
var codeReceiver = new LocalServerCodeReceiver ();
var authCode = new AuthorizationCodeInstalledApp (codeFlow, codeReceiver);
var credential = await authCode.AuthorizeAsync (GMailAccount, CancellationToken.None);
if (authCode.ShouldRequestAuthorizationCode (credential.Token))
await credential.RefreshTokenAsync (CancellationToken.None);
var oauth2 = new SaslMechanismOAuth2 (credential.UserId, credential.Token.AccessToken);
using (var client = new ImapClient ()) {
await client.ConnectAsync ("imap.gmail.com", 993, SecureSocketOptions.SslOnConnect);
await client.AuthenticateAsync (oauth2);
await client.DisconnectAsync (true);
}
How do I create a message with attachments?
To construct a message with attachments, the first thing you'll need to do is create a multipart/mixed container which you'll then want to add the message body to first. Once you've added the body, you can then add MIME parts to it that contain the content of the files you'd like to attach, being sure to set the Content-Disposition header value to attachment. You'll probably also want to set the filename parameter on the Content-Disposition header as well as the name parameter on the Content-Type header. The most convenient way to do this is to use the MimePartFileName property which will set both parameters for you as well as setting the Content-Disposition header value to attachment if it has not already been set to something else.
var message = new MimeMessage ();
message.From.Add (new MailboxAddress ("Joey", "joey@friends.com"));
message.To.Add (new MailboxAddress ("Alice", "alice@wonderland.com"));
message.Subject = "How you doin?";
// create our message text, just like before (except don't set it as the message.Body)
var body = new TextPart ("plain") {
Text = @"Hey Alice,
What are you up to this weekend? Monica is throwing one of her parties on
Saturday and I was hoping you could make it.
Will you be my +1?
-- Joey
"
};
// create an image attachment for the file located at path
var attachment = new MimePart ("image", "gif") {
Content = new MimeContent (File.OpenRead (path), ContentEncoding.Unspecified),
ContentDisposition = new ContentDisposition (ContentDisposition.Attachment),
ContentTransferEncoding = ContentEncoding.Base64,
FileName = Path.GetFileName (path)
};
// now create the multipart/mixed container to hold the message text and the
// image attachment
var multipart = new Multipart ("mixed");
multipart.Add (body);
multipart.Add (attachment);
// now set the multipart/mixed as the message body
message.Body = multipart;
A simpler way to construct messages with attachments is to take advantage of the BodyBuilder class.
var message = new MimeMessage ();
message.From.Add (new MailboxAddress ("Joey", "joey@friends.com"));
message.To.Add (new MailboxAddress ("Alice", "alice@wonderland.com"));
message.Subject = "How you doin?";
var builder = new BodyBuilder ();
// Set the plain-text version of the message text
builder.TextBody = @"Hey Alice,
What are you up to this weekend? Monica is throwing one of her parties on
Saturday and I was hoping you could make it.
Will you be my +1?
-- Joey
";
// We may also want to attach a calendar event for Monica's party...
builder.Attachments.Add (@"C:\Users\Joey\Documents\party.ics");
// Now we just need to set the message body and we're done
message.Body = builder.ToMessageBody ();
For more information, see Creating Messages.
How do I get the main body of a message?
(Note: for the TL;DR version, skip to the end)
MIME is a tree structure of parts. There are multiparts which contain other parts (even other multiparts). There are message parts which contain messages. And finally, there are leaf-node parts which contain content.
There are a few common message structures:
-
The message contains only a text/plain or text/html part (easy, just use that).
-
The message contains a multipart/alternative which will typically look a bit like this:
multipart/alternative text/plain text/html
-
Same as above, but the html part is inside a multipart/related so that it can embed images:
multipart/alternative text/plain multipart/related text/html image/jpeg image/png -
The message contains a textual body part as well as some attachments:
multipart/mixed text/plain application/octet-stream application/zip
-
The same as above, but with the first part replaced with either #2 or #3 To illustrate:
multipart/mixed multipart/alternative text/plain text/html application/octet-stream application/zipOr:
multipart/mixed multipart/alternative text/plain multipart/related text/html image/jpeg image/png application/octet-stream application/zip
For your convenience, the MimeMessage class has 2 properties that you may find useful: TextBody and HtmlBody.
For more information, see Working with messages.
How do I tell if a message has attachments?
In most cases, a message with a body that has a MIME-type of multipart/mixed containing more than a single part probably has attachments. As illustrated above, the first part of a multipart/mixed is typically the textual body of the message, but it is not always quite that simple.
In general, MIME attachments will have a Content-Disposition header with a value of attachment. To get the list of body parts matching this criteria, you can use the MimeMessageAttachments property.
Unfortunately, not all mail clients follow this convention and so you may need to write your own custom logic. For example, you may wish to treat all body parts having a name or filename parameter set on them:
var attachments = message.BodyParts.OfType<MimePart> ().Where (part => !string.IsNullOrEmpty (part.FileName));
A more sophisticated approach is to treat body parts not referenced by the main textual body part of the message as attachments. In other words, treat any body part not used for rendering the message as an attachment. For an example on how to do this, consider the following code snippets:
/// <summary>
/// Visits a MimeMessage and generates HTML suitable to be rendered by a browser control.
/// </summary>
class HtmlPreviewVisitor : MimeVisitor
{
List<MultipartRelated> stack = new List<MultipartRelated> ();
List<MimeEntity> attachments = new List<MimeEntity> ();
string body;
/// <summary>
/// Creates a new HtmlPreviewVisitor.
/// </summary>
public HtmlPreviewVisitor ()
{
}
/// <summary>
/// The list of attachments that were in the MimeMessage.
/// </summary>
public IList<MimeEntity> Attachments {
get { return attachments; }
}
/// <summary>
/// The HTML string that can be set on the BrowserControl.
/// </summary>
public string HtmlBody {
get { return body ?? string.Empty; }
}
protected override void VisitMultipartAlternative (MultipartAlternative alternative)
{
// walk the multipart/alternative children backwards from greatest level of faithfulness to the least faithful
for (int i = alternative.Count - 1; i >= 0 && body == null; i--)
alternative[i].Accept (this);
}
protected override void VisitMultipartRelated (MultipartRelated related)
{
var root = related.Root;
// push this multipart/related onto our stack
stack.Add (related);
// visit the root document
root.Accept (this);
// pop this multipart/related off our stack
stack.RemoveAt (stack.Count - 1);
}
// look up the image based on the img src url within our multipart/related stack
bool TryGetImage (string url, out MimePart image)
{
UriKind kind;
int index;
Uri uri;
if (Uri.IsWellFormedUriString (url, UriKind.Absolute))
kind = UriKind.Absolute;
else if (Uri.IsWellFormedUriString (url, UriKind.Relative))
kind = UriKind.Relative;
else
kind = UriKind.RelativeOrAbsolute;
try {
uri = new Uri (url, kind);
} catch {
image = null;
return false;
}
for (int i = stack.Count - 1; i >= 0; i--) {
if ((index = stack[i].IndexOf (uri)) == -1)
continue;
image = stack[i][index] as MimePart;
return image != null;
}
image = null;
return false;
}
/// <summary>
/// Get a data: URI for the image attachment.
/// </summary>
/// <remarks>
/// Encodes the image attachment into a string suitable for setting as a src= attribute value in
/// an img tag.
/// </remarks>
/// <returns>The data: URI.</returns>
/// <param name="image">The image attachment.</param>
string GetDataUri (MimePart image)
{
using (var memory = new MemoryStream ()) {
image.Content.DecodeTo (memory);
var buffer = memory.GetBuffer ();
var length = (int) memory.Length;
var base64 = Convert.ToBase64String (buffer, 0, length);
return string.Format ("data:{0};base64,{1}", image.ContentType.MimeType, base64);
}
}
// Replaces <img src=...> urls that refer to images embedded within the message with
// "file://" urls that the browser control will actually be able to load.
void HtmlTagCallback (HtmlTagContext ctx, HtmlWriter htmlWriter)
{
if (ctx.TagId == HtmlTagId.Meta && !ctx.IsEndTag) {
bool isContentType = false;
ctx.WriteTag (htmlWriter, false);
// replace charsets with "utf-8" since our output will be in utf-8 (and not whatever the original charset was)
foreach (var attribute in ctx.Attributes) {
if (attribute.Id == HtmlAttributeId.Charset) {
htmlWriter.WriteAttributeName (attribute.Name);
htmlWriter.WriteAttributeValue ("utf-8");
} else if (isContentType && attribute.Id == HtmlAttributeId.Content) {
htmlWriter.WriteAttributeName (attribute.Name);
htmlWriter.WriteAttributeValue ("text/html; charset=utf-8");
} else {
if (attribute.Id == HtmlAttributeId.HttpEquiv && attribute.Value != null
&& attribute.Value.Equals ("Content-Type", StringComparison.OrdinalIgnoreCase))
isContentType = true;
htmlWriter.WriteAttribute (attribute);
}
}
} else if (ctx.TagId == HtmlTagId.Image && !ctx.IsEndTag && stack.Count > 0) {
ctx.WriteTag (htmlWriter, false);
// replace the src attribute with a "data:" URL
foreach (var attribute in ctx.Attributes) {
if (attribute.Id == HtmlAttributeId.Src) {
if (!TryGetImage (attribute.Value, out var image)) {
htmlWriter.WriteAttribute (attribute);
continue;
}
var dataUri = GetDataUri (image);
htmlWriter.WriteAttributeName (attribute.Name);
htmlWriter.WriteAttributeValue (dataUri);
} else {
htmlWriter.WriteAttribute (attribute);
}
}
} else {
// pass the tag through to the output
ctx.WriteTag (htmlWriter, true);
}
}
protected override void VisitTextPart (TextPart entity)
{
TextConverter converter;
if (body != null) {
// since we've already found the body, treat this as an attachment
attachments.Add (entity);
return;
}
if (entity.IsHtml) {
converter = new HtmlToHtml {
HtmlTagCallback = HtmlTagCallback
};
} else if (entity.IsFlowed) {
var flowed = new FlowedToHtml ();
string delsp;
if (entity.ContentType.Parameters.TryGetValue ("delsp", out delsp))
flowed.DeleteSpace = delsp.Equals ("yes", StringComparison.OrdinalIgnoreCase);
converter = flowed;
} else {
converter = new TextToHtml ();
}
body = converter.Convert (entity.Text);
}
protected override void VisitTnefPart (TnefPart entity)
{
// extract any attachments in the MS-TNEF part
using (var tnef = entity.LoadTnefMessage ()) {
// the converted message (and its attachments) outlives the TnefMessage
var result = tnef.ConvertToMime ();
attachments.AddRange (result.Message.Attachments);
}
}
protected override void VisitMessagePart (MessagePart entity)
{
// treat message/rfc822 parts as attachments
attachments.Add (entity);
}
protected override void VisitMimePart (MimePart entity)
{
// realistically, if we've gotten this far, then we can treat this as an attachment
// even if the IsAttachment property is false.
attachments.Add (entity);
}
}
And the way you'd use this visitor might look something like this:
void Render (MimeMessage message)
{
var tmpDir = Path.Combine (Path.GetTempPath (), message.MessageId);
var visitor = new HtmlPreviewVisitor (tmpDir);
Directory.CreateDirectory (tmpDir);
message.Accept (visitor);
DisplayHtml (visitor.HtmlBody);
DisplayAttachments (visitor.Attachments);
}
Once you've rendered the message using the above technique, you'll have a list of attachments that were not used, even if they did not match the simplistic criteria used by the MimeMessageAttachments property.
Why doesn't the MimeMessage class implement ISerializable so that I can serialize a message to disk and read it back later?
The MimeKit API was designed to use the existing MIME format for serialization. In light of this, the ability to use the .NET serialization API and format did not make much sense to support.
You can easily serialize a MimeMessage to a stream using the WriteTo methods.
How do I parse messages?
One of the more common operations that MimeKit is meant for is parsing email messages from arbitrary streams. There are two ways of accomplishing this task.
The first way is to use one of the Load methods:
// Load a MimeMessage from a stream
var message = MimeMessage.Load (stream);
Or you can load a message from a file path:
// Load a MimeMessage from a file path
var message = MimeMessage.Load ("message.eml");
The second way is to use the MimeParser class. For the most part, using the MimeParser directly is not necessary unless you wish to parse a Unix mbox file stream. However, this is how you would do it:
// Load a MimeMessage from a stream
var parser = new MimeParser (stream, MimeFormat.Entity);
var message = parser.ParseMessage ();
For Unix mbox file streams, you would use the parser like this:
// Load every message from a Unix mbox
var parser = new MimeParser (stream, MimeFormat.Mbox);
while (!parser.IsEndOfStream) {
var message = parser.ParseMessage ();
// do something with the message
}
For more details, see Parsing messages.
How do I save messages?
One you've got a MimeMessage, you can save it to a file using the WriteTo method:
message.WriteTo ("message.eml");
The WriteTo method also has overloads that allow you to write the message to a Stream instead.
By default, the WriteTo method will save the message using DOS line-endings on Windows and Unix line-endings on Unix-based systems such as macOS and Linux. You can override this behavior by passing a FormatOptions argument to the method:
// clone the default formatting options
var format = FormatOptions.Default.Clone ();
// override the line-endings to be DOS no matter what platform we are on
format.NewLineFormat = NewLineFormat.Dos;
message.WriteTo (format, "message.eml");
How do I save attachments?
If you've already got a MimePart that represents the attachment that you'd like to save, here's how you might save it:
using (var stream = File.Create (fileName))
attachment.Content.DecodeTo (stream);
Pretty simple, right?
But what if your attachment is actually a MessagePart?
To save the content of a message/rfc822 part, you'd use the following code snippet:
using (var stream = File.Create (fileName))
attachment.Message.WriteTo (stream);
If you are iterating over all of the attachments in a message, you might do something like this:
foreach (var attachment in message.Attachments) {
if (attachment is MessagePart) {
var fileName = attachment.ContentDisposition?.FileName ?? attachment.ContentType.Name;
var rfc822 = (MessagePart) attachment;
if (string.IsNullOrEmpty (fileName))
fileName = "attached-message.eml";
// make sure that the filename value does not contain a full path or invalid path characters
fileName = Path.GetFileName (fileName);
using (var stream = File.Create (fileName))
rfc822.Message.WriteTo (stream);
} else {
var part = (MimePart) attachment;
var fileName = part.FileName;
if (string.IsNullOrEmpty (fileName))
fileName = "untitled.dat";
// make sure that the filename value does not contain a full path or invalid path characters
fileName = Path.GetFileName (fileName);
using (var stream = File.Create (fileName))
part.Content.DecodeTo (stream);
}
}
How do I get the email addresses in the From, To, and Cc headers?
The From, To, and Cc properties of a MimeMessage are all of type InternetAddressList. An InternetAddressList is a list of InternetAddress items. This is where some people start to get lost, conceptually, because InternetAddress is an abstract class that only really has a Name property.
As you've probably already discovered, the Name property contains the name of the person (if available), but what you probably want is his or her email address, not their name. So how do you get it?
To get the email address, you'll need to figure out what subclass of address each InternetAddress really is. There are 2 direct subclasses of InternetAddress. They are: GroupAddress and MailboxAddress.
A GroupAddress is a named group of more InternetAddress items that are contained within the Members property. To get an idea of what a group address represents, consider the following examples:
To: My Friends: Joey <joey@friends.com>, Monica <monica@friends.com>, "Mrs. Chanandler Bong" <chandler@friends.com>, Ross <ross@friends.com>, Rachel <rachel@friends.com>;
In the above example, the To header's InternetAddressList will contain only 1 item which will be a GroupAddress with a Name value of My Friends. The Members property of the GroupAddress will contain 5 more InternetAddress items (which will all be instances of MailboxAddress).
The above example, however, is not very likely to ever be seen in messages you deal with. A far more common example would be the one below:
To: undisclosed-recipients:;
Most of the time, the From, To, and Cc headers will only contain mailbox addresses. As you will notice, a MailboxAddress has an Address property which will contain the email address of the mailbox. In the following example, the Address property will contain the value john@smith.com:
To: John Smith <john@smith.com>
If you only care about getting a flattened list of the mailbox addresses in one of the address headers, you can do something like this:
foreach (var mailbox in message.To.Mailboxes)
Console.WriteLine ("{0}'s email address is {1}", mailbox.Name, mailbox.Address);
Why do attachments with Unicode filenames appear as "ATT0####.dat" in Outlook?
An attachment filename is stored as a MIME parameter on the Content-Disposition header. Unfortunately, the original MIME specifications did not specify a method for encoding non-ascii filenames. In 1997, rfc2184 (later updated by rfc2231) was published which specified an encoding mechanism to use for encoding them. Since there was a window in time where the MIME specifications did not define a way to encode them, some mail client developers decided to use the mechanism described by rfc2047 which was meant for encoding non-ASCII text in headers. While this may at first seem logical, the problem with this approach was that rfc2047 encoded-word tokens are not allowed to be in quotes (as well as some other issues) and so another, more appropriate, encoding mechanism was needed.
Outlook is one of those mail clients which decided to encode filenames using the mechanism described in rfc2047 and until Outlook 2007, did not support filenames encoded using the mechanism defined in rfc2231.
As of MimeKit v1.2.18, it is possible to configure MimeKit to use the rfc2047 encoding mechanism for filenames in the following two ways:
The first way is to set the encoding method on each individual Parameter:
Parameter param;
if (attachment.ContentDisposition.Parameters.TryGetValue ("filename", out param))
param.EncodingMethod = ParameterEncodingMethod.Rfc2047;
The other way is to use a FormatOptions:
var options = FormatOptions.Default.Clone ();
options.ParameterEncodingMethod = ParameterEncodingMethod.Rfc2047;
message.WriteTo (options, stream);
How do I decrypt PGP messages that are embedded in the main message text?
Some PGP-enabled mail clients, such as Thunderbird, embed encrypted PGP blurbs within the text/plain body of the message rather than using the PGP/MIME format that MimeKit prefers.
These messages often look something like this:
Return-Path: <pgp-enthusiast@example.com>
Received: from [127.0.0.1] (hostname.example.com. [201.95.8.17])
by mx.google.com with ESMTPSA id l67sm26628445yha.8.2014.04.27.13.49.44
for <pgp-enthusiast@example.com>
(version=TLSv1 cipher=ECDHE-RSA-RC4-SHA bits=128/128);
Sun, 27 Apr 2014 13:49:44 -0700 (PDT)
Message-ID: <535D6D67.8020803@example.com>
Date: Sun, 27 Apr 2014 17:49:43 -0300
From: Die-Hard PGP Fan <pgp-enthusiast@example.com>
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:24.0) Gecko/20100101 Thunderbird/24.4.0
MIME-Version: 1.0
To: undisclosed-recipients:;
Subject: Test of inline encrypted PGP blocks
X-Enigmail-Version: 1.6
Content-Type: text/plain; charset=ISO-8859-1
Content-Transfer-Encoding: 8bit
X-Antivirus: avast! (VPS 140427-1, 27/04/2014), Outbound message
X-Antivirus-Status: Clean
-----BEGIN PGP MESSAGE-----
Charset: ISO-8859-1
Version: GnuPG v2.0.22 (MingW32)
Comment: Using GnuPG with Thunderbird - http://www.enigmail.net/
SGFoISBJIGZvb2xlZCB5b3UsIHRoaXMgdGV4dCBpc24ndCBhY3R1YWxseSBlbmNy
eXB0ZWQgd2l0aCBQR1AsCml0J3MgYWN0dWFsbHkgb25seSBiYXNlNjQgZW5jb2Rl
ZCEKCkknbSBqdXN0IHVzaW5nIHRoaXMgYXMgYW4gZXhhbXBsZSwgdGhvdWdoLCBz
byBpdCBkb2Vzbid0IHJlYWxseSBtYXR0ZXIuCgpGb3IgdGhlIHNha2Ugb2YgYXJn
dW1lbnQsIHdlJ2xsIHByZXRlbmQgdGhhdCB0aGlzIGlzIGFjdHVhbGx5IGFuIGVu
Y3J5cHRlZApibHVyYi4gTW1ta2F5PyBUaGFua3MuCg==
-----END PGP MESSAGE-----
To deal with these kinds of messages, MimeKit's OpenPgpContext includes a DecryptTo(Stream, Stream, CancellationToken) method which can be used to get the raw decrypted stream.
The method variant that has a DigitalSignatureCollection output parameter is useful in cases where the encrypted PGP blurb is also digitally signed, allowing you to get your hands on the list of digital signatures in order for you to verify each of them.
To decrypt the sample message above, you could use the following code snippet:
static Stream Decrypt (MimeMessage message)
{
var text = message.TextBody;
using (var encrypted = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) {
using (var ctx = new MyGnuPGContext ()) {
var decrypted = new MemoryStream ();
ctx.DecryptTo (encrypted, decrypted);
decrypted.Position = 0;
return decrypted;
}
}
}
How do I reply to a message using MimeKit?
Replying to a message is fairly simple. For the most part, you'd just create the reply message the same way you'd create any other message. There are only a few slight differences:
-
In the reply message, you'll want to prefix the Subject header with "Re: " if the prefix doesn't already exist in the message you are replying to (in other words, if you are replying to a message with a Subject of "Re: party tomorrow night!", you would not prefix it with another "Re: ").
-
You will want to set the reply message's In-Reply-To header to the value of the Message-Id header in the original message.
-
You will want to copy the original message's References header into the reply message's References header and then append the original message's Message-Id header.
-
You will probably want to "quote" the original message's text in the reply.
If this logic were to be expressed in code, it might look something like this:
public static MimeMessage Reply (MimeMessage message, MailboxAddress from, bool replyToAll)
{
var reply = new MimeMessage ();
reply.From.Add (from);
// reply to the sender of the message
if (message.ReplyTo.Count > 0) {
reply.To.AddRange (message.ReplyTo);
} else if (message.From.Count > 0) {
reply.To.AddRange (message.From);
} else if (message.Sender != null) {
reply.To.Add (message.Sender);
}
if (replyToAll) {
// include all of the other original recipients (removing ourselves from the list)
reply.To.AddRange (message.To.Mailboxes.Where (x => x.Address != from.Address));
reply.Cc.AddRange (message.Cc.Mailboxes.Where (x => x.Address != from.Address));
}
// set the reply subject
if (!message.Subject.StartsWith ("Re:", StringComparison.OrdinalIgnoreCase))
reply.Subject = "Re: " + message.Subject;
else
reply.Subject = message.Subject;
// construct the In-Reply-To and References headers
if (!string.IsNullOrEmpty (message.MessageId)) {
reply.InReplyTo = message.MessageId;
foreach (var id in message.References)
reply.References.Add (id);
reply.References.Add (message.MessageId);
}
// quote the original message text
using (var quoted = new StringWriter ()) {
var sender = message.Sender ?? message.From.Mailboxes.FirstOrDefault ();
var name = sender != null ? (!string.IsNullOrEmpty (sender.Name) ? sender.Name : sender.Address) : "someone";
quoted.WriteLine ("On {0}, {1} wrote:", message.Date.ToString ("f"), name);
using (var reader = new StringReader (message.TextBody)) {
string line;
while ((line = reader.ReadLine ()) != null) {
quoted.Write ("> ");
quoted.WriteLine (line);
}
}
reply.Body = new TextPart ("plain") {
Text = quoted.ToString ()
};
}
return reply;
}
But what if you wanted to reply to a message and quote the HTML formatting of the original message body (assuming it has an HTML body) while still including the embedded images?
This gets a bit more complicated, but it's still doable...
The first thing we'd need to do is implement our own MimeVisitor to handle this:
public class ReplyVisitor : MimeVisitor
{
readonly Stack<Multipart> stack = new Stack<Multipart> ();
MimeMessage message;
MimeEntity body;
/// <summary>
/// Creates a new ReplyVisitor.
/// </summary>
public ReplyVisitor ()
{
}
/// <summary>
/// Gets the reply.
/// </summary>
/// <value>The reply.</value>
public MimeEntity Body {
get { return body; }
}
void Push (MimeEntity entity)
{
var multipart = entity as Multipart;
if (body == null) {
body = entity;
} else {
var parent = stack.Peek ();
parent.Add (entity);
}
if (multipart != null)
stack.Push (multipart);
}
void Pop ()
{
stack.Pop ();
}
public static string GetOnDateSenderWrote (MimeMessage message)
{
var sender = message.Sender != null ? message.Sender : message.From.Mailboxes.FirstOrDefault ();
var name = sender != null ? (!string.IsNullOrEmpty (sender.Name) ? sender.Name : sender.Address) : "someone";
return string.Format ("On {0}, {1} wrote:", message.Date.ToString ("f"), name);
}
/// <summary>
/// Visit the specified message.
/// </summary>
/// <param name="message">The message.</param>
public override void Visit (MimeMessage message)
{
this.message = message;
stack.Clear ();
base.Visit (message);
}
protected override void VisitMultipartAlternative (MultipartAlternative alternative)
{
var multipart = new MultipartAlternative ();
Push (multipart);
for (int i = 0; i < alternative.Count; i++)
alternative[i].Accept (this);
Pop ();
}
protected override void VisitMultipartRelated (MultipartRelated related)
{
var multipart = new MultipartRelated ();
var root = related.Root;
Push (multipart);
root.Accept (this);
for (int i = 0; i < related.Count; i++) {
if (related[i] != root)
related[i].Accept (this);
}
Pop ();
}
protected override void VisitMultipart (Multipart multipart)
{
foreach (var part in multipart) {
if (part is MultipartAlternative)
part.Accept (this);
else if (part is MultipartRelated)
part.Accept (this);
else if (part is TextPart)
part.Accept (this);
}
}
void HtmlTagCallback (HtmlTagContext ctx, HtmlWriter htmlWriter)
{
if (ctx.TagId == HtmlTagId.Body && !ctx.IsEmptyElementTag) {
if (ctx.IsEndTag) {
// end our opening <blockquote>
htmlWriter.WriteEndTag (HtmlTagId.BlockQuote);
// pass the </body> tag through to the output
ctx.WriteTag (htmlWriter, true);
} else {
// pass the <body> tag through to the output
ctx.WriteTag (htmlWriter, true);
// prepend the HTML reply with "On {DATE}, {SENDER} wrote:"
htmlWriter.WriteStartTag (HtmlTagId.P);
htmlWriter.WriteText (GetOnDateSenderWrote (message));
htmlWriter.WriteEndTag (HtmlTagId.P);
// Wrap the original content in a <blockquote>
htmlWriter.WriteStartTag (HtmlTagId.BlockQuote);
htmlWriter.WriteAttribute (HtmlAttributeId.Style, "border-left: 1px #ccc solid; margin: 0 0 0 .8ex; padding-left: 1ex;");
ctx.InvokeCallbackForEndTag = true;
}
} else {
// pass the tag through to the output
ctx.WriteTag (htmlWriter, true);
}
}
string QuoteText (string text)
{
using (var quoted = new StringWriter ()) {
quoted.WriteLine (GetOnDateSenderWrote (message));
using (var reader = new StringReader (text)) {
string line;
while ((line = reader.ReadLine ()) != null) {
quoted.Write ("> ");
quoted.WriteLine (line);
}
}
return quoted.ToString ();
}
}
protected override void VisitTextPart (TextPart entity)
{
string text;
if (entity.IsHtml) {
var converter = new HtmlToHtml {
HtmlTagCallback = HtmlTagCallback
};
text = converter.Convert (entity.Text);
} else if (entity.IsFlowed) {
var converter = new FlowedToText ();
text = converter.Convert (entity.Text);
text = QuoteText (text);
} else {
// quote the original message text
text = QuoteText (entity.Text);
}
var part = new TextPart (entity.ContentType.MediaSubtype.ToLowerInvariant ()) {
Text = text
};
Push (part);
}
protected override void VisitMessagePart (MessagePart entity)
{
// don't descend into message/rfc822 parts
}
}
public static MimeMessage Reply (MimeMessage message, MailboxAddress from, bool replyToAll)
{
var visitor = new ReplyVisitor ();
var reply = new MimeMessage ();
reply.From.Add (from);
// reply to the sender of the message
if (message.ReplyTo.Count > 0) {
reply.To.AddRange (message.ReplyTo);
} else if (message.From.Count > 0) {
reply.To.AddRange (message.From);
} else if (message.Sender != null) {
reply.To.Add (message.Sender);
}
if (replyToAll) {
// include all of the other original recipients (removing ourselves from the list)
reply.To.AddRange (message.To.Mailboxes.Where (x => x.Address != from.Address));
reply.Cc.AddRange (message.Cc.Mailboxes.Where (x => x.Address != from.Address));
}
// set the reply subject
if (!message.Subject.StartsWith ("Re:", StringComparison.OrdinalIgnoreCase))
reply.Subject = "Re: " + message.Subject;
else
reply.Subject = message.Subject;
// construct the In-Reply-To and References headers
if (!string.IsNullOrEmpty (message.MessageId)) {
reply.InReplyTo = message.MessageId;
foreach (var id in message.References)
reply.References.Add (id);
reply.References.Add (message.MessageId);
}
visitor.Visit (message);
reply.Body = visitor.Body ?? new TextPart ("plain") { Text = ReplyVisitor.GetOnDateSenderWrote (message) + Environment.NewLine };
return reply;
}
How do I forward a message?
There are 2 common ways of forwarding a message: attaching the original message as an attachment and inlining the message body much like replying typically does. Which method you choose is up to you.
To forward a message by attaching it as an attachment, you would do do something like this:
public static MimeMessage Forward (MimeMessage original, MailboxAddress from, IEnumerable<InternetAddress> to)
{
var message = new MimeMessage ();
message.From.Add (from);
message.To.AddRange (to);
// set the forwarded subject
if (!original.Subject?.StartsWith ("FW:", StringComparison.OrdinalIgnoreCase))
message.Subject = "FW: " + (original.Subject ?? string.Empty);
else
message.Subject = original.Subject;
// create the main textual body of the message
var text = new TextPart ("plain") { Text = "Here's the forwarded message:" };
// create the message/rfc822 attachment for the original message
var rfc822 = new MessagePart { Message = original };
// create a multipart/mixed container for the text body and the forwarded message
var multipart = new Multipart ("mixed");
multipart.Add (text);
multipart.Add (rfc822);
// set the multipart as the body of the message
message.Body = multipart;
return message;
}
To forward a message by inlining the original message's text content, you can do something like this:
public static MimeMessage Forward (MimeMessage original, MailboxAddress from, IEnumerable<InternetAddress> to)
{
var message = new MimeMessage ();
message.From.Add (from);
message.To.AddRange (to);
// set the forwarded subject
if (!original.Subject?.StartsWith ("FW:", StringComparison.OrdinalIgnoreCase))
message.Subject = "FW: " + (original.Subject ?? string.Empty);
else
message.Subject = original.Subject;
// quote the original message text
using (var text = new StringWriter ()) {
text.WriteLine ();
text.WriteLine ("-----Original Message-----");
test.WriteLine ("From: {0}", original.From);
text.WriteLine ("Sent: {0}", DateUtils.FormatDate (original.Date));
text.WriteLine ("To: {0}", original.To);
text.WriteLine ("Subject: {0}", original.Subject ?? string.Empty);
text.WriteLine ();
text.Write (original.TextBody);
message.Body = new TextPart ("plain") {
Text = text.ToString ()
};
}
return message;
}
How would I parse multipart/form-data from an HTTP web request?
Since classes like HttpWebRequest take care of parsing the HTTP headers (which includes the Content-Type header) and only offer a content stream to consume, MimeKit provides a way to deal with this using the following two static methods: MimeEntityLoad(ParserOptions, ContentType, Stream, CancellationToken) and MimeEntityLoad(ContentType, Stream, CancellationToken)
Here's how you might use these methods:
MimeEntity ParseMultipartFormData (HttpWebResponse response)
{
var contentType = ContentType.Parse (response.ContentType);
return MimeEntity.Load (contentType, response.GetResponseStream ());
}
If the multipart/form-data HTTP response is expected to be large and you do not wish for the content to be read into memory, you can use the following approach:
MimeEntity ParseMultipartFormData (HttpWebResponse response, Stream stream)
{
// The caller supplies an empty, readable, writable, seekable temporary-file stream
// and keeps it open and unchanged until the returned entity is no longer needed.
// create a header for the multipart/form-data MIME entity based on the Content-Type value of the HTTP
// response
var header = Encoding.UTF8.GetBytes (string.Format ("Content-Type: {0}\r\n\r\n", response.ContentType));
// write the header to the stream
stream.Write (header, 0, header.Length);
// copy the content of the HTTP response to our temporary stream
response.GetResponseStream ().CopyTo (stream);
// reset the stream back to the beginning
stream.Position = 0;
// reference the source stream instead of loading the content into memory
return MimeEntity.Load (stream, storage: MimeContentStorage.Source);
}
How do I get the number of unread messages in a folder?
If the folder is open (via Open), then the MailFolderUnread property will be kept up to date (at least as-of the latest command issued to the server).
If the folder isn't open, then you will need to query the unread state of the folder using the ImapFolderStatus(StatusItems, CancellationToken) method with the appropriate StatusItems flag(s).
For example, to get the total and unread counts, you can do this:
folder.Status (StatusItems.Count | StatusItems.Unread);
int total = folder.Count;
int unread = folder.Unread;
How do I get all of the unread messages in a folder?
The easiest way is to search for all of the messages that do not have the \Seen flag on them, like so:
foreach (var uid in folder.Search (SearchQuery.NotSeen)) {
var message = folder.GetMessage (uid);
}
How do I search for messages delivered between two dates?
The obvious solution is:
var query = SearchQuery.DeliveredAfter (dateRange.BeginDate)
.And (SearchQuery.DeliveredBefore (dateRange.EndDate));
var results = folder.Search (query);
However, it has been reported that this doesn't work reliably depending on the IMAP server implementation.
If you find that this query doesn't get the expected results for your IMAP server, here's another solution that should always work:
var query = SearchQuery.Not (SearchQuery.DeliveredBefore (dateRange.BeginDate)
.Or (SearchQuery.DeliveredAfter (dateRange.EndDate)));
var results = folder.Search (query);
What does "The ImapClient is currently busy processing a command." mean?
If you get an InvalidOperationException with the message, "The ImapClient is currently busy processing a command.", it means that you are trying to use the ImapClient and/or one of its ImapFolders from multiple threads.
To avoid this situation, you'll need to lock the ImapClientSyncRoot property when performing operations on the ImapClient or any of its folders.
For example:
lock (client.SyncRoot) {
client.NoOp ();
}
Why do I get InvalidOperationException: "The folder is not currently open."?
If you get this exception, it's probably because you thought you had to open the destination folder that you passed as an argument to one of the CopyTo or MoveTo methods. When you opened that destination folder, you also inadvertently closed the source folder which is why you are getting this exception.
The IMAP server can only have a single folder open at a time. Whenever you open a folder, you automatically close the previously opened folder.
When copying or moving messages from one folder to another, you only need to have the source folder open.
Why doesn't ImapFolder.MoveTo() move the message out of the source folder?
If you look at the source code for the ImapFolder.MoveTo() method, what you'll notice is that there are several code paths depending on the features that the IMAP server supports.
If the IMAP server supports the MOVE extension, then MailKit's MoveTo() method will use the MOVE command. If you are seeing this problem, your server most likely does not support the MOVE command.
When the IMAP server does not support the MOVE command, MailKit has to use the COPY command to copy the message(s) to the destination folder. Once the COPY command has completed, it will then mark the messages that you asked it to move for deletion by setting the \Deleted flag on those messages.
If the server supports the UIDPLUS extension, then MailKit will attempt to EXPUNGE the subset of messages that it just marked for deletion, however, if the UIDPLUS extension is not supported by the IMAP server, then it cannot safely expunge just that subset of messages and so it stops there.
Chances are that your server supports neither MOVE nor UIDPLUS and that is why clients like Outlook continue to see the messages in your folder. Outlook does, however, have a setting to show deleted messages with a strikeout (which you have probably disabled).
So to answer your question more succinctly: After calling folder.MoveTo (...);, if you are confident that the messages marked for deletion should be expunged, call folder.Expunge ();
How do I mark messages as read using IMAP?
The way to mark messages as read using the IMAP protocol is to set the \Seen flag on the message(s).
To do this using MailKit, you will first need to know either the index(es) or the UID(s) of the messages that you would like to set the \Seen flag on. Once you have that information, you will want to call one of the AddFlags methods on the folder. For example:
folder.AddFlags (uids, MessageFlags.Seen, true);
To mark messages as unread, you would remove the \Seen flag using one of the RemoveFlags methods, like so:
folder.RemoveFlags (uids, MessageFlags.Seen, true);
How do I re-synchronize the cache for an IMAP folder?
Assuming your IMAP server does not support the QRESYNC extension (which simplifies this procedure a ton), here is some simple code to illustrate how to go about re-synchronizing your cache with the remote IMAP server.
/// <summary>
/// Just a simple class to represent the cached information about a message.
/// </summary>
class CachedMessageInfo
{
public UniqueId UniqueId;
public MessageFlags Flags;
public IEnumerable<string> Keywords;
public Envelope Envelope;
public BodyPart Body;
}
/// <summary>
/// Resynchronize the cache with the remote IMAP folder.
/// </summary>
/// <param name="folder">The IMAP folder.</param>
/// <param name="cache">The local cache of message metadata.</param>
/// <param name="cachedUidValidity">The cached UIDVALIDITY value of the IMAP folder from a previous session.</param>
static void ResyncFolder (ImapFolder folder, List<CachedMessageInfo> cache, ref uint cachedUidValidity)
{
IList<IMessageSummary> summaries;
// Step 1: Open the folder.
// Note: we only need read-only access to update our cache, but depending on
// what you plan to do with the folder after resynchronizing, you may want
// to open the folder in read-write mode instead.
folder.Open (FolderAccess.ReadOnly);
if (cache.Count > 0) {
if (folder.UidValidity == cachedUidValidity) {
// Step 2: Remove messages from our cache that no longer exist on the server.
// get the full list of UIDs on the server...
var all = folder.Search (SearchQuery.All);
// remove any messages from our cache that no longer exist...
for (int i = 0; i < cache.Count; i++) {
if (!all.Contains (cache[i].UniqueId)) {
cache.RemoveAt (i);
i--;
}
}
// Step 3: Sync any flag changes for our cached messages.
// get a list of known uids... astute observers will note that an easy
// optimization to make here would be to merge this loop with the above
// loop.
var known = new UniqueIdSet (SortOrder.Ascending);
for (int i = 0; i < cache.Count; i++)
known.Add (cache[i].UniqueId);
// fetch the flags for our known messages...
summaries = folder.Fetch (known, MessageSummaryItems.Flags);
for (int i = 0; i < summaries.Count; i++) {
// Note: the indexes should match up with our cache, but it wouldn't
// hurt to add error checking to make sure. I'm not bothering to here
// for simplicity reasons.
cache[i].Flags = summaries[i].Flags.Value;
cache[i].Keywords = summaries[i].Keywords;
}
} else {
// The UIDVALIDITY of the folder has changed. This means that our entire
// cache is obsolete. We need to clear our cache and start from scratch.
cachedUidValidity = folder.UidValidity;
cache.Clear ();
}
} else {
// We have nothing cached, so just start from scratch.
cachedUidValidity = folder.UidValidity;
}
// Step 4: Fetch the messages we don't already know about and add them to our cache.
summaries = folder.Fetch (cache.Count, -1, MessageSummaryItems.UniqueId | MessageSummaryItems.Flags | MessageSummaryItems.Envelope | MessageSummaryItems.BodyStructure);
for (int i = 0; i < summaries.Count; i++) {
cache.Add (new CachedMessageInfo {
UniqueId = summaries[i].UniqueId,
Flags = summaries[i].Flags.Value,
Keywords = summaries[i].Keywords,
Envelope = summaries[i].Envelope,
Body = summaries[i].Body
});
}
// Tada! Now we are resynchronized with the server!
}
How do I log in to a shared mailbox in Office365?
var result = await GetPublicClientOAuth2CredentialsAsync ("IMAP", "sharedMailboxName@custom-domain.com");
// Note: We always use result.Account.Username instead of `Username` because the user may have selected an alternative account.
var oauth2 = new SaslMechanismOAuth2 (result.Account.Username, result.AccessToken);
using (var client = new ImapClient ()) {
await client.ConnectAsync ("outlook.office365.com", 993, SecureSocketOptions.SslOnConnect);
await client.AuthenticateAsync (oauth2);
// ...
await client.DisconnectAsync (true);
}
Why doesn't the message show up in the "Sent Mail" folder after sending it?
It seems to be a common misunderstanding that messages sent via SMTP will magically show up in the account's "Sent Mail" folder.
In order for the message to show up in the "Sent Mail" folder, you will need to append the message to the "Sent Mail" folder yourself because the SMTP protocol does not support doing this automatically.
If the "Sent Mail" folder is a local mbox folder, you'll need to append it like this:
using (var mbox = File.Open ("C:\\path\\to\\Sent Mail.mbox", FileMode.Append, FileAccess.Write)) {
var marker = string.Format ("From MAILER-DAEMON {0}{1}", DateTime.Now.ToString ("ddd MMM d HH:mm:ss yyyy", CultureInfo.InvariantCulture), Environment.NewLine);
var bytes = Encoding.ASCII.GetBytes (marker);
// Write the mbox marker bytes.
mbox.Write (bytes, 0, bytes.Length);
// Write the message, making sure to escape any line that looks like an mbox From-marker.
using (var filtered = new FilteredStream (mbox)) {
filtered.Add (new MboxFromFilter ());
message.WriteTo (filtered);
filtered.Flush ();
}
mbox.Flush ();
}
If the "Sent Mail" folder exists on an IMAP server, you would need to do something more like this:
using (var client = new ImapClient ()) {
client.Connect ("imap.server.com", 993, SecureSocketOptions.SslOnConnect);
client.Authenticate ("username", "password");
IMailFolder sentMail;
if (client.Capabilities.Contains (ImapCapability.SpecialUse)) {
sentMail = client.GetFolder (SpecialFolder.Sent);
} else {
var personal = client.GetFolder (client.PersonalNamespaces[0]);
// Note: This assumes that the "Sent Mail" folder lives at the root of the folder hierarchy
// and is named "Sent Mail" as opposed to "Sent" or "Sent Items" or any other variation.
sentMail = personal.GetSubfolder ("Sent Mail");
}
sentMail.Append (message, MessageFlags.Seen);
client.Disconnect (true);
}
How do I send email to a SpecifiedPickupDirectory?
Based on Microsoft's referencesource, when SmtpDeliveryMethod.SpecifiedPickupDirectory is used, the System.Net.Mail.SmtpClient saves the message to the specified pickup directory location using a randomly generated filename based on Guid.NewGuid ().ToString () + ".eml", so to achieve the same results with MailKit, you could do something like this:
public static void SaveToPickupDirectory (MimeMessage message, string pickupDirectory)
{
do {
// Generate a random file name to save the message to.
var path = Path.Combine (pickupDirectory, Guid.NewGuid ().ToString () + ".eml");
Stream stream;
try {
// Attempt to create the new file.
stream = File.Open (path, FileMode.CreateNew);
} catch (IOException) {
// If the file already exists, try again with a new Guid.
if (File.Exists (path))
continue;
// Otherwise, fail immediately since it probably means that there is
// no graceful way to recover from this error.
throw;
}
try {
using (stream) {
// IIS pickup directories expect the message to be "byte-stuffed"
// which means that lines beginning with "." need to be escaped
// by adding an extra "." to the beginning of the line.
//
// Use an SmtpDataFilter "byte-stuff" the message as it is written
// to the file stream. This is the same process that an SmtpClient
// would use when sending the message in a `DATA` command.
using (var filtered = new FilteredStream (stream)) {
filtered.Add (new SmtpDataFilter ());
// Make sure to write the message in DOS (CRLF) format.
var options = FormatOptions.Default.Clone ();
options.NewLineFormat = NewLineFormat.Dos;
message.WriteTo (options, filtered);
filtered.Flush ();
return;
}
}
} catch {
// An exception here probably means that the disk is full.
//
// Delete the file that was created above so that incomplete files are not
// left behind for IIS to send accidentally.
File.Delete (path);
throw;
}
} while (true);
}
How do I request a notification when the message is read by the user?
The first thing to make clear is that requesting a notification does not guarantee that you'll actually get one. In order for you to receive a notification that the message was read by its recipient, the recipient's mail client needs to know how to send such a notification and the user must have enabled it to do so.
That said, here's how you can request a notification when the recipient reads the message that has been sent:
// Add the following header to tell the recipient's client that you want to receive a
// notification when the message has been read by the user.
message.Headers[HeaderId.DispositionNotificationTo] = new MailboxAddress ("My Name", "me@example.com").ToString (true);
For more information on this topic, read rfc3798.
How do I process a read receipt notification?
A read receipt notification comes in the form of a MIME message with a top-level MIME part with a MIME-type of multipart/report that has a report-type parameter with a value of disposition-notification.
You could check for this in code like this:
var report = message.Body as MultipartReport;
if (report != null && report.ReportType.Equals ("disposition-notification", StringComparison.OrdinalIgnoreCase)) {
// This is a read receipt notification.
}
The first part of the multipart/report will be a human-readable explanation of the notification.
The second part will have a MIME-type of message/disposition-notification and be represented by a MessageDispositionNotification.
This notification part will contain a list of header-like fields containing information about the message that this notification is for such as the Original-Message-Id, Original-Recipient, etc.
var notification = report[1] as MessageDispositionNotification;
if (notification != null) {
// Get the Message-Id of the message this notification is for...
var messageId = notification.Fields["Original-Message-Id"];
}
For more information on this topic, read rfc3798.