1289 lines
47 KiB
Markdown
1289 lines
47 KiB
Markdown
# Frequently Asked Questions
|
|
|
|
## Question Index
|
|
|
|
### General
|
|
|
|
* [Are MimeKit and MailKit completely free? Can I use them in my proprietary product(s)?](#completely-free)
|
|
* [Why do I get `NotSupportedException: No data is available for encoding ######. For information on defining a custom encoding, see the documentation for the Encoding.RegisterProvider method.`?](#register-provider)
|
|
* [Why do I get a `TypeLoadException` when I try to create a new MimeMessage?](#type-load-exception)
|
|
|
|
### Messages
|
|
|
|
* [How do I create a message with attachments?](#create-attachments)
|
|
* [How do I get the main body of a message?](#message-body)
|
|
* [How do I tell if a message has attachments?](#has-attachments)
|
|
* [Why doesn't the `MimeMessage` class implement `ISerializable` so that I can serialize a message to disk and read it back later?](#serialize-message)
|
|
* [How do I parse messages?](#load-messages)
|
|
* [How do I save messages?](#save-messages)
|
|
* [How do I save attachments?](#save-attachments)
|
|
* [How do I get the email addresses in the From, To, and Cc headers?](#address-headers)
|
|
* [Why do attachments with unicode or long filenames appear as "ATT0####.dat" in Outlook?](#untitled-attachments)
|
|
* [How do I decrypt PGP messages that are embedded in the main message text?](#decrypt-inline-pgp)
|
|
* [How do I reply to a message using MimeKit?](#reply-message)
|
|
* [How do I forward a message?](#forward-message)
|
|
* [Why does text show up garbled in my ASP.NET Core / .NET Core / .NET 5 app?](#garbled-text)
|
|
|
|
### Specialty
|
|
|
|
* [How would I parse multipart/form-data from an HTTP web request?](#parse-web-request-form-data)
|
|
|
|
## General
|
|
|
|
### <a name="completely-free">Q: Are MimeKit and MailKit completely free? Can I use them in my proprietary product(s)?</a>
|
|
|
|
Yes. MimeKit and MailKit are both completely free and open source. They are both covered under the
|
|
[MIT](https://opensource.org/licenses/MIT) license.
|
|
|
|
### <a name="register-provider">Q: Why do I get `NotSupportedException: No data is available for encoding ######. For information on defining a custom encoding, see the documentation for the Encoding.RegisterProvider method.`?</a>
|
|
|
|
In .NET Core, Microsoft decided to split out the non-Unicode text encodings into a separate NuGet package called
|
|
[System.Text.Encoding.CodePages](https://www.nuget.org/packages/System.Text.Encoding.CodePages).
|
|
|
|
MimeKit already pulls in a reference to this NuGet package, so you shsouldn'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):
|
|
|
|
```csharp
|
|
System.Text.Encoding.RegisterProvider (System.Text.CodePagesEncodingProvider.Instance);
|
|
```
|
|
|
|
### <a name="type-load-exception">Q: Why do I get a `TypeLoadException` when I try to create a new MimeMessage?</a>
|
|
|
|
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`:
|
|
|
|
```xml
|
|
<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>
|
|
```
|
|
|
|
## Messages
|
|
|
|
### <a name="create-attachments">Q: How do I create a message with attachments?</a>
|
|
|
|
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
|
|
[MimePart.FileName](https://www.mimekit.net/docs/html/P_MimeKit_MimePart_FileName.htm) 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.
|
|
|
|
```csharp
|
|
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.Default),
|
|
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](https://www.mimekit.net/docs/html/T_MimeKit_BodyBuilder.htm) class.
|
|
|
|
```csharp
|
|
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](https://www.mimekit.net/docs/html/Creating-Messages.htm).
|
|
|
|
### <a name="message-body">Q: How do I get the main body of a message?</a>
|
|
|
|
(Note: for the TL;DR version, skip to [the end](#message-body-tldr))
|
|
|
|
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:
|
|
|
|
1. The message contains only a `text/plain` or `text/html` part (easy, just use that).
|
|
|
|
2. The message contains a `multipart/alternative` which will typically look a bit like this:
|
|
|
|
```
|
|
multipart/alternative
|
|
text/plain
|
|
text/html
|
|
```
|
|
|
|
3. 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
|
|
```
|
|
|
|
4. The message contains a textual body part as well as some attachments:
|
|
|
|
```
|
|
multipart/mixed
|
|
text/plain or text/html
|
|
application/octet-stream
|
|
application/zip
|
|
```
|
|
|
|
5. 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/zip
|
|
```
|
|
|
|
or...
|
|
|
|
```
|
|
multipart/mixed
|
|
multipart/alternative
|
|
text/plain
|
|
multipart/related
|
|
text/html
|
|
image/jpeg
|
|
image/png
|
|
application/octet-stream
|
|
application/zip
|
|
```
|
|
|
|
<a name="message-body-tldr"></a>Now, if you don't care about any of that and just want to get the text of
|
|
the first `text/plain` or `text/html` part you can find, that's easy.
|
|
|
|
[MimeMessage](https://www.mimekit.net/docs/html/T_MimeKit_MimeMessage.htm) has two convenience properties
|
|
for this: [TextBody](https://www.mimekit.net/docs/html/P_MimeKit_MimeMessage_TextBody.htm) and
|
|
[HtmlBody](https://www.mimekit.net/docs/html/P_MimeKit_MimeMessage_HtmlBody.htm).
|
|
|
|
`MimeMessage.HtmlBody`, as the name implies, will traverse the MIME structure for you and find the most
|
|
appropriate body part with a `Content-Type` of `text/html` that can be interpreted as the message body.
|
|
Likewise, the `TextBody` property can be used to get the `text/plain` version of the message body.
|
|
|
|
For more information, see [Working with Messages](https://www.mimekit.net/docs/html/Working-With-Messages.htm).
|
|
|
|
### <a name="has-attachments">Q: How do I tell if a message has attachments?</a>
|
|
|
|
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
|
|
[MimeMessage.Attachments](https://www.mimekit.net/docs/html/P_MimeKit_MimeMessage_Attachments.htm) 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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
/// <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> ();
|
|
List<MimeEntity> calenderAttachments = new List<MimeEntity> ();
|
|
|
|
readonly string tempDir;
|
|
string body;
|
|
|
|
/// <summary>
|
|
/// Creates a new HtmlPreviewVisitor.
|
|
/// </summary>
|
|
/// <param name="tempDirectory">A temporary directory used for storing image files.</param>
|
|
public HtmlPreviewVisitor (string tempDirectory)
|
|
{
|
|
tempDir = tempDirectory;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The list of attachments that were in the MimeMessage.
|
|
/// </summary>
|
|
public IList<MimeEntity> Attachments {
|
|
get { return attachments; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The list of text/calender entries that were in the MimeMessage.
|
|
/// </summary>
|
|
public IList<MimeEntity> CalenderAttachments {
|
|
get { return calenderAttachments; }
|
|
}
|
|
|
|
/// <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 file:// URI for the image attachment.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Saves the image attachment to a temp file and returns a file:// URI for the
|
|
/// temp file.
|
|
/// </remarks>
|
|
/// <returns>The file:// URI.</returns>
|
|
/// <param name="image">The image attachment.</param>
|
|
/// <param name="url">The original HTML image URL.</param>
|
|
string GetFileUri (MimePart image, string url)
|
|
{
|
|
string fileName = url.Replace (':', '_').Replace ('\\', '_').Replace ('/', '_');
|
|
|
|
string path = Path.Combine (tempDir, fileName);
|
|
|
|
if (!File.Exists (path)) {
|
|
using (var output = File.Create (path))
|
|
image.Content.DecodeTo (output);
|
|
}
|
|
|
|
return "file://" + path.Replace ('\\', '/');
|
|
}
|
|
|
|
/// <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 file:// URL
|
|
foreach (var attribute in ctx.Attributes) {
|
|
if (attribute.Id == HtmlAttributeId.Src) {
|
|
if (!TryGetImage (attribute.Value, out var image)) {
|
|
htmlWriter.WriteAttribute (attribute);
|
|
continue;
|
|
}
|
|
|
|
// Note: you can either use a "file://" URI or you can use a
|
|
// "data:" URI, the choice is yours.
|
|
var uri = GetFileUri (image, attribute.Value);
|
|
//var uri = GetDataUri (image);
|
|
|
|
htmlWriter.WriteAttributeName (attribute.Name);
|
|
htmlWriter.WriteAttributeValue (uri);
|
|
} else {
|
|
htmlWriter.WriteAttribute (attribute);
|
|
}
|
|
}
|
|
} else if (ctx.TagId == HtmlTagId.Body && !ctx.IsEndTag) {
|
|
ctx.WriteTag (htmlWriter, false);
|
|
|
|
// add and/or replace oncontextmenu="return false;"
|
|
foreach (var attribute in ctx.Attributes) {
|
|
if (attribute.Name.Equals ("oncontextmenu", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
htmlWriter.WriteAttribute (attribute);
|
|
}
|
|
|
|
htmlWriter.WriteAttribute ("oncontextmenu", "return false;");
|
|
} else {
|
|
// pass the tag through to the output
|
|
ctx.WriteTag (htmlWriter, true);
|
|
}
|
|
}
|
|
|
|
protected override void VisitTextPart (TextPart entity)
|
|
{
|
|
TextConverter converter;
|
|
|
|
// treat text/calendar parts as attachments rather than message bodies
|
|
if (entity.ContentType.IsMimeType ("text", "calendar")) {
|
|
calendarAattachments.Add (entity);
|
|
return;
|
|
}
|
|
|
|
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
|
|
attachments.AddRange (entity.ExtractAttachments ());
|
|
}
|
|
|
|
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:
|
|
|
|
```csharp
|
|
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 `MimeMessage.Attachments`
|
|
property.
|
|
|
|
### <a name="serialize-message">Q: Why doesn't the `MimeMessage` class implement `ISerializable` so that I can serialize a message to disk and read it back later?</a>
|
|
|
|
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](https://www.mimekit.net/docs/html/T_MimeKit_MimeMessage.htm) to a stream using the
|
|
[WriteTo](https://www.mimekit.net/docs/html/Overload_MimeKit_MimeMessage_WriteTo.htm) methods.
|
|
|
|
For more information on this topic, see the following other two topics:
|
|
|
|
* [How do I parse messages?](#load-messages)
|
|
* [How do I save messages?](#save-messages)
|
|
|
|
### <a name="load-messages">Q: How do I parse messages?</a>
|
|
|
|
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](https://www.mimekit.net/docs/html/Overload_MimeKit_MimeMessage_Load.htm) methods
|
|
on `MimeMessage`:
|
|
|
|
```csharp
|
|
// Load a MimeMessage from a stream
|
|
var message = MimeMessage.Load (stream);
|
|
```
|
|
|
|
Or you can load a message from a file path:
|
|
|
|
```csharp
|
|
// Load a MimeMessage from a file path
|
|
var message = MimeMessage.Load ("message.eml");
|
|
```
|
|
|
|
The second way is to use the [MimeParser](https://www.mimekit.net/docs/html/T_MimeKit_MimeParser.htm) 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:
|
|
|
|
```csharp
|
|
// 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:
|
|
|
|
```csharp
|
|
// 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
|
|
}
|
|
```
|
|
|
|
### <a name="save-messages">Q: How do I save messages?</a>
|
|
|
|
One you've got a [MimeMessage](https://www.mimekit.net/docs/html/T_MimeKit_MimeMessage.htm), you can save
|
|
it to a file using the [WriteTo](https://mimekit.net/docs/html/Overload_MimeKit_MimeMessage_WriteTo.htm) method:
|
|
|
|
```csharp
|
|
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](https://mimekit.net/docs/html/T_MimeKit_FormatOptions.htm) argument to
|
|
the method:
|
|
|
|
```csharp
|
|
// 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");
|
|
```
|
|
|
|
Note: While it may seem like you can safely use the `ToString` method to serialize a message,
|
|
***DON'T DO IT!*** This is ***not*** safe! MIME messages cannot be accurately represented as
|
|
strings due to the fact that each MIME part of the message *may* be encoded in a different
|
|
character set, thus making it impossible to convert the message into a unicode string using a
|
|
single charset to do the conversion (which is *exactly* what `ToString` does).
|
|
|
|
### <a name="save-attachments">Q: How do I save attachments?</a>
|
|
|
|
If you've already got a [MimePart](https://www.mimekit.net/docs/html/T_MimeKit_MimePart.htm) that represents
|
|
the attachment that you'd like to save, here's how you might save it:
|
|
|
|
```csharp
|
|
using (var stream = File.Create (fileName))
|
|
attachment.Content.DecodeTo (stream);
|
|
```
|
|
|
|
Pretty simple, right?
|
|
|
|
But what if your attachment is actually a [MessagePart](https://www.mimekit.net/docs/html/T_MimeKit_MessagePart.htm)?
|
|
|
|
To save the content of a `message/rfc822` part, you'd use the following code snippet:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
foreach (var attachment in message.Attachments) {
|
|
var fileName = attachment.ContentDisposition?.FileName ?? attachment.ContentType.Name;
|
|
|
|
using (var stream = File.Create (fileName)) {
|
|
if (attachment is MessagePart) {
|
|
var rfc822 = (MessagePart) attachment;
|
|
|
|
rfc822.Message.WriteTo (stream);
|
|
} else {
|
|
var part = (MimePart) attachment;
|
|
|
|
part.Content.DecodeTo (stream);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### <a name="address-headers">Q: How do I get the email addresses in the From, To, and Cc headers?</a>
|
|
|
|
The [From](https://www.mimekit.net/docs/html/P_MimeKit_MimeMessage_From.htm),
|
|
[To](https://www.mimekit.net/docs/html/P_MimeKit_MimeMessage_To.htm), and
|
|
[Cc](https://www.mimekit.net/docs/html/P_MimeKit_MimeMessage_Cc.htm) properties of a
|
|
[MimeMessage](https://www.mimekit.net/docs/html/T_MimeKit_MimeMessage.htm) are all of type
|
|
[InternetAddressList](https://www.mimekit.net/docs/html/T_MimeKit_InternetAddressList.htm). An
|
|
`InternetAddressList` is a list of
|
|
[InternetAddress](https://www.mimekit.net/docs/html/T_MimeKit_InternetAddress.htm) items. This is
|
|
where most people start to get lost because an `InternetAddress` is an abstract class that only
|
|
really has a [Name](https://www.mimekit.net/docs/html/P_MimeKit_InternetAddress_Name.htm) property.
|
|
|
|
As you've probably already discovered, the `Name` property contains the name of the person
|
|
(if available), but what you want is his or her email address, not their name.
|
|
|
|
To get the email address, you'll need to figure out what subclass of address each `InternetAddress`
|
|
really is. There are 2 subclasses of `InternetAddress`:
|
|
[GroupAddress](https://www.mimekit.net/docs/html/T_MimeKit_GroupAddress.htm) and
|
|
[MailboxAddress](https://www.mimekit.net/docs/html/T_MimeKit_MailboxAddress.htm).
|
|
|
|
A `GroupAddress` is a named group of more `InternetAddress` items that are contained within the
|
|
[Members](https://www.mimekit.net/docs/html/P_MimeKit_GroupAddress_Members.htm) 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](https://www.mimekit.net/docs/html/P_MimeKit_MailboxAddress_Address.htm) 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 a `From`, `To`, or `Cc`
|
|
header, you can do something like this:
|
|
|
|
```csharp
|
|
foreach (var mailbox in message.To.Mailboxes)
|
|
Console.WriteLine ("{0}'s email address is {1}", mailbox.Name, mailbox.Address);
|
|
```
|
|
|
|
### <a name="untitled-attachments">Q: Why do attachments with unicode or long filenames appear as "ATT0####.dat" in Outlook?</a>
|
|
|
|
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](https://tools.ietf.org/html/rfc2184) (later updated by [rfc2231](https://tools.ietf.org/html/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](https://tools.ietf.org/html/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](https://www.mimekit.net/docs/html/T_MimeKit_Parameter.htm):
|
|
|
|
```csharp
|
|
Parameter param;
|
|
|
|
if (attachment.ContentDisposition.Parameters.TryGetValue ("filename", out param))
|
|
param.EncodingMethod = ParameterEncodingMethod.Rfc2047;
|
|
```
|
|
|
|
Or
|
|
|
|
```csharp
|
|
foreach (var param in attachment.ContentDisposition.Parameters) {
|
|
param.EncodingMethod = ParameterEncodingMethod.Rfc2047;
|
|
}
|
|
```
|
|
|
|
### <a name="decrypt-inline-pgp">Q: How do I decrypt PGP messages that are embedded in the main message text?</a>
|
|
|
|
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:
|
|
|
|
```text
|
|
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, I've added a method to OpenPgpContext called `GetDecryptedStream` which
|
|
can be used to get the raw decrypted stream.
|
|
|
|
There are actually 2 variants of this method:
|
|
|
|
```csharp
|
|
public Stream GetDecryptedStream (Stream encryptedData, out DigitalSignatureCollection signatures)
|
|
```
|
|
|
|
and
|
|
|
|
```csharp
|
|
public Stream GetDecryptedStream (Stream encryptedData)
|
|
```
|
|
|
|
The first variant is useful in cases where the encrypted PGP blurb is also digitally signed, allowing you to get
|
|
your hands on the list of digitial signatures in order for you to verify each of them.
|
|
|
|
To decrypt the content of the message, you'll want to locate the `TextPart` (in this case, it'll just be
|
|
`message.Body`)
|
|
and then do this:
|
|
|
|
```csharp
|
|
static Stream DecryptEmbeddedPgp (TextPart text)
|
|
{
|
|
using (var memory = new MemoryStream ()) {
|
|
text.Content.DecodeTo (memory);
|
|
memory.Position = 0;
|
|
|
|
using (var ctx = new MyGnuPGContext ()) {
|
|
return ctx.GetDecryptedStream (memory);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
What you do with that decrypted stream is up to you. It's up to you to figure out what the decrypted content is
|
|
(is it text? a jpeg image? a video?) and how to display it to the user.
|
|
|
|
### <a name="reply-message">Q: How do I reply to a message?</a>
|
|
|
|
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:
|
|
|
|
1. 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: "`).
|
|
2. 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.
|
|
3. 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.
|
|
4. 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:
|
|
|
|
```csharp
|
|
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 - TODO: remove ourselves from these lists
|
|
reply.To.AddRange (message.To);
|
|
reply.Cc.AddRange (message.Cc);
|
|
}
|
|
|
|
// set the reply subject
|
|
if (!message.Subject?.StartsWith ("Re:", StringComparison.OrdinalIgnoreCase))
|
|
reply.Subject = "Re: " + (message.Subject ?? string.Empty);
|
|
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 ();
|
|
|
|
quoted.WriteLine ("On {0}, {1} wrote:", message.Date.ToString ("f"), !string.IsNullOrEmpty (sender.Name) ? sender.Name : sender.Address);
|
|
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](https://www.mimekit.net/docs/html/T_MimeKit_MimeVisitor.htm) to handle this:
|
|
|
|
```csharp
|
|
public class ReplyVisitor : MimeVisitor
|
|
{
|
|
readonly Stack<Multipart> stack = new Stack<Multipart> ();
|
|
MimeMessage original, reply;
|
|
MailboxAddress from;
|
|
bool replyToAll;
|
|
|
|
/// <summary>
|
|
/// Creates a new ReplyVisitor.
|
|
/// </summary>
|
|
public ReplyVisitor (MailboxAddress from, bool replyToAll)
|
|
{
|
|
this.replyToAll = replyToAll;
|
|
this.from = from;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the reply.
|
|
/// </summary>
|
|
/// <value>The reply.</value>
|
|
public MimeMessage Reply {
|
|
get { return reply; }
|
|
}
|
|
|
|
void Push (MimeEntity entity)
|
|
{
|
|
var multipart = entity as Multipart;
|
|
|
|
if (reply.Body == null) {
|
|
reply.Body = entity;
|
|
} else {
|
|
var parent = stack.Peek ();
|
|
parent.Add (entity);
|
|
}
|
|
|
|
if (multipart != null)
|
|
stack.Push (multipart);
|
|
}
|
|
|
|
void Pop ()
|
|
{
|
|
stack.Pop ();
|
|
}
|
|
|
|
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) : "an unknown sender";
|
|
|
|
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)
|
|
{
|
|
reply = new MimeMessage ();
|
|
original = message;
|
|
|
|
stack.Clear ();
|
|
|
|
reply.From.Add (from.Clone ());
|
|
|
|
// 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 - TODO: remove ourselves from these lists
|
|
reply.To.AddRange (message.To);
|
|
reply.Cc.AddRange (message.Cc);
|
|
}
|
|
|
|
// set the reply subject
|
|
if (!message.Subject?.StartsWith ("Re:", StringComparison.OrdinalIgnoreCase))
|
|
reply.Subject = "Re: " + (message.Subject ?? string.Empty);
|
|
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);
|
|
}
|
|
|
|
base.Visit (message);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Visit the specified entity.
|
|
/// </summary>
|
|
/// <param name="entity">The MIME entity.</param>
|
|
/// <exception cref="System.NotSupportedException">
|
|
/// Only Visit(MimeMessage) is supported.
|
|
/// </exception>
|
|
public override void Visit (MimeEntity entity)
|
|
{
|
|
throw new NotSupportedException ();
|
|
}
|
|
|
|
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 (original));
|
|
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 (original));
|
|
|
|
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
|
|
}
|
|
}
|
|
```
|
|
|
|
```csharp
|
|
public static MimeMessage Reply (MimeMessage message, MailboxAddress from, bool replyToAll)
|
|
{
|
|
var visitor = new ReplyVisitor (from, replyToAll);
|
|
|
|
visitor.Visit (message);
|
|
|
|
return visitor.Reply;
|
|
}
|
|
```
|
|
|
|
### <a name="forward-message">Q: How do I forward a message?</a>
|
|
|
|
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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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 --------");
|
|
text.WriteLine ("Subject: {0}", original.Subject ?? string.Empty);
|
|
text.WriteLine ("Date: {0}", DateUtils.FormatDate (original.Date));
|
|
text.WriteLine ("From: {0}", original.From);
|
|
text.WriteLine ("To: {0}", original.To);
|
|
text.WriteLine ();
|
|
|
|
text.Write (original.TextBody);
|
|
|
|
message.Body = new TextPart ("plain") {
|
|
Text = text.ToString ()
|
|
};
|
|
}
|
|
|
|
return message;
|
|
}
|
|
```
|
|
|
|
Keep in mind that not all messages will have a `TextBody` available, so you'll have to find a way to handle those cases.
|
|
|
|
### <a name="garbled-text">Q: Why does text show up garbled in my ASP.NET Core / .NET Core / .NET 5 app?</a>
|
|
|
|
.NET Core (and ASP.NET Core by extension) and .NET >= 5 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](https://docs.microsoft.com/en-us/dotnet/api/system.text.encoding.registerprovider?view=net-5.0) the encoding
|
|
provider that provides all of the additional encodings.
|
|
|
|
First, add a package reference for the [System.Text.Encoding.CodePages](https://www.nuget.org/packages/System.Text.Encoding.CodePages)
|
|
nuget package to your project and then register the additional text encodings using the following code snippet:
|
|
|
|
```csharp
|
|
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.
|
|
|
|
## Specialty
|
|
|
|
### <a name="parse-web-request-form-data">Q: How would I parse multipart/form-data from an HTTP web request?</a>
|
|
|
|
Since classes like `HttpWebResponse` 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 on `MimeEntity`:
|
|
|
|
```csharp
|
|
public static MimeEntity Load (ParserOptions options, ContentType contentType, Stream content, CancellationToken cancellationToken = default (CancellationToken));
|
|
|
|
public static MimeEntity Load (ContentType contentType, Stream content, CancellationToken cancellationToken = default (CancellationToken));
|
|
```
|
|
|
|
Here's how you might use these methods:
|
|
|
|
```csharp
|
|
MimeEntity ParseMultipartFormData (HttpWebResponse response)
|
|
{
|
|
var contentType = ContentType.Parse (response.ContentType);
|
|
|
|
return MimeEntity.Load (contentType, response.GetResponseStream ());
|
|
}
|
|
```
|