Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/mono/corefx.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pkg/Microsoft.Private.PackageBaseline/packageIndex.json5
-rw-r--r--pkg/descriptions.json10
-rw-r--r--src/Common/src/System/IO/DelegatingStream.cs (renamed from src/System.Net.Http/src/System/Net/Http/DelegatingStream.cs)7
-rw-r--r--src/Common/src/System/Net/Mail/DomainLiteralReader.cs (renamed from src/System.Net.Http/src/Internal/Mail/DomainLiteralReader.cs)0
-rw-r--r--src/Common/src/System/Net/Mail/DotAtomReader.cs (renamed from src/System.Net.Http/src/Internal/Mail/DotAtomReader.cs)0
-rw-r--r--src/Common/src/System/Net/Mail/MailAddress.cs (renamed from src/System.Net.Http/src/Internal/MailAddress.cs)0
-rw-r--r--src/Common/src/System/Net/Mail/MailAddressParser.cs (renamed from src/System.Net.Http/src/Internal/Mail/MailAddressParser.cs)0
-rw-r--r--src/Common/src/System/Net/Mail/MailBnfHelper.cs419
-rw-r--r--src/Common/src/System/Net/Mail/QuotedPairReader.cs (renamed from src/System.Net.Http/src/Internal/Mail/QuotedPairReader.cs)0
-rw-r--r--src/Common/src/System/Net/Mail/QuotedStringFormatReader.cs (renamed from src/System.Net.Http/src/Internal/Mail/QuotedStringFormatReader.cs)0
-rw-r--r--src/Common/src/System/Net/Mail/WhitespaceReader.cs (renamed from src/System.Net.Http/src/Internal/Mail/WhitespaceReader.cs)0
-rw-r--r--src/System.Net.Http/src/Internal/Mail/MailBnfHelper.cs117
-rw-r--r--src/System.Net.Http/src/System.Net.Http.csproj37
-rw-r--r--src/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj36
-rw-r--r--src/System.Net.Mime/System.Net.Mime.sln59
-rw-r--r--src/System.Net.Mime/dir.props7
-rw-r--r--src/System.Net.Mime/pkg/System.Net.Mime.builds8
-rw-r--r--src/System.Net.Mime/pkg/System.Net.Mime.pkgproj13
-rw-r--r--src/System.Net.Mime/ref/System.Net.Mime.cs78
-rw-r--r--src/System.Net.Mime/ref/System.Net.Mime.csproj17
-rw-r--r--src/System.Net.Mime/ref/project.json15
-rw-r--r--src/System.Net.Mime/src/Resources/Strings.resx172
-rw-r--r--src/System.Net.Mime/src/System.Net.Mime.builds12
-rw-r--r--src/System.Net.Mime/src/System.Net.Mime.csproj75
-rw-r--r--src/System.Net.Mime/src/System/Net/Mail/BufferBuilder.cs95
-rw-r--r--src/System.Net.Mime/src/System/Net/Mail/MailHeaderID.cs47
-rw-r--r--src/System.Net.Mime/src/System/Net/Mail/MailHeaderInfo.cs147
-rw-r--r--src/System.Net.Mime/src/System/Net/Mime/Base64Stream.cs586
-rw-r--r--src/System.Net.Mime/src/System/Net/Mime/Base64WriteStateInfo.cs19
-rw-r--r--src/System.Net.Mime/src/System/Net/Mime/BaseWriter.cs209
-rw-r--r--src/System.Net.Mime/src/System/Net/Mime/CloseableStream.cs32
-rw-r--r--src/System.Net.Mime/src/System/Net/Mime/ContentDisposition.cs342
-rw-r--r--src/System.Net.Mime/src/System/Net/Mime/ContentType.cs319
-rw-r--r--src/System.Net.Mime/src/System/Net/Mime/DispositionTypeNames.cs12
-rw-r--r--src/System.Net.Mime/src/System/Net/Mime/EightBitStream.cs164
-rw-r--r--src/System.Net.Mime/src/System/Net/Mime/EncodedStreamFactory.cs74
-rw-r--r--src/System.Net.Mime/src/System/Net/Mime/HeaderCollection.cs213
-rw-r--r--src/System.Net.Mime/src/System/Net/Mime/IEncodableStream.cs16
-rw-r--r--src/System.Net.Mime/src/System/Net/Mime/MediaTypeNames.cs33
-rw-r--r--src/System.Net.Mime/src/System/Net/Mime/MimeBasePart.cs289
-rw-r--r--src/System.Net.Mime/src/System/Net/Mime/MimePart.cs384
-rw-r--r--src/System.Net.Mime/src/System/Net/Mime/MultiAsyncResult.cs50
-rw-r--r--src/System.Net.Mime/src/System/Net/Mime/QEncodedStream.cs400
-rw-r--r--src/System.Net.Mime/src/System/Net/Mime/QuotedPrintableStream.cs444
-rw-r--r--src/System.Net.Mime/src/System/Net/Mime/SmtpDateTime.cs422
-rw-r--r--src/System.Net.Mime/src/System/Net/Mime/TrackingStringDictionary.cs73
-rw-r--r--src/System.Net.Mime/src/System/Net/Mime/TrackingValidationObjectDictionary.cs198
-rw-r--r--src/System.Net.Mime/src/System/Net/Mime/TransferEncoding.cs15
-rw-r--r--src/System.Net.Mime/src/System/Net/Mime/WriteStateInfoBase.cs135
-rw-r--r--src/System.Net.Mime/src/project.json32
-rw-r--r--src/System.Net.Mime/tests/ContentDispositionTests.cs152
-rw-r--r--src/System.Net.Mime/tests/ContentTypeTests.cs173
-rw-r--r--src/System.Net.Mime/tests/System.Net.Mime.Tests.builds15
-rw-r--r--src/System.Net.Mime/tests/System.Net.Mime.Tests.csproj36
-rw-r--r--src/System.Net.Mime/tests/project.json48
55 files changed, 6115 insertions, 146 deletions
diff --git a/pkg/Microsoft.Private.PackageBaseline/packageIndex.json b/pkg/Microsoft.Private.PackageBaseline/packageIndex.json
index 9707c578f7..758e7ac0c3 100644
--- a/pkg/Microsoft.Private.PackageBaseline/packageIndex.json
+++ b/pkg/Microsoft.Private.PackageBaseline/packageIndex.json
@@ -1154,6 +1154,11 @@
"4.0.1.0": "4.3.0"
}
},
+ "System.Net.Mime": {
+ "AssemblyVersionInPackageVersion": {
+ "4.0.0.0": "4.3.0"
+ }
+ },
"System.Net.NameResolution": {
"StableVersions": [
"4.0.0"
diff --git a/pkg/descriptions.json b/pkg/descriptions.json
index e93aa6a4b1..d38763484c 100644
--- a/pkg/descriptions.json
+++ b/pkg/descriptions.json
@@ -861,6 +861,16 @@
]
},
{
+ "Name": "System.Net.Mime",
+ "Description": "Provides types that are used to represent Multipurpose Internet Mail Exchange (MIME) headers.",
+ "CommonTypes": [
+ "System.Net.Mime.ContentDisposition",
+ "System.Net.Mime.ContentType",
+ "System.Net.Mime.DispositionTypeNames",
+ "System.Net.Mime.MediaTypeNames"
+ ]
+ },
+ {
"Name": "System.Net.NameResolution",
"Description": "Provides the System.Net.Dns class, which enables developers to perform simple domain name resolution.",
"CommonTypes": [
diff --git a/src/System.Net.Http/src/System/Net/Http/DelegatingStream.cs b/src/Common/src/System/IO/DelegatingStream.cs
index b3a3942892..4a7e3a5030 100644
--- a/src/System.Net.Http/src/System/Net/Http/DelegatingStream.cs
+++ b/src/Common/src/System/IO/DelegatingStream.cs
@@ -13,10 +13,15 @@ namespace System.Net.Http
// Forwards all calls to an inner stream except where overridden in a derived class.
internal abstract class DelegatingStream : Stream
{
- private Stream _innerStream;
+ private readonly Stream _innerStream;
#region Properties
+ protected Stream BaseStream
+ {
+ get { return _innerStream; }
+ }
+
public override bool CanRead
{
get { return _innerStream.CanRead; }
diff --git a/src/System.Net.Http/src/Internal/Mail/DomainLiteralReader.cs b/src/Common/src/System/Net/Mail/DomainLiteralReader.cs
index ffe542fbba..ffe542fbba 100644
--- a/src/System.Net.Http/src/Internal/Mail/DomainLiteralReader.cs
+++ b/src/Common/src/System/Net/Mail/DomainLiteralReader.cs
diff --git a/src/System.Net.Http/src/Internal/Mail/DotAtomReader.cs b/src/Common/src/System/Net/Mail/DotAtomReader.cs
index fc168748c3..fc168748c3 100644
--- a/src/System.Net.Http/src/Internal/Mail/DotAtomReader.cs
+++ b/src/Common/src/System/Net/Mail/DotAtomReader.cs
diff --git a/src/System.Net.Http/src/Internal/MailAddress.cs b/src/Common/src/System/Net/Mail/MailAddress.cs
index 560701d552..560701d552 100644
--- a/src/System.Net.Http/src/Internal/MailAddress.cs
+++ b/src/Common/src/System/Net/Mail/MailAddress.cs
diff --git a/src/System.Net.Http/src/Internal/Mail/MailAddressParser.cs b/src/Common/src/System/Net/Mail/MailAddressParser.cs
index bb5122f44e..bb5122f44e 100644
--- a/src/System.Net.Http/src/Internal/Mail/MailAddressParser.cs
+++ b/src/Common/src/System/Net/Mail/MailAddressParser.cs
diff --git a/src/Common/src/System/Net/Mail/MailBnfHelper.cs b/src/Common/src/System/Net/Mail/MailBnfHelper.cs
new file mode 100644
index 0000000000..b195645e41
--- /dev/null
+++ b/src/Common/src/System/Net/Mail/MailBnfHelper.cs
@@ -0,0 +1,419 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Text;
+using System.Net.Mail;
+using System.Globalization;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace System.Net.Mime
+{
+ internal static class MailBnfHelper
+ {
+ // characters allowed in atoms
+ internal static readonly bool[] Atext = CreateCharactersAllowedInAtoms();
+
+ // characters allowed in quoted strings (not including Unicode)
+ internal static readonly bool[] Qtext = CreateCharactersAllowedInQuotedStrings();
+
+ // characters allowed in domain literals
+ internal static readonly bool[] Dtext = CreateCharactersAllowedInDomainLiterals();
+
+ // characters allowed in header names
+ internal static readonly bool[] Ftext = CreateCharactersAllowedInHeaderNames();
+
+ // characters allowed in tokens
+ internal static readonly bool[] Ttext = CreateCharactersAllowedInTokens();
+
+ // characters allowed inside of comments
+ internal static readonly bool[] Ctext = CreateCharactersAllowedInComments();
+
+ internal static readonly int Ascii7bitMaxValue = 127;
+ internal static readonly char Quote = '\"';
+ internal static readonly char Space = ' ';
+ internal static readonly char Tab = '\t';
+ internal static readonly char CR = '\r';
+ internal static readonly char LF = '\n';
+ internal static readonly char StartComment = '(';
+ internal static readonly char EndComment = ')';
+ internal static readonly char Backslash = '\\';
+ internal static readonly char At = '@';
+ internal static readonly char EndAngleBracket = '>';
+ internal static readonly char StartAngleBracket = '<';
+ internal static readonly char StartSquareBracket = '[';
+ internal static readonly char EndSquareBracket = ']';
+ internal static readonly char Comma = ',';
+ internal static readonly char Dot = '.';
+
+ // NOTE: See RFC 2822 for more detail. By default, every value in the array is false and only
+ // those values which are allowed in that particular set are then set to true. The numbers
+ // annotating each definition below are the range of ASCII values which are allowed in that definition.
+
+ private static bool[] CreateCharactersAllowedInAtoms()
+ {
+ // atext = ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~"
+ var atext = new bool[128];
+ for (int i = '0'; i <= '9'; i++) { atext[i] = true; }
+ for (int i = 'A'; i <= 'Z'; i++) { atext[i] = true; }
+ for (int i = 'a'; i <= 'z'; i++) { atext[i] = true; }
+ atext['!'] = true;
+ atext['#'] = true;
+ atext['$'] = true;
+ atext['%'] = true;
+ atext['&'] = true;
+ atext['\''] = true;
+ atext['*'] = true;
+ atext['+'] = true;
+ atext['-'] = true;
+ atext['/'] = true;
+ atext['='] = true;
+ atext['?'] = true;
+ atext['^'] = true;
+ atext['_'] = true;
+ atext['`'] = true;
+ atext['{'] = true;
+ atext['|'] = true;
+ atext['}'] = true;
+ atext['~'] = true;
+ return atext;
+ }
+
+ private static bool[] CreateCharactersAllowedInQuotedStrings()
+ {
+ // fqtext = %d1-9 / %d11 / %d12 / %d14-33 / %d35-91 / %d93-127
+ var qtext = new bool[128];
+ for (int i = 1; i <= 9; i++) { qtext[i] = true; }
+ qtext[11] = true;
+ qtext[12] = true;
+ for (int i = 14; i <= 33; i++) { qtext[i] = true; }
+ for (int i = 35; i <= 91; i++) { qtext[i] = true; }
+ for (int i = 93; i <= 127; i++) { qtext[i] = true; }
+ return qtext;
+ }
+
+ private static bool[] CreateCharactersAllowedInDomainLiterals()
+ {
+ // fdtext = %d1-8 / %d11 / %d12 / %d14-31 / %d33-90 / %d94-127
+ var dtext = new bool[128];
+ for (int i = 1; i <= 8; i++) { dtext[i] = true; }
+ dtext[11] = true;
+ dtext[12] = true;
+ for (int i = 14; i <= 31; i++) { dtext[i] = true; }
+ for (int i = 33; i <= 90; i++) { dtext[i] = true; }
+ for (int i = 94; i <= 127; i++) { dtext[i] = true; }
+ return dtext;
+ }
+
+ private static bool[] CreateCharactersAllowedInHeaderNames()
+ {
+ // ftext = %d33-57 / %d59-126
+ var ftext = new bool[128];
+ for (int i = 33; i <= 57; i++) { ftext[i] = true; }
+ for (int i = 59; i <= 126; i++) { ftext[i] = true; }
+ return ftext;
+ }
+
+ private static bool[] CreateCharactersAllowedInTokens()
+ {
+ // ttext = %d33-126 except '()<>@,;:\"/[]?='
+ var ttext = new bool[128];
+ for (int i = 33; i <= 126; i++) { ttext[i] = true; }
+ ttext['('] = false;
+ ttext[')'] = false;
+ ttext['<'] = false;
+ ttext['>'] = false;
+ ttext['@'] = false;
+ ttext[','] = false;
+ ttext[';'] = false;
+ ttext[':'] = false;
+ ttext['\\'] = false;
+ ttext['"'] = false;
+ ttext['/'] = false;
+ ttext['['] = false;
+ ttext[']'] = false;
+ ttext['?'] = false;
+ ttext['='] = false;
+ return ttext;
+ }
+
+ private static bool[] CreateCharactersAllowedInComments()
+ {
+ // ctext- %d1-8 / %d11 / %d12 / %d14-31 / %33-39 / %42-91 / %93-127
+ var ctext = new bool[128];
+ for (int i = 1; i <= 8; i++) { ctext[i] = true; }
+ ctext[11] = true;
+ ctext[12] = true;
+ for (int i = 14; i <= 31; i++) { ctext[i] = true; }
+ for (int i = 33; i <= 39; i++) { ctext[i] = true; }
+ for (int i = 42; i <= 91; i++) { ctext[i] = true; }
+ for (int i = 93; i <= 127; i++) { ctext[i] = true; }
+ return ctext;
+ }
+
+ internal static bool SkipCFWS(string data, ref int offset)
+ {
+ int comments = 0;
+ for (; offset < data.Length; offset++)
+ {
+ if (data[offset] > 127)
+ throw new FormatException(SR.Format(SR.MailHeaderFieldInvalidCharacter, data[offset]));
+ else if (data[offset] == '\\' && comments > 0)
+ offset += 2;
+ else if (data[offset] == '(')
+ comments++;
+ else if (data[offset] == ')')
+ comments--;
+ else if (data[offset] != ' ' && data[offset] != '\t' && comments == 0)
+ return true;
+
+ if (comments < 0)
+ {
+ throw new FormatException(SR.Format(SR.MailHeaderFieldInvalidCharacter, data[offset]));
+ }
+ }
+
+ //returns false if end of string
+ return false;
+ }
+
+ internal static void ValidateHeaderName(string data)
+ {
+ int offset = 0;
+ for (; offset < data.Length; offset++)
+ {
+ if (data[offset] > Ftext.Length || !Ftext[data[offset]])
+ throw new FormatException(SR.InvalidHeaderName);
+ }
+ if (offset == 0)
+ throw new FormatException(SR.InvalidHeaderName);
+ }
+
+ internal static string ReadQuotedString(string data, ref int offset, StringBuilder builder)
+ {
+ return ReadQuotedString(data, ref offset, builder, false, false);
+ }
+
+ internal static string ReadQuotedString(string data, ref int offset, StringBuilder builder, bool doesntRequireQuotes, bool permitUnicodeInDisplayName)
+ {
+ // assume first char is the opening quote
+ if (!doesntRequireQuotes)
+ {
+ ++offset;
+ }
+ int start = offset;
+ StringBuilder localBuilder = (builder != null ? builder : new StringBuilder());
+ for (; offset < data.Length; offset++)
+ {
+ if (data[offset] == '\\')
+ {
+ localBuilder.Append(data, start, offset - start);
+ start = ++offset;
+ }
+ else if (data[offset] == '"')
+ {
+ localBuilder.Append(data, start, offset - start);
+ offset++;
+ return (builder != null ? null : localBuilder.ToString());
+ }
+ else if (data[offset] == '=' &&
+ data.Length > offset + 3 &&
+ data[offset + 1] == '\r' &&
+ data[offset + 2] == '\n' &&
+ (data[offset + 3] == ' ' || data[offset + 3] == '\t'))
+ {
+ //it's a soft crlf so it's ok
+ offset += 3;
+ }
+ else if (permitUnicodeInDisplayName)
+ {
+ //if data contains Unicode and Unicode is permitted, then
+ //it is valid in a quoted string in a header.
+ if (data[offset] <= Ascii7bitMaxValue && !Qtext[data[offset]])
+ throw new FormatException(SR.Format(SR.MailHeaderFieldInvalidCharacter, data[offset]));
+ }
+ //not permitting Unicode, in which case Unicode is a formatting error
+ else if (data[offset] > Ascii7bitMaxValue || !Qtext[data[offset]])
+ {
+ throw new FormatException(SR.Format(SR.MailHeaderFieldInvalidCharacter, data[offset]));
+ }
+ }
+ if (doesntRequireQuotes)
+ {
+ localBuilder.Append(data, start, offset - start);
+ return (builder != null ? null : localBuilder.ToString());
+ }
+ throw new FormatException(SR.MailHeaderFieldMalformedHeader);
+ }
+
+ internal static string ReadParameterAttribute(string data, ref int offset, StringBuilder builder)
+ {
+ if (!SkipCFWS(data, ref offset))
+ return null; //
+
+ return ReadToken(data, ref offset, null);
+ }
+
+ internal static string ReadToken(string data, ref int offset, StringBuilder builder)
+ {
+ int start = offset;
+ for (; offset < data.Length; offset++)
+ {
+ if (data[offset] > Ascii7bitMaxValue)
+ {
+ throw new FormatException(SR.Format(SR.MailHeaderFieldInvalidCharacter, data[offset]));
+ }
+ else if (!Ttext[data[offset]])
+ {
+ break;
+ }
+ }
+
+ if (start == offset)
+ {
+ throw new FormatException(SR.Format(SR.MailHeaderFieldInvalidCharacter, data[offset]));
+ }
+
+ return data.Substring(start, offset - start);
+ }
+
+ private static string[] s_months = new string[] { null, "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
+
+ internal static string GetDateTimeString(DateTime value, StringBuilder builder)
+ {
+ StringBuilder localBuilder = (builder != null ? builder : new StringBuilder());
+ localBuilder.Append(value.Day);
+ localBuilder.Append(' ');
+ localBuilder.Append(s_months[value.Month]);
+ localBuilder.Append(' ');
+ localBuilder.Append(value.Year);
+ localBuilder.Append(' ');
+ if (value.Hour <= 9)
+ {
+ localBuilder.Append('0');
+ }
+ localBuilder.Append(value.Hour);
+ localBuilder.Append(':');
+ if (value.Minute <= 9)
+ {
+ localBuilder.Append('0');
+ }
+ localBuilder.Append(value.Minute);
+ localBuilder.Append(':');
+ if (value.Second <= 9)
+ {
+ localBuilder.Append('0');
+ }
+ localBuilder.Append(value.Second);
+
+ string offset = TimeZoneInfo.Local.GetUtcOffset(value).ToString();
+ if (offset[0] != '-')
+ {
+ localBuilder.Append(" +");
+ }
+ else
+ {
+ localBuilder.Append(' ');
+ }
+
+ string[] offsetFields = offset.Split(':');
+ localBuilder.Append(offsetFields[0]);
+ localBuilder.Append(offsetFields[1]);
+ return (builder != null ? null : localBuilder.ToString());
+ }
+
+ internal static void GetTokenOrQuotedString(string data, StringBuilder builder, bool allowUnicode)
+ {
+ int offset = 0, start = 0;
+ for (; offset < data.Length; offset++)
+ {
+ if (CheckForUnicode(data[offset], allowUnicode))
+ {
+ continue;
+ }
+
+ if (!Ttext[data[offset]] || data[offset] == ' ')
+ {
+ builder.Append('"');
+ for (; offset < data.Length; offset++)
+ {
+ if (CheckForUnicode(data[offset], allowUnicode))
+ {
+ continue;
+ }
+ else if (IsFWSAt(data, offset)) // Allow FWS == "\r\n "
+ {
+ // No-op, skip these three chars
+ offset++;
+ offset++;
+ }
+ else if (!Qtext[data[offset]])
+ {
+ builder.Append(data, start, offset - start);
+ builder.Append('\\');
+ start = offset;
+ }
+ }
+ builder.Append(data, start, offset - start);
+ builder.Append('"');
+ return;
+ }
+ }
+
+ //always a quoted string if it was empty.
+ if (data.Length == 0)
+ {
+ builder.Append("\"\"");
+ }
+ // Token, no quotes needed
+ builder.Append(data);
+ }
+
+ private static bool CheckForUnicode(char ch, bool allowUnicode)
+ {
+ if (ch < Ascii7bitMaxValue)
+ {
+ return false;
+ }
+
+ if (!allowUnicode)
+ {
+ throw new FormatException(SR.Format(SR.MailHeaderFieldInvalidCharacter, ch));
+ }
+ return true;
+ }
+
+ internal static bool IsAllowedWhiteSpace(char c)
+ {
+ // all allowed whitespace characters
+ return c == Tab || c == Space || c == CR || c == LF;
+ }
+
+ internal static bool HasCROrLF(string data)
+ {
+ for (int i = 0; i < data.Length; i++)
+ {
+ if (data[i] == '\r' || data[i] == '\n')
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Is there a FWS ("\r\n " or "\r\n\t") starting at the given index?
+ internal static bool IsFWSAt(string data, int index)
+ {
+ Debug.Assert(index >= 0);
+ Debug.Assert(index < data.Length);
+
+ return (data[index] == MailBnfHelper.CR
+ && index + 2 < data.Length
+ && data[index + 1] == MailBnfHelper.LF
+ && (data[index + 2] == MailBnfHelper.Space
+ || data[index + 2] == MailBnfHelper.Tab));
+ }
+ }
+}
diff --git a/src/System.Net.Http/src/Internal/Mail/QuotedPairReader.cs b/src/Common/src/System/Net/Mail/QuotedPairReader.cs
index 8aea19a5a7..8aea19a5a7 100644
--- a/src/System.Net.Http/src/Internal/Mail/QuotedPairReader.cs
+++ b/src/Common/src/System/Net/Mail/QuotedPairReader.cs
diff --git a/src/System.Net.Http/src/Internal/Mail/QuotedStringFormatReader.cs b/src/Common/src/System/Net/Mail/QuotedStringFormatReader.cs
index 200914b5b5..200914b5b5 100644
--- a/src/System.Net.Http/src/Internal/Mail/QuotedStringFormatReader.cs
+++ b/src/Common/src/System/Net/Mail/QuotedStringFormatReader.cs
diff --git a/src/System.Net.Http/src/Internal/Mail/WhitespaceReader.cs b/src/Common/src/System/Net/Mail/WhitespaceReader.cs
index 11769b4a7a..11769b4a7a 100644
--- a/src/System.Net.Http/src/Internal/Mail/WhitespaceReader.cs
+++ b/src/Common/src/System/Net/Mail/WhitespaceReader.cs
diff --git a/src/System.Net.Http/src/Internal/Mail/MailBnfHelper.cs b/src/System.Net.Http/src/Internal/Mail/MailBnfHelper.cs
deleted file mode 100644
index f26fe32388..0000000000
--- a/src/System.Net.Http/src/Internal/Mail/MailBnfHelper.cs
+++ /dev/null
@@ -1,117 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-namespace System.Net.Mime
-{
- internal static class MailBnfHelper
- {
- // characters allowed in atoms
- internal static readonly bool[] Atext = CreateCharactersAllowedInAtoms();
-
- // characters allowed in quoted strings (not including Unicode)
- internal static readonly bool[] Qtext = CreateCharactersAllowedInQuotedStrings();
-
- // characters allowed in domain literals
- internal static readonly bool[] Dtext = CreateCharactersAllowedInDomainLiterals();
-
- // characters allowed inside of comments
- internal static readonly bool[] Ctext = CreateCharactersAllowedInComments();
-
- internal static readonly int Ascii7bitMaxValue = 127;
- internal static readonly char Quote = '\"';
- internal static readonly char Space = ' ';
- internal static readonly char Tab = '\t';
- internal static readonly char CR = '\r';
- internal static readonly char LF = '\n';
- internal static readonly char StartComment = '(';
- internal static readonly char EndComment = ')';
- internal static readonly char Backslash = '\\';
- internal static readonly char At = '@';
- internal static readonly char EndAngleBracket = '>';
- internal static readonly char StartAngleBracket = '<';
- internal static readonly char StartSquareBracket = '[';
- internal static readonly char EndSquareBracket = ']';
- internal static readonly char Comma = ',';
- internal static readonly char Dot = '.';
-
- // NOTE: See RFC 2822 for more detail. By default, every value in the array is false and only
- // those values which are allowed in that particular set are then set to true. The numbers
- // annotating each definition below are the range of ASCII values which are allowed in that definition.
-
- private static bool[] CreateCharactersAllowedInAtoms()
- {
- // atext = ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~"
- var atext = new bool[128];
- for (int i = '0'; i <= '9'; i++) { atext[i] = true; }
- for (int i = 'A'; i <= 'Z'; i++) { atext[i] = true; }
- for (int i = 'a'; i <= 'z'; i++) { atext[i] = true; }
- atext['!'] = true;
- atext['#'] = true;
- atext['$'] = true;
- atext['%'] = true;
- atext['&'] = true;
- atext['\''] = true;
- atext['*'] = true;
- atext['+'] = true;
- atext['-'] = true;
- atext['/'] = true;
- atext['='] = true;
- atext['?'] = true;
- atext['^'] = true;
- atext['_'] = true;
- atext['`'] = true;
- atext['{'] = true;
- atext['|'] = true;
- atext['}'] = true;
- atext['~'] = true;
- return atext;
- }
-
- private static bool[] CreateCharactersAllowedInQuotedStrings()
- {
- // fqtext = %d1-9 / %d11 / %d12 / %d14-33 / %d35-91 / %d93-127
- var qtext = new bool[128];
- for (int i = 1; i <= 9; i++) { qtext[i] = true; }
- qtext[11] = true;
- qtext[12] = true;
- for (int i = 14; i <= 33; i++) { qtext[i] = true; }
- for (int i = 35; i <= 91; i++) { qtext[i] = true; }
- for (int i = 93; i <= 127; i++) { qtext[i] = true; }
- return qtext;
- }
-
- private static bool[] CreateCharactersAllowedInDomainLiterals()
- {
- // fdtext = %d1-8 / %d11 / %d12 / %d14-31 / %d33-90 / %d94-127
- var dtext = new bool[128];
- for (int i = 1; i <= 8; i++) { dtext[i] = true; }
- dtext[11] = true;
- dtext[12] = true;
- for (int i = 14; i <= 31; i++) { dtext[i] = true; }
- for (int i = 33; i <= 90; i++) { dtext[i] = true; }
- for (int i = 94; i <= 127; i++) { dtext[i] = true; }
- return dtext;
- }
-
- private static bool[] CreateCharactersAllowedInComments()
- {
- // ctext- %d1-8 / %d11 / %d12 / %d14-31 / %33-39 / %42-91 / %93-127
- var ctext = new bool[128];
- for (int i = 1; i <= 8; i++) { ctext[i] = true; }
- ctext[11] = true;
- ctext[12] = true;
- for (int i = 14; i <= 31; i++) { ctext[i] = true; }
- for (int i = 33; i <= 39; i++) { ctext[i] = true; }
- for (int i = 42; i <= 91; i++) { ctext[i] = true; }
- for (int i = 93; i <= 127; i++) { ctext[i] = true; }
- return ctext;
- }
-
- internal static bool IsAllowedWhiteSpace(char c)
- {
- // all allowed whitespace characters
- return c == Tab || c == Space || c == CR || c == LF;
- }
- }
-}
diff --git a/src/System.Net.Http/src/System.Net.Http.csproj b/src/System.Net.Http/src/System.Net.Http.csproj
index f8440d54e3..f0433b88e6 100644
--- a/src/System.Net.Http/src/System.Net.Http.csproj
+++ b/src/System.Net.Http/src/System.Net.Http.csproj
@@ -39,7 +39,6 @@
<Compile Include="System\Net\Http\ByteArrayContent.cs" />
<Compile Include="System\Net\Http\ClientCertificateOption.cs" />
<Compile Include="System\Net\Http\DelegatingHandler.cs" />
- <Compile Include="System\Net\Http\DelegatingStream.cs" />
<Compile Include="System\Net\Http\FormUrlEncodedContent.cs" />
<Compile Include="System\Net\Http\HttpClient.cs" />
<Compile Include="System\Net\Http\HttpCompletionOption.cs" />
@@ -100,9 +99,33 @@
<Compile Include="System\Net\Http\Headers\UriHeaderParser.cs" />
<Compile Include="System\Net\Http\Headers\ViaHeaderValue.cs" />
<Compile Include="System\Net\Http\Headers\WarningHeaderValue.cs" />
- <!-- TODO #5715: Must be moved to the Common/System/Net folder -->
- <Compile Include="Internal\Mail\DotAtomReader.cs" />
- <Compile Include="Internal\Mail\WhitespaceReader.cs" />
+ <Compile Include="$(CommonPath)\System\Net\Mail\MailAddress.cs">
+ <Link>Common\System\Net\Mail\MailAddress.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)\System\Net\Mail\DomainLiteralReader.cs">
+ <Link>Common\System\Net\Mail\DomainLiteralReader.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)\System\Net\Mail\MailAddressParser.cs">
+ <Link>Common\System\Net\Mail\MailAddressParser.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)\System\Net\Mail\MailBnfHelper.cs">
+ <Link>Common\System\Net\Mail\MailBnfHelper.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)\System\Net\Mail\QuotedPairReader.cs">
+ <Link>Common\System\Net\Mail\QuotedPairReader.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)\System\Net\Mail\QuotedStringFormatReader.cs">
+ <Link>Common\System\Net\Mail\QuotedStringFormatReader.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)\System\Net\Mail\DotAtomReader.cs">
+ <Link>Common\System\Net\Mail\DotAtomReader.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)\System\Net\Mail\WhitespaceReader.cs">
+ <Link>Common\System\Net\Mail\WhitespaceReader.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)\System\IO\DelegatingStream.cs">
+ <Link>Common\System\IO\DelegatingStream.cs</Link>
+ </Compile>
<Compile Include="$(CommonPath)\System\Net\Logging\LoggingHash.cs">
<Link>Common\System\Net\Logging\LoggingHash.cs</Link>
</Compile>
@@ -119,12 +142,6 @@
<ItemGroup Condition=" '$(TargetsWindows)' != 'true' Or '$(TargetGroup)' != 'net46' ">
<!-- TODO #5715: Must be moved to the Common/System/Net folder -->
<Compile Include="Internal\ICloneable.cs" />
- <Compile Include="Internal\MailAddress.cs" />
- <Compile Include="Internal\Mail\DomainLiteralReader.cs" />
- <Compile Include="Internal\Mail\MailAddressParser.cs" />
- <Compile Include="Internal\Mail\MailBnfHelper.cs" />
- <Compile Include="Internal\Mail\QuotedPairReader.cs" />
- <Compile Include="Internal\Mail\QuotedStringFormatReader.cs" />
</ItemGroup>
<PropertyGroup Condition=" '$(TargetsWindows)' == 'true' And '$(TargetGroup)' == '' ">
<DefineConstants>$(DefineConstants);HTTP_DLL</DefineConstants>
diff --git a/src/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj b/src/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj
index 339d93edea..a27a7c9f02 100644
--- a/src/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj
+++ b/src/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj
@@ -59,29 +59,29 @@
<Compile Include="..\..\src\Internal\ICloneable.cs">
<Link>ProductionCode\Internal\ICloneable.cs</Link>
</Compile>
- <Compile Include="..\..\src\Internal\MailAddress.cs">
- <Link>ProductionCode\Internal\MailAddress.cs</Link>
+ <Compile Include="$(CommonPath)\System\Net\Mail\MailAddress.cs">
+ <Link>ProductionCode\Common\src\System\Net\Mail\MailAddress.cs</Link>
</Compile>
- <Compile Include="..\..\src\Internal\Mail\DomainLiteralReader.cs">
- <Link>ProductionCode\Internal\DomainLiteralReader.cs</Link>
+ <Compile Include="$(CommonPath)\System\Net\Mail\DomainLiteralReader.cs">
+ <Link>ProductionCode\Common\src\System\Net\Mail\DomainLiteralReader.cs</Link>
</Compile>
- <Compile Include="..\..\src\Internal\Mail\DotAtomReader.cs">
- <Link>ProductionCode\Internal\DotAtomReader.cs</Link>
+ <Compile Include="$(CommonPath)\System\Net\Mail\DotAtomReader.cs">
+ <Link>ProductionCode\Common\src\System\Net\Mail\DotAtomReader.cs</Link>
</Compile>
- <Compile Include="..\..\src\Internal\Mail\MailAddressParser.cs">
- <Link>ProductionCode\Internal\MailAddressParser.cs</Link>
+ <Compile Include="$(CommonPath)\System\Net\Mail\MailAddressParser.cs">
+ <Link>ProductionCode\Common\src\System\Net\Mail\MailAddressParser.cs</Link>
</Compile>
- <Compile Include="..\..\src\Internal\Mail\MailBnfHelper.cs">
- <Link>ProductionCode\Internal\MailBnfHelper.cs</Link>
+ <Compile Include="$(CommonPath)\System\Net\Mail\MailBnfHelper.cs">
+ <Link>ProductionCode\Common\src\System\Net\Mail\MailBnfHelper.cs</Link>
</Compile>
- <Compile Include="..\..\src\Internal\Mail\QuotedPairReader.cs">
- <Link>ProductionCode\Internal\QuotedPairReader.cs</Link>
+ <Compile Include="$(CommonPath)\System\Net\Mail\QuotedPairReader.cs">
+ <Link>ProductionCode\Common\src\System\Net\Mail\QuotedPairReader.cs</Link>
</Compile>
- <Compile Include="..\..\src\Internal\Mail\QuotedStringFormatReader.cs">
- <Link>ProductionCode\Internal\QuotedStringFormatReader.cs</Link>
+ <Compile Include="$(CommonPath)\System\Net\Mail\QuotedStringFormatReader.cs">
+ <Link>ProductionCode\Common\src\System\Net\Mail\QuotedStringFormatReader.cs</Link>
</Compile>
- <Compile Include="..\..\src\Internal\Mail\WhitespaceReader.cs">
- <Link>ProductionCode\Internal\WhitespaceReader.cs</Link>
+ <Compile Include="$(CommonPath)\System\Net\Mail\WhitespaceReader.cs">
+ <Link>ProductionCode\Common\src\System\Net\Mail\WhitespaceReader.cs</Link>
</Compile>
<Compile Include="..\..\src\Internal\UriHelper.cs">
<Link>ProductionCode\Internal\UriHelper.cs</Link>
@@ -98,8 +98,8 @@
<Compile Include="..\..\src\System\Net\Http\DelegatingHandler.cs">
<Link>ProductionCode\System\Net\Http\DelegatingHandler.cs</Link>
</Compile>
- <Compile Include="..\..\src\System\Net\Http\DelegatingStream.cs">
- <Link>ProductionCode\System\Net\Http\DelegatingStream.cs</Link>
+ <Compile Include="$(CommonPath)\System\IO\DelegatingStream.cs">
+ <Link>ProductionCode\System\IO\DelegatingStream.cs</Link>
</Compile>
<Compile Include="..\..\src\System\Net\Http\FormUrlEncodedContent.cs">
<Link>ProductionCode\System\Net\Http\FormUrlEncodedContent.cs</Link>
diff --git a/src/System.Net.Mime/System.Net.Mime.sln b/src/System.Net.Mime/System.Net.Mime.sln
new file mode 100644
index 0000000000..787e204033
--- /dev/null
+++ b/src/System.Net.Mime/System.Net.Mime.sln
@@ -0,0 +1,59 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 14
+VisualStudioVersion = 14.0.25420.1
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{350C956A-B4ED-4376-9B04-2908528D10FF}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Net.Mime", "ref\System.Net.Mime.csproj", "{14FAFC3A-8266-45C7-8604-8C2CB567E50D}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827B5CCD-073A-4A75-9661-73D54F0A3528}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Net.Mime", "src\System.Net.Mime.csproj", "{53D09AF4-0C13-4197-B8AD-9746F0374E88}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{096E7AD0-17A2-4EAF-9AC5-2F0FC67A4112}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Net.Mime.Tests", "tests\System.Net.Mime.Tests.csproj", "{0D1E2954-A5C7-4B8C-932A-31EB4A96A726}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ net46_Debug|Any CPU = net46_Debug|Any CPU
+ net46_Release|Any CPU = net46_Release|Any CPU
+ netstandard1.3_Debug|Any CPU = netstandard1.3_Debug|Any CPU
+ netstandard1.3_Release|Any CPU = netstandard1.3_Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {14FAFC3A-8266-45C7-8604-8C2CB567E50D}.net46_Debug|Any CPU.ActiveCfg = netstandard1.7_Release|Any CPU
+ {14FAFC3A-8266-45C7-8604-8C2CB567E50D}.net46_Debug|Any CPU.Build.0 = netstandard1.7_Release|Any CPU
+ {14FAFC3A-8266-45C7-8604-8C2CB567E50D}.net46_Release|Any CPU.ActiveCfg = netstandard1.7_Release|Any CPU
+ {14FAFC3A-8266-45C7-8604-8C2CB567E50D}.net46_Release|Any CPU.Build.0 = netstandard1.7_Release|Any CPU
+ {14FAFC3A-8266-45C7-8604-8C2CB567E50D}.netstandard1.3_Debug|Any CPU.ActiveCfg = netstandard1.7_Release|Any CPU
+ {14FAFC3A-8266-45C7-8604-8C2CB567E50D}.netstandard1.3_Debug|Any CPU.Build.0 = netstandard1.7_Release|Any CPU
+ {14FAFC3A-8266-45C7-8604-8C2CB567E50D}.netstandard1.3_Release|Any CPU.ActiveCfg = netstandard1.7_Release|Any CPU
+ {14FAFC3A-8266-45C7-8604-8C2CB567E50D}.netstandard1.3_Release|Any CPU.Build.0 = netstandard1.7_Release|Any CPU
+ {53D09AF4-0C13-4197-B8AD-9746F0374E88}.net46_Debug|Any CPU.ActiveCfg = net463_Release|Any CPU
+ {53D09AF4-0C13-4197-B8AD-9746F0374E88}.net46_Debug|Any CPU.Build.0 = net463_Release|Any CPU
+ {53D09AF4-0C13-4197-B8AD-9746F0374E88}.net46_Release|Any CPU.ActiveCfg = net463_Release|Any CPU
+ {53D09AF4-0C13-4197-B8AD-9746F0374E88}.net46_Release|Any CPU.Build.0 = net463_Release|Any CPU
+ {53D09AF4-0C13-4197-B8AD-9746F0374E88}.netstandard1.3_Debug|Any CPU.ActiveCfg = netstandard1.3_Debug|Any CPU
+ {53D09AF4-0C13-4197-B8AD-9746F0374E88}.netstandard1.3_Debug|Any CPU.Build.0 = netstandard1.3_Debug|Any CPU
+ {53D09AF4-0C13-4197-B8AD-9746F0374E88}.netstandard1.3_Release|Any CPU.ActiveCfg = netstandard1.3_Release|Any CPU
+ {53D09AF4-0C13-4197-B8AD-9746F0374E88}.netstandard1.3_Release|Any CPU.Build.0 = netstandard1.3_Release|Any CPU
+ {0D1E2954-A5C7-4B8C-932A-31EB4A96A726}.net46_Debug|Any CPU.ActiveCfg = netstandard1.7_Release|Any CPU
+ {0D1E2954-A5C7-4B8C-932A-31EB4A96A726}.net46_Debug|Any CPU.Build.0 = netstandard1.7_Release|Any CPU
+ {0D1E2954-A5C7-4B8C-932A-31EB4A96A726}.net46_Release|Any CPU.ActiveCfg = netstandard1.7_Release|Any CPU
+ {0D1E2954-A5C7-4B8C-932A-31EB4A96A726}.net46_Release|Any CPU.Build.0 = netstandard1.7_Release|Any CPU
+ {0D1E2954-A5C7-4B8C-932A-31EB4A96A726}.netstandard1.3_Debug|Any CPU.ActiveCfg = netstandard1.7_Release|Any CPU
+ {0D1E2954-A5C7-4B8C-932A-31EB4A96A726}.netstandard1.3_Debug|Any CPU.Build.0 = netstandard1.7_Release|Any CPU
+ {0D1E2954-A5C7-4B8C-932A-31EB4A96A726}.netstandard1.3_Release|Any CPU.ActiveCfg = netstandard1.7_Release|Any CPU
+ {0D1E2954-A5C7-4B8C-932A-31EB4A96A726}.netstandard1.3_Release|Any CPU.Build.0 = netstandard1.7_Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {14FAFC3A-8266-45C7-8604-8C2CB567E50D} = {350C956A-B4ED-4376-9B04-2908528D10FF}
+ {53D09AF4-0C13-4197-B8AD-9746F0374E88} = {827B5CCD-073A-4A75-9661-73D54F0A3528}
+ {0D1E2954-A5C7-4B8C-932A-31EB4A96A726} = {096E7AD0-17A2-4EAF-9AC5-2F0FC67A4112}
+ EndGlobalSection
+EndGlobal
diff --git a/src/System.Net.Mime/dir.props b/src/System.Net.Mime/dir.props
new file mode 100644
index 0000000000..e58893f6ab
--- /dev/null
+++ b/src/System.Net.Mime/dir.props
@@ -0,0 +1,7 @@
+<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="..\dir.props" />
+ <PropertyGroup>
+ <AssemblyVersion>4.0.0.0</AssemblyVersion>
+ </PropertyGroup>
+</Project>
+
diff --git a/src/System.Net.Mime/pkg/System.Net.Mime.builds b/src/System.Net.Mime/pkg/System.Net.Mime.builds
new file mode 100644
index 0000000000..98647725d8
--- /dev/null
+++ b/src/System.Net.Mime/pkg/System.Net.Mime.builds
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.props))\dir.props" />
+ <ItemGroup>
+ <Project Include="System.Net.Mime.pkgproj"/>
+ </ItemGroup>
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.traversal.targets))\dir.traversal.targets" />
+</Project>
diff --git a/src/System.Net.Mime/pkg/System.Net.Mime.pkgproj b/src/System.Net.Mime/pkg/System.Net.Mime.pkgproj
new file mode 100644
index 0000000000..1dc2e4c2dd
--- /dev/null
+++ b/src/System.Net.Mime/pkg/System.Net.Mime.pkgproj
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.props))\dir.props" />
+
+ <ItemGroup>
+ <ProjectReference Include="..\ref\System.Net.Mime.csproj">
+ <SupportedFramework>net463;netcoreapp1.1;$(AllXamarinFrameworks)</SupportedFramework>
+ </ProjectReference>
+ <ProjectReference Include="..\src\System.Net.Mime.builds" />
+ </ItemGroup>
+
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.targets))\dir.targets" />
+</Project>
diff --git a/src/System.Net.Mime/ref/System.Net.Mime.cs b/src/System.Net.Mime/ref/System.Net.Mime.cs
new file mode 100644
index 0000000000..79442402e3
--- /dev/null
+++ b/src/System.Net.Mime/ref/System.Net.Mime.cs
@@ -0,0 +1,78 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// ------------------------------------------------------------------------------
+// Changes to this file must follow the http://aka.ms/api-review process.
+// ------------------------------------------------------------------------------
+
+namespace System.Net.Mime
+{
+ public class ContentDisposition
+ {
+ public ContentDisposition() { }
+ public ContentDisposition(string disposition) { }
+ public string DispositionType { get { return default(string); } set { } }
+ public System.Collections.Specialized.StringDictionary Parameters { get { return default(System.Collections.Specialized.StringDictionary); } }
+ public string FileName { get { return default(string); } set { } }
+ public DateTime CreationDate { get { return default(DateTime); } set { } }
+ public DateTime ModificationDate { get { return default(DateTime); } set { } }
+ public bool Inline { get { return default(bool); } set { } }
+ public DateTime ReadDate { get { return default(DateTime); } set { } }
+ public long Size { get { return default(long); } set { } }
+ public override string ToString() { return default(string); }
+ public override bool Equals(object rparam) { return default(bool); }
+ public override int GetHashCode() { return default(int); }
+ }
+ public class ContentType
+ {
+ public ContentType() { }
+ public ContentType(string contentType) { }
+ public string Boundary { get { return default(string); } set { } }
+ public string CharSet { get { return default(string); } set { } }
+ public string MediaType { get { return default(string); } set { } }
+ public string Name { get { return default(string); } set { } }
+ public System.Collections.Specialized.StringDictionary Parameters { get { return default(System.Collections.Specialized.StringDictionary); } }
+ public override string ToString() { return default(string); }
+ public override bool Equals(object rparam) { return default(bool); }
+ public override int GetHashCode() { return default(int); }
+ }
+ public static class DispositionTypeNames
+ {
+ public const string Inline = "inline";
+ public const string Attachment = "attachment";
+ }
+ public static class MediaTypeNames
+ {
+ public static class Text
+ {
+ public const string Plain = "text/plain";
+ public const string Html = "text/html";
+ public const string Xml = "text/xml";
+ public const string RichText = "text/richtext";
+ }
+
+ public static class Application
+ {
+ public const string Soap = "application/soap+xml";
+ public const string Octet = "application/octet-stream";
+ public const string Rtf = "application/rtf";
+ public const string Pdf = "application/pdf";
+ public const string Zip = "application/zip";
+ }
+
+ public static class Image
+ {
+ public const string Gif = "image/gif";
+ public const string Tiff = "image/tiff";
+ public const string Jpeg = "image/jpeg";
+ }
+ }
+ public enum TransferEncoding
+ {
+ Unknown = -1,
+ QuotedPrintable = 0,
+ Base64 = 1,
+ SevenBit = 2,
+ EightBit = 3,
+ }
+}
diff --git a/src/System.Net.Mime/ref/System.Net.Mime.csproj b/src/System.Net.Mime/ref/System.Net.Mime.csproj
new file mode 100644
index 0000000000..161ee07032
--- /dev/null
+++ b/src/System.Net.Mime/ref/System.Net.Mime.csproj
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.props))\dir.props" />
+ <PropertyGroup>
+ <OutputType>Library</OutputType>
+ <NuGetTargetMoniker>.NETStandard,Version=v1.7</NuGetTargetMoniker>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'netstandard1.7_Debug|AnyCPU'" />
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'netstandard1.7_Release|AnyCPU'" />
+ <ItemGroup>
+ <Compile Include="System.Net.Mime.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="project.json" />
+ </ItemGroup>
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.targets))\dir.targets" />
+</Project>
diff --git a/src/System.Net.Mime/ref/project.json b/src/System.Net.Mime/ref/project.json
new file mode 100644
index 0000000000..884405d76d
--- /dev/null
+++ b/src/System.Net.Mime/ref/project.json
@@ -0,0 +1,15 @@
+{
+ "dependencies": {
+ "System.Collections.Specialized": "4.0.0",
+ "System.IO": "4.1.0",
+ "System.Runtime": "4.1.0",
+ "System.Text.Encoding": "4.0.11"
+ },
+ "frameworks": {
+ "netstandard1.3": {
+ "imports": [
+ "dotnet5.2"
+ ]
+ }
+ }
+}
diff --git a/src/System.Net.Mime/src/Resources/Strings.resx b/src/System.Net.Mime/src/Resources/Strings.resx
new file mode 100644
index 0000000000..e2697d1912
--- /dev/null
+++ b/src/System.Net.Mime/src/Resources/Strings.resx
@@ -0,0 +1,172 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+
+ <data name="net_emptystringcall" xml:space="preserve">
+ <value>The parameter '{0}' cannot be an empty string.</value>
+ </data>
+ <data name="net_io_invalidasyncresult" xml:space="preserve">
+ <value>The IAsyncResult object was not returned from the corresponding asynchronous method on this class.</value>
+ </data>
+ <data name="net_io_invalidendcall" xml:space="preserve">
+ <value>{0} can only be called once for each asynchronous operation.</value>
+ </data>
+ <data name="net_emptystringset" xml:space="preserve">
+ <value>This property cannot be set to an empty string.</value>
+ </data>
+ <data name="MailBase64InvalidCharacter" xml:space="preserve">
+ <value>An invalid character was found in the Base-64 stream.</value>
+ </data>
+ <data name="MailCollectionIsReadOnly" xml:space="preserve">
+ <value>The collection is read-only.</value>
+ </data>
+ <data name="MailDateInvalidFormat" xml:space="preserve">
+ <value>The date is in an invalid format.</value>
+ </data>
+ <data name="MailHeaderFieldInvalidCharacter" xml:space="preserve">
+ <value>An invalid character was found in the mail header: '{0}'.</value>
+ </data>
+ <data name="MailHeaderFieldMalformedHeader" xml:space="preserve">
+ <value>The mail header is malformed.</value>
+ </data>
+ <data name="MailWriterIsInContent" xml:space="preserve">
+ <value>This operation cannot be performed while in content.</value>
+ </data>
+ <data name="MimeTransferEncodingNotSupported" xml:space="preserve">
+ <value>The MIME transfer encoding '{0}' is not supported.</value>
+ </data>
+ <data name="InvalidHexDigit" xml:space="preserve">
+ <value>Invalid hex digit '{0}'.</value>
+ </data>
+ <data name="InvalidHeaderName" xml:space="preserve">
+ <value>An invalid character was found in header name.</value>
+ </data>
+ <data name="ContentTypeInvalid" xml:space="preserve">
+ <value>The specified content type is invalid.</value>
+ </data>
+ <data name="ContentDispositionInvalid" xml:space="preserve">
+ <value>The specified content disposition is invalid.</value>
+ </data>
+ <data name="MimePartCantResetStream" xml:space="preserve">
+ <value>One of the streams has already been used and can't be reset to the origin.</value>
+ </data>
+ <data name="MediaTypeInvalid" xml:space="preserve">
+ <value>The specified media type is invalid.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/System.Net.Mime/src/System.Net.Mime.builds b/src/System.Net.Mime/src/System.Net.Mime.builds
new file mode 100644
index 0000000000..614c43b45a
--- /dev/null
+++ b/src/System.Net.Mime/src/System.Net.Mime.builds
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.props))\dir.props" />
+ <ItemGroup>
+ <Project Include="System.Net.Mime.csproj" />
+ <Project Include="System.Net.Mime.csproj">
+ <TargetGroup>net463</TargetGroup>
+ </Project>
+ </ItemGroup>
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.traversal.targets))\dir.traversal.targets" />
+</Project>
+
diff --git a/src/System.Net.Mime/src/System.Net.Mime.csproj b/src/System.Net.Mime/src/System.Net.Mime.csproj
new file mode 100644
index 0000000000..ad444f48bb
--- /dev/null
+++ b/src/System.Net.Mime/src/System.Net.Mime.csproj
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Build">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.props))\dir.props" />
+ <PropertyGroup>
+ <ProjectGuid>{53D09AF4-0C13-4197-B8AD-9746F0374E88}</ProjectGuid>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ <IsPartialFacadeAssembly Condition="'$(TargetGroup)'=='net463'">true</IsPartialFacadeAssembly>
+ <NuGetTargetMoniker Condition="'$(TargetGroup)' == ''">.NETStandard,Version=v1.7</NuGetTargetMoniker>
+ </PropertyGroup>
+ <!-- Default configurations to help VS understand the options -->
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'netstandard1.3_Debug|AnyCPU'" />
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'netstandard1.3_Release|AnyCPU'" />
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'net463_Debug|AnyCPU'" />
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'net463_Release|AnyCPU'" />
+ <ItemGroup Condition="'$(TargetGroup)' != 'net463'">
+ <Compile Include="System\Net\Mime\Base64Stream.cs" />
+ <Compile Include="System\Net\Mime\MimePart.cs" />
+ <Compile Include="System\Net\Mime\Base64WriteStateInfo.cs" />
+ <Compile Include="System\Net\Mime\QuotedPrintableStream.cs" />
+ <Compile Include="System\Net\Mime\CloseableStream.cs" />
+ <Compile Include="System\Net\Mime\EightBitStream.cs" />
+ <Compile Include="System\Net\Mime\EncodedStreamFactory.cs" />
+ <Compile Include="System\Net\Mime\IEncodableStream.cs" />
+ <Compile Include="System\Net\Mime\QEncodedStream.cs" />
+ <Compile Include="System\Net\Mime\WriteStateInfoBase.cs" />
+ <Compile Include="System\Net\Mime\BaseWriter.cs" />
+ <Compile Include="System\Net\Mime\TransferEncoding.cs" />
+ <Compile Include="System\Net\Mime\ContentDisposition.cs" />
+ <Compile Include="System\Net\Mime\ContentType.cs" />
+ <Compile Include="System\Net\Mime\DispositionTypeNames.cs" />
+ <Compile Include="System\Net\Mime\HeaderCollection.cs" />
+ <Compile Include="System\Net\Mime\MediaTypeNames.cs" />
+ <Compile Include="System\Net\Mime\MimeBasePart.cs" />
+ <Compile Include="System\Net\Mime\SmtpDateTime.cs" />
+ <Compile Include="System\Net\Mime\MultiAsyncResult.cs" />
+ <Compile Include="System\Net\Mime\TrackingStringDictionary.cs" />
+ <Compile Include="System\Net\Mime\TrackingValidationObjectDictionary.cs" />
+ <Compile Include="System\Net\Mail\MailHeaderInfo.cs" />
+ <Compile Include="System\Net\Mail\BufferBuilder.cs" />
+ <Compile Include="$(CommonPath)\System\IO\DelegatingStream.cs">
+ <Link>Common\System\IO\DelegatingStream.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)\System\Net\InternalException.cs">
+ <Link>Common\System\Net\InternalException.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)\System\Net\LazyAsyncResult.cs">
+ <Link>Common\System\Net\LazyAsyncResult.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)\System\Net\Mail\MailBnfHelper.cs">
+ <Link>Common\System\Net\Mail\MailBnfHelper.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)\System\Net\Logging\LoggingHash.cs">
+ <Link>Common\System\Net\Logging\LoggingHash.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)\System\Net\Logging\GlobalLog.cs">
+ <Link>Common\System\Net\Logging\GlobalLog.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)\System\Net\Logging\EventSourceLogging.cs">
+ <Link>Common\System\Net\Logging\EventSourceLogging.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)\System\Net\Shims\DBNull.cs">
+ <Link>Common\System\Net\Shims\DBNull.cs</Link>
+ </Compile>
+ </ItemGroup>
+ <ItemGroup Condition="'$(TargetGroup)' == 'net463'">
+ <TargetingPackReference Include="System" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="project.json" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="System\Net\Mail\MailHeaderID.cs" />
+ </ItemGroup>
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.targets))\dir.targets" />
+</Project> \ No newline at end of file
diff --git a/src/System.Net.Mime/src/System/Net/Mail/BufferBuilder.cs b/src/System.Net.Mime/src/System/Net/Mail/BufferBuilder.cs
new file mode 100644
index 0000000000..9d6ffc0c86
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mail/BufferBuilder.cs
@@ -0,0 +1,95 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Text;
+
+namespace System.Net.Mail
+{
+ internal sealed class BufferBuilder
+ {
+ private byte[] _buffer;
+ private int _offset;
+
+ internal BufferBuilder() : this(256) { }
+ internal BufferBuilder(int initialSize)
+ {
+ _buffer = new byte[initialSize];
+ }
+
+ private void EnsureBuffer(int count)
+ {
+ if (count > _buffer.Length - _offset)
+ {
+ byte[] newBuffer = new byte[((_buffer.Length * 2) > (_buffer.Length + count)) ? (_buffer.Length * 2) : (_buffer.Length + count)];
+ Buffer.BlockCopy(_buffer, 0, newBuffer, 0, _offset);
+ _buffer = newBuffer;
+ }
+ }
+
+ internal void Append(byte value)
+ {
+ EnsureBuffer(1);
+ _buffer[_offset++] = value;
+ }
+
+ internal void Append(byte[] value)
+ {
+ Append(value, 0, value.Length);
+ }
+
+ internal void Append(byte[] value, int offset, int count)
+ {
+ EnsureBuffer(count);
+ Buffer.BlockCopy(value, offset, _buffer, _offset, count);
+ _offset += count;
+ }
+
+ internal void Append(string value)
+ {
+ Append(value, false);
+ }
+
+ internal void Append(string value, bool allowUnicode)
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ return;
+ }
+ Append(value, 0, value.Length, allowUnicode);
+ }
+
+ internal void Append(string value, int offset, int count, bool allowUnicode)
+ {
+ if (allowUnicode)
+ {
+ byte[] bytes = Encoding.UTF8.GetBytes(value.ToCharArray(), offset, count);
+ Append(bytes);
+ }
+ else
+ {
+ Append(value, offset, count);
+ }
+ }
+
+ // Does not allow unicode, only ANSI
+ internal void Append(string value, int offset, int count)
+ {
+ EnsureBuffer(count);
+ for (int i = 0; i < count; i++)
+ {
+ char c = value[offset + i];
+ if (c > 0xFF)
+ {
+ throw new FormatException(SR.Format(SR.MailHeaderFieldInvalidCharacter, c));
+ }
+ _buffer[_offset + i] = (byte)c;
+ }
+ _offset += count;
+ }
+
+ internal int Length => _offset;
+ internal byte[] GetBuffer() => _buffer;
+ internal void Reset() { _offset = 0; }
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mail/MailHeaderID.cs b/src/System.Net.Mime/src/System/Net/Mail/MailHeaderID.cs
new file mode 100644
index 0000000000..688c706eb0
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mail/MailHeaderID.cs
@@ -0,0 +1,47 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace System.Net.Mail
+{
+ // Enumeration of the well-known headers.
+ // If you add to this enum you MUST also add the appropriate initializer in MailHeaderInfo.s_headerInfo
+ internal enum MailHeaderID
+ {
+ Bcc = 0,
+ Cc,
+ Comments,
+ ContentDescription,
+ ContentDisposition,
+ ContentID,
+ ContentLocation,
+ ContentTransferEncoding,
+ ContentType,
+ Date,
+ From,
+ Importance,
+ InReplyTo,
+ Keywords,
+ Max,
+ MessageID,
+ MimeVersion,
+ Priority,
+ References,
+ ReplyTo,
+ ResentBcc,
+ ResentCc,
+ ResentDate,
+ ResentFrom,
+ ResentMessageID,
+ ResentSender,
+ ResentTo,
+ Sender,
+ Subject,
+ To,
+ XPriority,
+ XReceiver,
+ XSender,
+ ZMaxEnumValue = XSender, // Keep this to equal to the last "known" enum entry if you add to the end
+ Unknown = -1
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mail/MailHeaderInfo.cs b/src/System.Net.Mime/src/System/Net/Mail/MailHeaderInfo.cs
new file mode 100644
index 0000000000..96dfedb6da
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mail/MailHeaderInfo.cs
@@ -0,0 +1,147 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+
+namespace System.Net.Mail
+{
+ internal static class MailHeaderInfo
+ {
+ // Structure that wraps information about a single mail header
+ private struct HeaderInfo
+ {
+ public readonly string NormalizedName;
+ public readonly bool IsSingleton;
+ public readonly MailHeaderID ID;
+ public readonly bool IsUserSettable;
+ public readonly bool AllowsUnicode;
+
+ public HeaderInfo(MailHeaderID id, string name, bool isSingleton, bool isUserSettable, bool allowsUnicode)
+ {
+ ID = id;
+ NormalizedName = name;
+ IsSingleton = isSingleton;
+ IsUserSettable = isUserSettable;
+ AllowsUnicode = allowsUnicode;
+ }
+ }
+
+ // Table of well-known mail headers.
+ // Keep the initializers in sync with the enum above.
+ private static readonly HeaderInfo[] s_headerInfo = {
+ // ID NormalizedString IsSingleton IsUserSettable AllowsUnicode
+ new HeaderInfo(MailHeaderID.Bcc, "Bcc", true, false, true),
+ new HeaderInfo(MailHeaderID.Cc, "Cc", true, false, true),
+ new HeaderInfo(MailHeaderID.Comments, "Comments", false, true, true),
+ new HeaderInfo(MailHeaderID.ContentDescription, "Content-Description", true, true, true),
+ new HeaderInfo(MailHeaderID.ContentDisposition, "Content-Disposition", true, true, true),
+ new HeaderInfo(MailHeaderID.ContentID, "Content-ID", true, false, false),
+ new HeaderInfo(MailHeaderID.ContentLocation, "Content-Location", true, false, true),
+ new HeaderInfo(MailHeaderID.ContentTransferEncoding, "Content-Transfer-Encoding", true, false, false),
+ new HeaderInfo(MailHeaderID.ContentType, "Content-Type", true, false, false),
+ new HeaderInfo(MailHeaderID.Date, "Date", true, false, false),
+ new HeaderInfo(MailHeaderID.From, "From", true, false, true),
+ new HeaderInfo(MailHeaderID.Importance, "Importance", true, false, false),
+ new HeaderInfo(MailHeaderID.InReplyTo, "In-Reply-To", true, true, false),
+ new HeaderInfo(MailHeaderID.Keywords, "Keywords", false, true, true),
+ new HeaderInfo(MailHeaderID.Max, "Max", false, true, false),
+ new HeaderInfo(MailHeaderID.MessageID, "Message-ID", true, true, false),
+ new HeaderInfo(MailHeaderID.MimeVersion, "MIME-Version", true, false, false),
+ new HeaderInfo(MailHeaderID.Priority, "Priority", true, false, false),
+ new HeaderInfo(MailHeaderID.References, "References", true, true, false),
+ new HeaderInfo(MailHeaderID.ReplyTo, "Reply-To", true, false, true),
+ new HeaderInfo(MailHeaderID.ResentBcc, "Resent-Bcc", false, true, true),
+ new HeaderInfo(MailHeaderID.ResentCc, "Resent-Cc", false, true, true),
+ new HeaderInfo(MailHeaderID.ResentDate, "Resent-Date", false, true, false),
+ new HeaderInfo(MailHeaderID.ResentFrom, "Resent-From", false, true, true),
+ new HeaderInfo(MailHeaderID.ResentMessageID, "Resent-Message-ID", false, true, false),
+ new HeaderInfo(MailHeaderID.ResentSender, "Resent-Sender", false, true, true),
+ new HeaderInfo(MailHeaderID.ResentTo, "Resent-To", false, true, true),
+ new HeaderInfo(MailHeaderID.Sender, "Sender", true, false, true),
+ new HeaderInfo(MailHeaderID.Subject, "Subject", true, false, true),
+ new HeaderInfo(MailHeaderID.To, "To", true, false, true),
+ new HeaderInfo(MailHeaderID.XPriority, "X-Priority", true, false, false),
+ new HeaderInfo(MailHeaderID.XReceiver, "X-Receiver", false, true, true),
+ new HeaderInfo(MailHeaderID.XSender, "X-Sender", true, true, true)
+ };
+
+ private static readonly Dictionary<string, int> s_headerDictionary;
+
+ static MailHeaderInfo()
+ {
+#if DEBUG
+ // Check that enum and header info array are in sync
+ for (int i = 0; i < s_headerInfo.Length; i++)
+ {
+ if ((int)s_headerInfo[i].ID != i)
+ {
+ throw new Exception("Header info data structures are not in sync");
+ }
+ }
+#endif
+
+ // Create dictionary for string-to-enum lookup. Ordinal and IgnoreCase are intentional.
+ s_headerDictionary = new Dictionary<string, int>((int)MailHeaderID.ZMaxEnumValue + 1, StringComparer.OrdinalIgnoreCase);
+ for (int i = 0; i < s_headerInfo.Length; i++)
+ {
+ s_headerDictionary.Add(s_headerInfo[i].NormalizedName, i);
+ }
+ }
+
+ internal static string GetString(MailHeaderID id)
+ {
+ switch (id)
+ {
+ case MailHeaderID.Unknown:
+ case MailHeaderID.ZMaxEnumValue + 1:
+ return null;
+ default:
+ return s_headerInfo[(int)id].NormalizedName;
+ }
+ }
+
+ internal static MailHeaderID GetID(string name)
+ {
+ int id;
+ return s_headerDictionary.TryGetValue(name, out id) ? (MailHeaderID)id : MailHeaderID.Unknown;
+ }
+
+ internal static bool IsWellKnown(string name)
+ {
+ int dummy;
+ return s_headerDictionary.TryGetValue(name, out dummy);
+ }
+
+ internal static bool IsUserSettable(string name)
+ {
+ //values not in the list of well-known headers are always user-settable
+ int index;
+ return !s_headerDictionary.TryGetValue(name, out index) || s_headerInfo[index].IsUserSettable;
+ }
+
+ internal static bool IsSingleton(string name)
+ {
+ int index;
+ return s_headerDictionary.TryGetValue(name, out index) && s_headerInfo[index].IsSingleton;
+ }
+
+ internal static string NormalizeCase(string name)
+ {
+ int index;
+ return s_headerDictionary.TryGetValue(name, out index) ? s_headerInfo[index].NormalizedName : name;
+ }
+
+ internal static bool IsMatch(string name, MailHeaderID header)
+ {
+ int index;
+ return s_headerDictionary.TryGetValue(name, out index) && (MailHeaderID)index == header;
+ }
+
+ internal static bool AllowsUnicode(string name)
+ {
+ int index;
+ return !s_headerDictionary.TryGetValue(name, out index) || s_headerInfo[index].AllowsUnicode;
+ }
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mime/Base64Stream.cs b/src/System.Net.Mime/src/System/Net/Mime/Base64Stream.cs
new file mode 100644
index 0000000000..cbebe7238c
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mime/Base64Stream.cs
@@ -0,0 +1,586 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.IO;
+using System.Net.Mime;
+using System.Text;
+using System.Diagnostics;
+using System.Net.Http;
+
+namespace System.Net
+{
+ internal sealed class Base64Stream : DelegatingStream, IEncodableStream
+ {
+ private static readonly byte[] s_base64DecodeMap = new byte[] {
+ //0 1 2 3 4 5 6 7 8 9 A B C D E F
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 0
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 1
+ 255,255,255,255,255,255,255,255,255,255,255, 62,255,255,255, 63, // 2
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,255,255,255,255,255,255, // 3
+ 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 4
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,255,255,255,255,255, // 5
+ 255, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 6
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,255,255,255,255,255, // 7
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 8
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 9
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // A
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // B
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // C
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // D
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // E
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // F
+ };
+
+ private static readonly byte[] s_base64EncodeMap = new byte[] {
+ 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80,
+ 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99,100,101,102,
+ 103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,
+ 119,120,121,122, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 43, 47,
+ 61
+ };
+
+ private readonly int _lineLength;
+ private readonly Base64WriteStateInfo _writeState;
+ private ReadStateInfo _readState;
+
+ //the number of bytes needed to encode three bytes (see algorithm description in Encode method below)
+ private const int SizeOfBase64EncodedChar = 4;
+
+ //bytes with this value in the decode map are invalid
+ private const byte InvalidBase64Value = 255;
+
+ internal Base64Stream(Stream stream, Base64WriteStateInfo writeStateInfo) : base(stream)
+ {
+ _writeState = new Base64WriteStateInfo();
+ _lineLength = writeStateInfo.MaxLineLength;
+ }
+
+ internal Base64Stream(Stream stream, int lineLength) : base(stream)
+ {
+ _lineLength = lineLength;
+ _writeState = new Base64WriteStateInfo();
+ }
+
+ internal Base64Stream(Base64WriteStateInfo writeStateInfo) : base(new MemoryStream()) // TODO: added this... what should this be?
+ {
+ _lineLength = writeStateInfo.MaxLineLength;
+ _writeState = writeStateInfo;
+ }
+
+ private ReadStateInfo ReadState => _readState ?? (_readState = new ReadStateInfo());
+
+ internal Base64WriteStateInfo WriteState
+ {
+ get
+ {
+ Debug.Assert(_writeState != null, "_writeState was null");
+ return _writeState;
+ }
+ }
+
+ public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException(nameof(buffer));
+ }
+ if (offset < 0 || offset > buffer.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset));
+ }
+ if (offset + count > buffer.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
+ }
+
+ var result = new ReadAsyncResult(this, buffer, offset, count, callback, state);
+ result.Read();
+ return result;
+ }
+
+ public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException(nameof(buffer));
+ }
+ if (offset < 0 || offset > buffer.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset));
+ }
+ if (offset + count > buffer.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
+ }
+
+ var result = new WriteAsyncResult(this, buffer, offset, count, callback, state);
+ result.Write();
+ return result;
+ }
+
+
+ public override void Close()
+ {
+ if (_writeState != null && WriteState.Length > 0)
+ {
+ switch (WriteState.Padding)
+ {
+ case 1:
+ WriteState.Append(s_base64EncodeMap[WriteState.LastBits], s_base64EncodeMap[64]);
+ break;
+ case 2:
+ WriteState.Append(s_base64EncodeMap[WriteState.LastBits], s_base64EncodeMap[64], s_base64EncodeMap[64]);
+ break;
+ }
+ WriteState.Padding = 0;
+ FlushInternal();
+ }
+
+ base.Close();
+ }
+
+ public unsafe int DecodeBytes(byte[] buffer, int offset, int count)
+ {
+ fixed (byte* pBuffer = buffer)
+ {
+ byte* start = pBuffer + offset;
+ byte* source = start;
+ byte* dest = start;
+ byte* end = start + count;
+
+ while (source < end)
+ {
+ //space and tab are ok because folding must include a whitespace char.
+ if (*source == '\r' || *source == '\n' || *source == '=' || *source == ' ' || *source == '\t')
+ {
+ source++;
+ continue;
+ }
+
+ byte s = s_base64DecodeMap[*source];
+
+ if (s == InvalidBase64Value)
+ {
+ throw new FormatException(SR.MailBase64InvalidCharacter);
+ }
+
+ switch (ReadState.Pos)
+ {
+ case 0:
+ ReadState.Val = (byte)(s << 2);
+ ReadState.Pos++;
+ break;
+ case 1:
+ *dest++ = (byte)(ReadState.Val + (s >> 4));
+ ReadState.Val = (byte)(s << 4);
+ ReadState.Pos++;
+ break;
+ case 2:
+ *dest++ = (byte)(ReadState.Val + (s >> 2));
+ ReadState.Val = (byte)(s << 6);
+ ReadState.Pos++;
+ break;
+ case 3:
+ *dest++ = (byte)(ReadState.Val + s);
+ ReadState.Pos = 0;
+ break;
+ }
+ source++;
+ }
+
+ count = (int)(dest - start);
+ }
+
+ return count;
+ }
+
+ public int EncodeBytes(byte[] buffer, int offset, int count) =>
+ EncodeBytes(buffer, offset, count, true, true);
+
+ internal int EncodeBytes(byte[] buffer, int offset, int count, bool dontDeferFinalBytes, bool shouldAppendSpaceToCRLF)
+ {
+ Debug.Assert(buffer != null, "buffer was null");
+ Debug.Assert(_writeState != null, "writestate was null");
+ Debug.Assert(_writeState.Buffer != null, "writestate.buffer was null");
+
+ // Add Encoding header, if any. e.g. =?encoding?b?
+ WriteState.AppendHeader();
+
+ int cur = offset;
+ switch (WriteState.Padding)
+ {
+ case 2:
+ WriteState.Append(s_base64EncodeMap[WriteState.LastBits | ((buffer[cur] & 0xf0) >> 4)]);
+ if (count == 1)
+ {
+ WriteState.LastBits = (byte)((buffer[cur] & 0x0f) << 2);
+ WriteState.Padding = 1;
+ return cur - offset;
+ }
+ WriteState.Append(s_base64EncodeMap[((buffer[cur] & 0x0f) << 2) | ((buffer[cur + 1] & 0xc0) >> 6)]);
+ WriteState.Append(s_base64EncodeMap[(buffer[cur + 1] & 0x3f)]);
+ cur += 2;
+ count -= 2;
+ WriteState.Padding = 0;
+ break;
+ case 1:
+ WriteState.Append(s_base64EncodeMap[WriteState.LastBits | ((buffer[cur] & 0xc0) >> 6)]);
+ WriteState.Append(s_base64EncodeMap[(buffer[cur] & 0x3f)]);
+ cur++;
+ count--;
+ WriteState.Padding = 0;
+ break;
+ }
+
+ int calcLength = cur + (count - (count % 3));
+
+ // Convert three bytes at a time to base64 notation. This will output 4 chars.
+ for (; cur < calcLength; cur += 3)
+ {
+ if ((_lineLength != -1) && (WriteState.CurrentLineLength + SizeOfBase64EncodedChar + _writeState.FooterLength > _lineLength))
+ {
+ WriteState.AppendCRLF(shouldAppendSpaceToCRLF);
+ }
+
+ //how we actually encode: get three bytes in the
+ //buffer to be encoded. Then, extract six bits at a time and encode each six bit chunk as a base-64 character.
+ //this means that three bytes of data will be encoded as four base64 characters. It also means that to encode
+ //a character, we must have three bytes to encode so if the number of bytes is not divisible by three, we
+ //must pad the buffer (this happens below)
+ WriteState.Append(s_base64EncodeMap[(buffer[cur] & 0xfc) >> 2]);
+ WriteState.Append(s_base64EncodeMap[((buffer[cur] & 0x03) << 4) | ((buffer[cur + 1] & 0xf0) >> 4)]);
+ WriteState.Append(s_base64EncodeMap[((buffer[cur + 1] & 0x0f) << 2) | ((buffer[cur + 2] & 0xc0) >> 6)]);
+ WriteState.Append(s_base64EncodeMap[(buffer[cur + 2] & 0x3f)]);
+ }
+
+ cur = calcLength; //Where we left off before
+
+ // See if we need to fold before writing the last section (with possible padding)
+ if ((count % 3 != 0) && (_lineLength != -1) && (WriteState.CurrentLineLength + SizeOfBase64EncodedChar + _writeState.FooterLength >= _lineLength))
+ {
+ WriteState.AppendCRLF(shouldAppendSpaceToCRLF);
+ }
+
+ //now pad this thing if we need to. Since it must be a number of bytes that is evenly divisble by 3,
+ //if there are extra bytes, pad with '=' until we have a number of bytes divisible by 3
+ switch (count % 3)
+ {
+ case 2: //One character padding needed
+ WriteState.Append(s_base64EncodeMap[(buffer[cur] & 0xFC) >> 2]);
+ WriteState.Append(s_base64EncodeMap[((buffer[cur] & 0x03) << 4) | ((buffer[cur + 1] & 0xf0) >> 4)]);
+ if (dontDeferFinalBytes)
+ {
+ WriteState.Append(s_base64EncodeMap[((buffer[cur + 1] & 0x0f) << 2)]);
+ WriteState.Append(s_base64EncodeMap[64]);
+ WriteState.Padding = 0;
+ }
+ else
+ {
+ WriteState.LastBits = (byte)((buffer[cur + 1] & 0x0F) << 2);
+ WriteState.Padding = 1;
+ }
+ cur += 2;
+ break;
+
+ case 1: // Two character padding needed
+ WriteState.Append(s_base64EncodeMap[(buffer[cur] & 0xFC) >> 2]);
+ if (dontDeferFinalBytes)
+ {
+ WriteState.Append(s_base64EncodeMap[(byte)((buffer[cur] & 0x03) << 4)]);
+ WriteState.Append(s_base64EncodeMap[64]);
+ WriteState.Append(s_base64EncodeMap[64]);
+ WriteState.Padding = 0;
+ }
+ else
+ {
+ WriteState.LastBits = (byte)((buffer[cur] & 0x03) << 4);
+ WriteState.Padding = 2;
+ }
+ cur++;
+ break;
+ }
+
+ // Write out the last footer, if any. e.g. ?=
+ WriteState.AppendFooter();
+ return cur - offset;
+ }
+
+ public Stream GetStream() => this;
+
+ public string GetEncodedString() => Encoding.ASCII.GetString(WriteState.Buffer, 0, WriteState.Length);
+
+ public override int EndRead(IAsyncResult asyncResult)
+ {
+ if (asyncResult == null)
+ {
+ throw new ArgumentNullException(nameof(asyncResult));
+ }
+
+ return ReadAsyncResult.End(asyncResult);
+ }
+
+ public override void EndWrite(IAsyncResult asyncResult)
+ {
+ if (asyncResult == null)
+ {
+ throw new ArgumentNullException(nameof(asyncResult));
+ }
+
+ WriteAsyncResult.End(asyncResult);
+ }
+
+ public override void Flush()
+ {
+ if (_writeState != null && WriteState.Length > 0)
+ {
+ FlushInternal();
+ }
+
+ base.Flush();
+ }
+
+ private void FlushInternal()
+ {
+ base.Write(WriteState.Buffer, 0, WriteState.Length);
+ WriteState.Reset();
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException(nameof(buffer));
+ }
+ if (offset < 0 || offset > buffer.Length)
+ throw new ArgumentOutOfRangeException(nameof(offset));
+
+ if (offset + count > buffer.Length)
+ throw new ArgumentOutOfRangeException(nameof(count));
+
+ for (;;)
+ {
+ // read data from the underlying stream
+ int read = base.Read(buffer, offset, count);
+
+ // if the underlying stream returns 0 then there
+ // is no more data - ust return 0.
+ if (read == 0)
+ {
+ return 0;
+ }
+
+ // while decoding, we may end up not having
+ // any bytes to return pending additional data
+ // from the underlying stream.
+ read = DecodeBytes(buffer, offset, read);
+ if (read > 0)
+ {
+ return read;
+ }
+ }
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException(nameof(buffer));
+ }
+ if (offset < 0 || offset > buffer.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset));
+ }
+ if (offset + count > buffer.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
+ }
+
+ int written = 0;
+
+ // do not append a space when writing from a stream since this means
+ // it's writing the email body
+ for (;;)
+ {
+ written += EncodeBytes(buffer, offset + written, count - written, false, false);
+ if (written < count)
+ {
+ FlushInternal();
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+
+ private sealed class ReadAsyncResult : LazyAsyncResult
+ {
+ private readonly Base64Stream _parent;
+ private readonly byte[] _buffer;
+ private readonly int _offset;
+ private readonly int _count;
+ private int _read;
+
+ private static readonly AsyncCallback s_onRead = OnRead;
+
+ internal ReadAsyncResult(Base64Stream parent, byte[] buffer, int offset, int count, AsyncCallback callback, object state) : base(null, state, callback)
+ {
+ _parent = parent;
+ _buffer = buffer;
+ _offset = offset;
+ _count = count;
+ }
+
+ private bool CompleteRead(IAsyncResult result)
+ {
+ _read = _parent.BaseStream.EndRead(result);
+
+ // if the underlying stream returns 0 then there
+ // is no more data - ust return 0.
+ if (_read == 0)
+ {
+ InvokeCallback();
+ return true;
+ }
+
+ // while decoding, we may end up not having
+ // any bytes to return pending additional data
+ // from the underlying stream.
+ _read = _parent.DecodeBytes(_buffer, _offset, _read);
+ if (_read > 0)
+ {
+ InvokeCallback();
+ return true;
+ }
+
+ return false;
+ }
+
+ internal void Read()
+ {
+ for (;;)
+ {
+ IAsyncResult result = _parent.BaseStream.BeginRead(_buffer, _offset, _count, s_onRead, this);
+ if (!result.CompletedSynchronously || CompleteRead(result))
+ {
+ break;
+ }
+ }
+ }
+
+ private static void OnRead(IAsyncResult result)
+ {
+ if (!result.CompletedSynchronously)
+ {
+ ReadAsyncResult thisPtr = (ReadAsyncResult)result.AsyncState;
+ try
+ {
+ if (!thisPtr.CompleteRead(result))
+ {
+ thisPtr.Read();
+ }
+ }
+ catch (Exception e)
+ {
+ if (thisPtr.IsCompleted)
+ {
+ throw;
+ }
+ thisPtr.InvokeCallback(e);
+ }
+ }
+ }
+
+ internal static int End(IAsyncResult result)
+ {
+ ReadAsyncResult thisPtr = (ReadAsyncResult)result;
+ thisPtr.InternalWaitForCompletion();
+ return thisPtr._read;
+ }
+ }
+
+ private sealed class WriteAsyncResult : LazyAsyncResult
+ {
+ private readonly static AsyncCallback s_onWrite = OnWrite;
+
+ private readonly Base64Stream _parent;
+ private readonly byte[] _buffer;
+ private readonly int _offset;
+ private readonly int _count;
+ private int _written;
+
+ internal WriteAsyncResult(Base64Stream parent, byte[] buffer, int offset, int count, AsyncCallback callback, object state) : base(null, state, callback)
+ {
+ _parent = parent;
+ _buffer = buffer;
+ _offset = offset;
+ _count = count;
+ }
+
+ internal void Write()
+ {
+ for (;;)
+ {
+ // do not append a space when writing from a stream since this means
+ // it's writing the email body
+ _written += _parent.EncodeBytes(_buffer, _offset + _written, _count - _written, false, false);
+ if (_written < _count)
+ {
+ IAsyncResult result = _parent.BaseStream.BeginWrite(_parent.WriteState.Buffer, 0, _parent.WriteState.Length, s_onWrite, this);
+ if (!result.CompletedSynchronously)
+ {
+ break;
+ }
+ CompleteWrite(result);
+ }
+ else
+ {
+ InvokeCallback();
+ break;
+ }
+ }
+ }
+
+ private void CompleteWrite(IAsyncResult result)
+ {
+ _parent.BaseStream.EndWrite(result);
+ _parent.WriteState.Reset();
+ }
+
+ private static void OnWrite(IAsyncResult result)
+ {
+ if (!result.CompletedSynchronously)
+ {
+ WriteAsyncResult thisPtr = (WriteAsyncResult)result.AsyncState;
+ try
+ {
+ thisPtr.CompleteWrite(result);
+ thisPtr.Write();
+ }
+ catch (Exception e)
+ {
+ if (thisPtr.IsCompleted)
+ {
+ throw;
+ }
+ thisPtr.InvokeCallback(e);
+ }
+ }
+ }
+
+ internal static void End(IAsyncResult result)
+ {
+ WriteAsyncResult thisPtr = (WriteAsyncResult)result;
+ thisPtr.InternalWaitForCompletion();
+ Debug.Assert(thisPtr._written == thisPtr._count);
+ }
+ }
+
+ private sealed class ReadStateInfo
+ {
+ internal byte Val { get; set; }
+ internal byte Pos { get; set; }
+ }
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mime/Base64WriteStateInfo.cs b/src/System.Net.Mime/src/System/Net/Mime/Base64WriteStateInfo.cs
new file mode 100644
index 0000000000..169713276a
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mime/Base64WriteStateInfo.cs
@@ -0,0 +1,19 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace System.Net.Mime
+{
+ internal class Base64WriteStateInfo : WriteStateInfoBase
+ {
+ internal Base64WriteStateInfo() { }
+
+ internal Base64WriteStateInfo(int bufferSize, byte[] header, byte[] footer, int maxLineLength, int mimeHeaderLength) :
+ base(bufferSize, header, footer, maxLineLength, mimeHeaderLength)
+ {
+ }
+
+ internal int Padding { get; set; }
+ internal byte LastBits { get; set; }
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mime/BaseWriter.cs b/src/System.Net.Mime/src/System/Net/Mime/BaseWriter.cs
new file mode 100644
index 0000000000..3e5ecdc179
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mime/BaseWriter.cs
@@ -0,0 +1,209 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.IO;
+using System.Collections.Specialized;
+using System.Net.Mail;
+
+namespace System.Net.Mime
+{
+ internal abstract class BaseWriter
+ {
+ // This is the maximum default line length that can actually be written. When encoding
+ // headers, the line length is more conservative to account for things like folding.
+ // In MailWriter, all encoding has already been done so this will only fold lines
+ // that are NOT encoded already, which means being less conservative is ok.
+ private const int DefaultLineLength = 76;
+ private static readonly AsyncCallback s_onWrite = OnWrite;
+ protected static readonly byte[] s_crlf = new byte[] { (byte)'\r', (byte)'\n' };
+
+ protected readonly BufferBuilder _bufferBuilder;
+ protected readonly Stream _stream;
+ private readonly EventHandler _onCloseHandler;
+ private readonly bool _shouldEncodeLeadingDots;
+ private int _lineLength;
+ protected Stream _contentStream;
+ protected bool _isInContent;
+
+ protected BaseWriter(Stream stream, bool shouldEncodeLeadingDots)
+ {
+ if (stream == null)
+ {
+ throw new ArgumentNullException(nameof(stream));
+ }
+
+ _stream = stream;
+ _shouldEncodeLeadingDots = shouldEncodeLeadingDots;
+ _onCloseHandler = new EventHandler(OnClose);
+ _bufferBuilder = new BufferBuilder();
+ _lineLength = DefaultLineLength;
+ }
+
+ #region Headers
+
+ internal abstract void WriteHeaders(NameValueCollection headers, bool allowUnicode);
+
+ internal void WriteHeader(string name, string value, bool allowUnicode)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+ if (_isInContent)
+ {
+ throw new InvalidOperationException(SR.MailWriterIsInContent);
+ }
+
+ CheckBoundary();
+ _bufferBuilder.Append(name);
+ _bufferBuilder.Append(": ");
+ WriteAndFold(value, name.Length + 2, allowUnicode);
+ _bufferBuilder.Append(s_crlf);
+ }
+
+ private void WriteAndFold(string value, int charsAlreadyOnLine, bool allowUnicode)
+ {
+ int lastSpace = 0, startOfLine = 0;
+ for (int index = 0; index < value.Length; index++)
+ {
+ // When we find a FWS (CRLF) copy it as is.
+ if (MailBnfHelper.IsFWSAt(value, index)) // At the first char of "\r\n " or "\r\n\t"
+ {
+ index += 2; // Skip the FWS
+ _bufferBuilder.Append(value, startOfLine, index - startOfLine, allowUnicode);
+ // Reset for the next line
+ startOfLine = index;
+ lastSpace = index;
+ charsAlreadyOnLine = 0;
+ }
+ // When we pass the line length limit, and know where there was a space to fold at, fold there
+ else if (((index - startOfLine) > (_lineLength - charsAlreadyOnLine)) && lastSpace != startOfLine)
+ {
+ _bufferBuilder.Append(value, startOfLine, lastSpace - startOfLine, allowUnicode);
+ _bufferBuilder.Append(s_crlf);
+ startOfLine = lastSpace;
+ charsAlreadyOnLine = 0;
+ }
+ // Mark a foldable space. If we go over the line length limit, fold here.
+ else if (value[index] == MailBnfHelper.Space || value[index] == MailBnfHelper.Tab)
+ {
+ lastSpace = index;
+ }
+ }
+ // Write any remaining data to the buffer.
+ if (value.Length - startOfLine > 0)
+ {
+ _bufferBuilder.Append(value, startOfLine, value.Length - startOfLine, allowUnicode);
+ }
+ }
+
+ #endregion Headers
+
+ #region Content
+
+ internal Stream GetContentStream() => GetContentStream(null);
+
+ private Stream GetContentStream(MultiAsyncResult multiResult)
+ {
+ if (_isInContent)
+ {
+ throw new InvalidOperationException(SR.MailWriterIsInContent);
+ }
+
+ _isInContent = true;
+
+ CheckBoundary();
+
+ _bufferBuilder.Append(s_crlf);
+ Flush(multiResult);
+
+ ClosableStream cs = new ClosableStream(new EightBitStream(_stream, _shouldEncodeLeadingDots), _onCloseHandler);
+ _contentStream = cs;
+ return cs;
+ }
+
+ internal IAsyncResult BeginGetContentStream(AsyncCallback callback, object state)
+ {
+ MultiAsyncResult multiResult = new MultiAsyncResult(this, callback, state);
+
+ Stream s = GetContentStream(multiResult);
+
+ if (!(multiResult.Result is Exception))
+ {
+ multiResult.Result = s;
+ }
+
+ multiResult.CompleteSequence();
+
+ return multiResult;
+ }
+
+ internal Stream EndGetContentStream(IAsyncResult result)
+ {
+ object o = MultiAsyncResult.End(result);
+ if (o is Exception)
+ {
+ throw (Exception)o;
+ }
+ return (Stream)o;
+ }
+
+ #endregion Content
+
+ #region Cleanup
+
+ protected void Flush(MultiAsyncResult multiResult)
+ {
+ if (_bufferBuilder.Length > 0)
+ {
+ if (multiResult != null)
+ {
+ multiResult.Enter();
+ IAsyncResult result = _stream.BeginWrite(_bufferBuilder.GetBuffer(), 0,
+ _bufferBuilder.Length, s_onWrite, multiResult);
+ if (result.CompletedSynchronously)
+ {
+ _stream.EndWrite(result);
+ multiResult.Leave();
+ }
+ }
+ else
+ {
+ _stream.Write(_bufferBuilder.GetBuffer(), 0, _bufferBuilder.Length);
+ }
+ _bufferBuilder.Reset();
+ }
+ }
+
+ protected static void OnWrite(IAsyncResult result)
+ {
+ if (!result.CompletedSynchronously)
+ {
+ MultiAsyncResult multiResult = (MultiAsyncResult)result.AsyncState;
+ BaseWriter thisPtr = (BaseWriter)multiResult.Context;
+ try
+ {
+ thisPtr._stream.EndWrite(result);
+ multiResult.Leave();
+ }
+ catch (Exception e)
+ {
+ multiResult.Leave(e);
+ }
+ }
+ }
+
+ internal abstract void Close();
+
+ protected abstract void OnClose(object sender, EventArgs args);
+
+ #endregion Cleanup
+
+ protected virtual void CheckBoundary() { }
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mime/CloseableStream.cs b/src/System.Net.Mime/src/System/Net/Mime/CloseableStream.cs
new file mode 100644
index 0000000000..db04a5f7a2
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mime/CloseableStream.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.IO;
+using System.Threading;
+using System.Net.Http;
+
+namespace System.Net
+{
+ /// <summary>Provides a stream that notifies an event when the Close method is called.</summary>
+ internal class ClosableStream : DelegatingStream
+ {
+ private readonly EventHandler _onClose;
+ private int _closed;
+
+ internal ClosableStream(Stream stream, EventHandler onClose) : base(stream)
+ {
+ _onClose = onClose;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (Interlocked.Increment(ref _closed) == 1)
+ {
+ _onClose?.Invoke(this, new EventArgs());
+ }
+
+ base.Dispose(disposing);
+ }
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mime/ContentDisposition.cs b/src/System.Net.Mime/src/System/Net/Mime/ContentDisposition.cs
new file mode 100644
index 0000000000..8b1a736bcb
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mime/ContentDisposition.cs
@@ -0,0 +1,342 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Globalization;
+using System.Net.Mail;
+using System.Text;
+
+namespace System.Net.Mime
+{
+ public class ContentDisposition
+ {
+ private const string CreationDateKey = "creation-date";
+ private const string ModificationDateKey = "modification-date";
+ private const string ReadDateKey = "read-date";
+ private const string FileNameKey = "filename";
+ private const string SizeKey = "size";
+
+ private TrackingValidationObjectDictionary _parameters;
+ private string _disposition;
+ private string _dispositionType;
+ private bool _isChanged;
+ private bool _isPersisted;
+
+ private static readonly TrackingValidationObjectDictionary.ValidateAndParseValue s_dateParser =
+ new TrackingValidationObjectDictionary.ValidateAndParseValue(v => new SmtpDateTime(v.ToString()));
+ // this will throw a FormatException if the value supplied is not a valid SmtpDateTime
+
+ private static readonly TrackingValidationObjectDictionary.ValidateAndParseValue s_longParser =
+ new TrackingValidationObjectDictionary.ValidateAndParseValue((object value) => {
+ long longValue;
+ if (!long.TryParse(value.ToString(), NumberStyles.None, CultureInfo.InvariantCulture, out longValue))
+ {
+ throw new FormatException(SR.ContentDispositionInvalid);
+ }
+ return longValue;
+ });
+
+ private static readonly Dictionary<string, TrackingValidationObjectDictionary.ValidateAndParseValue> s_validators =
+ new Dictionary<string, TrackingValidationObjectDictionary.ValidateAndParseValue>() {
+ { CreationDateKey, s_dateParser },
+ { ModificationDateKey, s_dateParser },
+ { ReadDateKey, s_dateParser },
+ { SizeKey, s_longParser }
+ };
+
+ public ContentDisposition()
+ {
+ _isChanged = true;
+ _disposition = _dispositionType = "attachment";
+ // no need to parse disposition since there's nothing to parse
+ }
+
+ public ContentDisposition(string disposition)
+ {
+ if (disposition == null)
+ {
+ throw new ArgumentNullException(nameof(disposition));
+ }
+ _isChanged = true;
+ _disposition = disposition;
+ ParseValue();
+ }
+
+ internal DateTime GetDateParameter(string parameterName)
+ {
+ SmtpDateTime dateValue = ((TrackingValidationObjectDictionary)Parameters).InternalGet(parameterName) as SmtpDateTime;
+ return dateValue == null ? DateTime.MinValue : dateValue.Date;
+ }
+
+ /// <summary>
+ /// Gets the disposition type of the content.
+ /// </summary>
+ public string DispositionType
+ {
+ get { return _dispositionType; }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+ if (value == string.Empty)
+ {
+ throw new ArgumentException(SR.net_emptystringset, nameof(value));
+ }
+
+ _isChanged = true;
+ _dispositionType = value;
+ }
+ }
+
+ public StringDictionary Parameters => _parameters ?? (_parameters = new TrackingValidationObjectDictionary(s_validators));
+
+ /// <summary>
+ /// Gets the value of the Filename parameter.
+ /// </summary>
+ public string FileName
+ {
+ get { return Parameters[FileNameKey]; }
+ set
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ Parameters.Remove(FileNameKey);
+ }
+ else
+ {
+ Parameters[FileNameKey] = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the value of the Creation-Date parameter.
+ /// </summary>
+ public DateTime CreationDate
+ {
+ get { return GetDateParameter(CreationDateKey); }
+ set
+ {
+ SmtpDateTime date = new SmtpDateTime(value);
+ ((TrackingValidationObjectDictionary)Parameters).InternalSet(CreationDateKey, date);
+ }
+ }
+
+ /// <summary>
+ /// Gets the value of the Modification-Date parameter.
+ /// </summary>
+ public DateTime ModificationDate
+ {
+ get { return GetDateParameter(ModificationDateKey); }
+ set
+ {
+ SmtpDateTime date = new SmtpDateTime(value);
+ ((TrackingValidationObjectDictionary)Parameters).InternalSet(ModificationDateKey, date);
+ }
+ }
+
+ public bool Inline
+ {
+ get { return _dispositionType == DispositionTypeNames.Inline; }
+ set
+ {
+ _isChanged = true;
+ _dispositionType = value ? DispositionTypeNames.Inline : DispositionTypeNames.Attachment;
+ }
+ }
+
+ /// <summary>
+ /// Gets the value of the Read-Date parameter.
+ /// </summary>
+ public DateTime ReadDate
+ {
+ get { return GetDateParameter(ReadDateKey); }
+ set
+ {
+ SmtpDateTime date = new SmtpDateTime(value);
+ ((TrackingValidationObjectDictionary)Parameters).InternalSet(ReadDateKey, date);
+ }
+ }
+
+ /// <summary>
+ /// Gets the value of the Size parameter (-1 if unspecified).
+ /// </summary>
+ public long Size
+ {
+ get
+ {
+ object sizeValue = ((TrackingValidationObjectDictionary)Parameters).InternalGet(SizeKey);
+ return sizeValue == null ? -1 : (long)sizeValue;
+ }
+ set
+ {
+ ((TrackingValidationObjectDictionary)Parameters).InternalSet(SizeKey, value);
+ }
+ }
+
+ internal void Set(string contentDisposition, HeaderCollection headers)
+ {
+ // we don't set ischanged because persistence was already handled
+ // via the headers.
+ _disposition = contentDisposition;
+ ParseValue();
+ headers.InternalSet(MailHeaderInfo.GetString(MailHeaderID.ContentDisposition), ToString());
+ _isPersisted = true;
+ }
+
+ internal void PersistIfNeeded(HeaderCollection headers, bool forcePersist)
+ {
+ if (IsChanged || !_isPersisted || forcePersist)
+ {
+ headers.InternalSet(MailHeaderInfo.GetString(MailHeaderID.ContentDisposition), ToString());
+ _isPersisted = true;
+ }
+ }
+
+ internal bool IsChanged => _isChanged || _parameters != null && _parameters.IsChanged;
+
+ public override string ToString()
+ {
+ if (_disposition == null || _isChanged || _parameters != null && _parameters.IsChanged)
+ {
+ _disposition = Encode(false); // Legacy wire-safe format
+ _isChanged = false;
+ _parameters.IsChanged = false;
+ _isPersisted = false;
+ }
+ return _disposition;
+ }
+
+ internal string Encode(bool allowUnicode)
+ {
+ var builder = new StringBuilder();
+ builder.Append(_dispositionType); // Must not have unicode, already validated
+
+ // Validate and encode unicode where required
+ foreach (string key in Parameters.Keys)
+ {
+ builder.Append("; ");
+ EncodeToBuffer(key, builder, allowUnicode);
+
+ builder.Append('=');
+ EncodeToBuffer(_parameters[key], builder, allowUnicode);
+ }
+
+ return builder.ToString();
+ }
+
+ private static void EncodeToBuffer(string value, StringBuilder builder, bool allowUnicode)
+ {
+ Encoding encoding = MimeBasePart.DecodeEncoding(value);
+ if (encoding != null) // Manually encoded elsewhere, pass through
+ {
+ builder.Append('"').Append(value).Append('"');
+ }
+ else if ((allowUnicode && !MailBnfHelper.HasCROrLF(value)) // Unicode without CL or LF's
+ || MimeBasePart.IsAscii(value, false)) // Ascii
+ {
+ MailBnfHelper.GetTokenOrQuotedString(value, builder, allowUnicode);
+ }
+ else
+ {
+ // MIME Encoding required
+ encoding = Encoding.GetEncoding(MimeBasePart.DefaultCharSet);
+ builder.Append('"').Append(MimeBasePart.EncodeHeaderValue(value, encoding, MimeBasePart.ShouldUseBase64Encoding(encoding))).Append('"');
+ }
+ }
+
+ public override bool Equals(object rparam)
+ {
+ return rparam == null ?
+ false :
+ string.Compare(ToString(), rparam.ToString(), StringComparison.OrdinalIgnoreCase) == 0;
+ }
+
+ public override int GetHashCode() => ToString().ToLowerInvariant().GetHashCode();
+
+ private void ParseValue()
+ {
+ int offset = 0;
+ try
+ {
+ // the disposition MUST be the first parameter in the string
+ _dispositionType = MailBnfHelper.ReadToken(_disposition, ref offset, null);
+
+ // disposition MUST not be empty
+ if (string.IsNullOrEmpty(_dispositionType))
+ {
+ throw new FormatException(SR.MailHeaderFieldMalformedHeader);
+ }
+
+ // now we know that there are parameters so we must initialize or clear
+ // and parse
+ if (_parameters == null)
+ {
+ _parameters = new TrackingValidationObjectDictionary(s_validators);
+ }
+ else
+ {
+ _parameters.Clear();
+ }
+
+ while (MailBnfHelper.SkipCFWS(_disposition, ref offset))
+ {
+ // ensure that the separator charactor is present
+ if (_disposition[offset++] != ';')
+ {
+ throw new FormatException(SR.Format(SR.MailHeaderFieldInvalidCharacter, _disposition[offset - 1]));
+ }
+
+ // skip whitespace and see if there's anything left to parse or if we're done
+ if (!MailBnfHelper.SkipCFWS(_disposition, ref offset))
+ {
+ break;
+ }
+
+ string paramAttribute = MailBnfHelper.ReadParameterAttribute(_disposition, ref offset, null);
+ string paramValue;
+
+ // verify the next character after the parameter is correct
+ if (_disposition[offset++] != '=')
+ {
+ throw new FormatException(SR.MailHeaderFieldMalformedHeader);
+ }
+
+ if (!MailBnfHelper.SkipCFWS(_disposition, ref offset))
+ {
+ // parameter was at end of string and has no value
+ // this is not valid
+ throw new FormatException(SR.ContentDispositionInvalid);
+ }
+
+ paramValue = _disposition[offset] == '"' ?
+ MailBnfHelper.ReadQuotedString(_disposition, ref offset, null) :
+ MailBnfHelper.ReadToken(_disposition, ref offset, null);
+
+ // paramValue could potentially still be empty if it was a valid quoted string that
+ // contained no inner value. this is invalid
+ if (string.IsNullOrEmpty(paramAttribute) || string.IsNullOrEmpty(paramValue))
+ {
+ throw new FormatException(SR.ContentDispositionInvalid);
+ }
+
+ // if validation is needed, the parameters dictionary will have a validator registered
+ // for the parameter that is being set so no additional formatting checks are needed here
+ Parameters.Add(paramAttribute, paramValue);
+ }
+ }
+ catch (FormatException exception)
+ {
+ // it's possible that something in MailBNFHelper could throw so ensure that we catch it and wrap it
+ // so that the exception has the correct text
+ throw new FormatException(SR.ContentDispositionInvalid, exception);
+ }
+
+ _parameters.IsChanged = false;
+ }
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mime/ContentType.cs b/src/System.Net.Mime/src/System/Net/Mime/ContentType.cs
new file mode 100644
index 0000000000..0fc80e1ce2
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mime/ContentType.cs
@@ -0,0 +1,319 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Specialized;
+using System.Net.Mail;
+using System.Text;
+
+namespace System.Net.Mime
+{
+ // Typed Content-Type header
+ //
+ // We parse the type during construction and set.
+ // null and string.empty will throw for construction,set and mediatype/subtype
+ // constructors set isPersisted to false. isPersisted needs to be tracked seperately
+ // than isChanged because isChanged only determines if the cached value should be used.
+ // isPersisted tracks if the object has been persisted. However, obviously if isChanged is true
+ // the object isn't persisted.
+ // If any subcomponents are changed, isChanged is set to true and isPersisted is false
+ // ToString caches the value until a isChanged is true, then it recomputes the full value.
+
+ public class ContentType
+ {
+ private readonly TrackingStringDictionary _parameters = new TrackingStringDictionary();
+
+ private string _mediaType;
+ private string _subType;
+ private bool _isChanged;
+ private string _type;
+ private bool _isPersisted;
+
+ /// <summary>
+ /// Default content type - can be used if the Content-Type header
+ /// is not defined in the message headers.
+ /// </summary>
+ internal const string Default = "application/octet-stream";
+
+ public ContentType() : this(Default)
+ {
+ }
+
+ /// <summary>
+ /// ctor.
+ /// </summary>
+ /// <param name="fieldValue">Unparsed value of the Content-Type header.</param>
+ public ContentType(string contentType)
+ {
+ if (contentType == null)
+ {
+ throw new ArgumentNullException(nameof(contentType));
+ }
+ if (contentType == string.Empty)
+ {
+ throw new ArgumentException(SR.Format(SR.net_emptystringcall, nameof(contentType)), nameof(contentType));
+ }
+
+ _isChanged = true;
+ _type = contentType;
+ ParseValue();
+ }
+
+ public string Boundary
+ {
+ get { return Parameters["boundary"]; }
+ set
+ {
+ if (value == null || value == string.Empty)
+ {
+ Parameters.Remove("boundary");
+ }
+ else
+ {
+ Parameters["boundary"] = value;
+ }
+ }
+ }
+
+ public string CharSet
+ {
+ get { return Parameters["charset"]; }
+ set
+ {
+ if (value == null || value == string.Empty)
+ {
+ Parameters.Remove("charset");
+ }
+ else
+ {
+ Parameters["charset"] = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the media type.
+ /// </summary>
+ public string MediaType
+ {
+ get { return _mediaType + "/" + _subType; }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ if (value == string.Empty)
+ {
+ throw new ArgumentException(SR.net_emptystringset, nameof(value));
+ }
+
+ int offset = 0;
+ _mediaType = MailBnfHelper.ReadToken(value, ref offset, null);
+ if (_mediaType.Length == 0 || offset >= value.Length || value[offset++] != '/')
+ throw new FormatException(SR.MediaTypeInvalid);
+
+ _subType = MailBnfHelper.ReadToken(value, ref offset, null);
+ if (_subType.Length == 0 || offset < value.Length)
+ {
+ throw new FormatException(SR.MediaTypeInvalid);
+ }
+
+ _isChanged = true;
+ _isPersisted = false;
+ }
+ }
+
+
+ public string Name
+ {
+ get
+ {
+ string value = Parameters["name"];
+ Encoding nameEncoding = MimeBasePart.DecodeEncoding(value);
+ if (nameEncoding != null)
+ {
+ value = MimeBasePart.DecodeHeaderValue(value);
+ }
+ return value;
+ }
+ set
+ {
+ if (value == null || value == string.Empty)
+ {
+ Parameters.Remove("name");
+ }
+ else
+ {
+ Parameters["name"] = value;
+ }
+ }
+ }
+
+
+ public StringDictionary Parameters => _parameters;
+
+ internal void Set(string contentType, HeaderCollection headers)
+ {
+ _type = contentType;
+ ParseValue();
+ headers.InternalSet(MailHeaderInfo.GetString(MailHeaderID.ContentType), ToString());
+ _isPersisted = true;
+ }
+
+ internal void PersistIfNeeded(HeaderCollection headers, bool forcePersist)
+ {
+ if (IsChanged || !_isPersisted || forcePersist)
+ {
+ headers.InternalSet(MailHeaderInfo.GetString(MailHeaderID.ContentType), ToString());
+ _isPersisted = true;
+ }
+ }
+
+ internal bool IsChanged => _isChanged || _parameters != null && _parameters.IsChanged;
+
+ public override string ToString()
+ {
+ if (_type == null || IsChanged)
+ {
+ _type = Encode(false); // Legacy wire-safe format
+ _isChanged = false;
+ _parameters.IsChanged = false;
+ _isPersisted = false;
+ }
+ return _type;
+ }
+
+ internal string Encode(bool allowUnicode)
+ {
+ var builder = new StringBuilder();
+
+ builder.Append(_mediaType); // Must not have unicode, already validated
+ builder.Append('/');
+ builder.Append(_subType); // Must not have unicode, already validated
+
+ // Validate and encode unicode where required
+ foreach (string key in Parameters.Keys)
+ {
+ builder.Append("; ");
+ EncodeToBuffer(key, builder, allowUnicode);
+ builder.Append('=');
+ EncodeToBuffer(_parameters[key], builder, allowUnicode);
+ }
+
+ return builder.ToString();
+ }
+
+ private static void EncodeToBuffer(string value, StringBuilder builder, bool allowUnicode)
+ {
+ Encoding encoding = MimeBasePart.DecodeEncoding(value);
+ if (encoding != null) // Manually encoded elsewhere, pass through
+ {
+ builder.Append('\"').Append(value).Append('"');
+ }
+ else if ((allowUnicode && !MailBnfHelper.HasCROrLF(value)) // Unicode without CL or LF's
+ || MimeBasePart.IsAscii(value, false)) // Ascii
+ {
+ MailBnfHelper.GetTokenOrQuotedString(value, builder, allowUnicode);
+ }
+ else
+ {
+ // MIME Encoding required
+ encoding = Encoding.GetEncoding(MimeBasePart.DefaultCharSet);
+ builder.Append('"').Append(MimeBasePart.EncodeHeaderValue(value, encoding, MimeBasePart.ShouldUseBase64Encoding(encoding))).Append('"');
+ }
+ }
+
+ public override bool Equals(object rparam) =>
+ rparam == null ? false : string.Compare(ToString(), rparam.ToString(), StringComparison.OrdinalIgnoreCase) == 0;
+
+ public override int GetHashCode() => ToString().ToLowerInvariant().GetHashCode();
+
+ // Helper methods.
+
+ private void ParseValue()
+ {
+ int offset = 0;
+ Exception exception = null;
+
+ try
+ {
+ _mediaType = MailBnfHelper.ReadToken(_type, ref offset, null);
+ if (_mediaType == null || _mediaType.Length == 0 || offset >= _type.Length || _type[offset++] != '/')
+ {
+ exception = new FormatException(SR.ContentTypeInvalid);
+ }
+
+ if (exception == null)
+ {
+ _subType = MailBnfHelper.ReadToken(_type, ref offset, null);
+ if (_subType == null || _subType.Length == 0)
+ {
+ exception = new FormatException(SR.ContentTypeInvalid);
+ }
+ }
+
+ if (exception == null)
+ {
+ while (MailBnfHelper.SkipCFWS(_type, ref offset))
+ {
+ if (_type[offset++] != ';')
+ {
+ exception = new FormatException(SR.ContentTypeInvalid);
+ break;
+ }
+
+ if (!MailBnfHelper.SkipCFWS(_type, ref offset))
+ {
+ break;
+ }
+
+ string paramAttribute = MailBnfHelper.ReadParameterAttribute(_type, ref offset, null);
+
+ if (paramAttribute == null || paramAttribute.Length == 0)
+ {
+ exception = new FormatException(SR.ContentTypeInvalid);
+ break;
+ }
+
+ string paramValue;
+ if (offset >= _type.Length || _type[offset++] != '=')
+ {
+ exception = new FormatException(SR.ContentTypeInvalid);
+ break;
+ }
+
+ if (!MailBnfHelper.SkipCFWS(_type, ref offset))
+ {
+ exception = new FormatException(SR.ContentTypeInvalid);
+ break;
+ }
+
+ paramValue = _type[offset] == '"' ?
+ MailBnfHelper.ReadQuotedString(_type, ref offset, null) :
+ MailBnfHelper.ReadToken(_type, ref offset, null);
+
+ if (paramValue == null)
+ {
+ exception = new FormatException(SR.ContentTypeInvalid);
+ break;
+ }
+
+ _parameters.Add(paramAttribute, paramValue);
+ }
+ }
+ _parameters.IsChanged = false;
+ }
+ catch (FormatException)
+ {
+ throw new FormatException(SR.ContentTypeInvalid);
+ }
+
+ if (exception != null)
+ {
+ throw new FormatException(SR.ContentTypeInvalid);
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mime/DispositionTypeNames.cs b/src/System.Net.Mime/src/System/Net/Mime/DispositionTypeNames.cs
new file mode 100644
index 0000000000..e1f448e159
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mime/DispositionTypeNames.cs
@@ -0,0 +1,12 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace System.Net.Mime
+{
+ public static class DispositionTypeNames
+ {
+ public const string Inline = "inline";
+ public const string Attachment = "attachment";
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mime/EightBitStream.cs b/src/System.Net.Mime/src/System/Net/Mime/EightBitStream.cs
new file mode 100644
index 0000000000..452b39521b
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mime/EightBitStream.cs
@@ -0,0 +1,164 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.IO;
+using System.Net.Http;
+
+namespace System.Net.Mime
+{
+ /// <summary>
+ /// This stream does not encode content, but merely allows the user to declare
+ /// that the content does not need encoding.
+ ///
+ /// This stream is also used to implement RFC 2821 Section 4.5.2 (pad leading
+ /// dots on a line) on the entire message so we don't have to implement it
+ /// on all of the individual components.
+ ///
+ /// History: This class used to be called SevenBitStream and was supposed to
+ /// validate that outgoing bytes were within the acceptable range of 0 - 127
+ /// and throw if a value > 127 is found.
+ /// However, the enforcement was not properly implemented and rarely executed.
+ /// For legacy (app-compat) reasons we have chosen to remove the enforcement
+ /// and rename the class from SevenBitStream to EightBitStream.
+ /// </summary>
+ internal class EightBitStream : DelegatingStream, IEncodableStream
+ {
+ private WriteStateInfoBase _writeState;
+
+ // Should we do RFC 2821 Section 4.5.2 encoding of leading dots on a line?
+ // We make this optional because this stream may be used recursively and
+ // the encoding should only be done once.
+ private bool _shouldEncodeLeadingDots = false;
+
+ private WriteStateInfoBase WriteState => _writeState ?? (_writeState = new WriteStateInfoBase());
+
+ /// <summary>
+ /// ctor.
+ /// </summary>
+ /// <param name="stream">Underlying stream</param>
+ internal EightBitStream(Stream stream) : base(stream) { }
+
+ internal EightBitStream(Stream stream, bool shouldEncodeLeadingDots) : this(stream)
+ {
+ _shouldEncodeLeadingDots = shouldEncodeLeadingDots;
+ }
+
+ /// <summary>
+ /// Writes the specified content to the underlying stream
+ /// </summary>
+ /// <param name="buffer">Buffer to write</param>
+ /// <param name="offset">Offset within buffer to start writing</param>
+ /// <param name="count">Count of bytes to write</param>
+ /// <param name="callback">Callback to call when write completes</param>
+ /// <param name="state">State to pass to callback</param>
+ public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException(nameof(buffer));
+ }
+ if (offset < 0 || offset >= buffer.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset));
+ }
+ if (offset + count > buffer.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
+ }
+
+ IAsyncResult result;
+ if (_shouldEncodeLeadingDots)
+ {
+ EncodeLines(buffer, offset, count);
+ result = base.BeginWrite(WriteState.Buffer, 0, WriteState.Length, callback, state);
+ }
+ else
+ {
+ // Note: for legacy reasons we are not enforcing buffer[i] <= 127.
+ result = base.BeginWrite(buffer, offset, count, callback, state);
+ }
+
+ return result;
+ }
+
+ public override void EndWrite(IAsyncResult asyncResult)
+ {
+ base.EndWrite(asyncResult);
+ WriteState.BufferFlushed();
+ }
+
+ /// <summary>
+ /// Writes the specified content to the underlying stream
+ /// </summary>
+ /// <param name="buffer">Buffer to write</param>
+ /// <param name="offset">Offset within buffer to start writing</param>
+ /// <param name="count">Count of bytes to write</param>
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException(nameof(buffer));
+ }
+ if (offset < 0 || offset >= buffer.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset));
+ }
+ if (offset + count > buffer.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
+ }
+
+ if (_shouldEncodeLeadingDots)
+ {
+ EncodeLines(buffer, offset, count);
+ base.Write(WriteState.Buffer, 0, WriteState.Length);
+ WriteState.BufferFlushed();
+ }
+ else
+ {
+ // Note: for legacy reasons we are not enforcing buffer[i] <= 127.
+ base.Write(buffer, offset, count);
+ }
+ }
+
+ // helper methods
+
+ // Despite not having to encode content, we still have to implement
+ // RFC 2821 Section 4.5.2 about leading dots on a line
+ private void EncodeLines(byte[] buffer, int offset, int count)
+ {
+ for (int i = offset; (i < offset + count) && (i < buffer.Length); i++)
+ {
+ // Note: for legacy reasons we are not enforcing buffer[i] <= 127.
+
+ // Detect CRLF line endings
+ if ((buffer[i] == '\r') && ((i + 1) < (offset + count)) && (buffer[i + 1] == '\n'))
+ {
+ WriteState.AppendCRLF(false); // Resets CurrentLineLength to 0
+ i++; // Skip past the recorded CRLF
+ }
+ else if ((WriteState.CurrentLineLength == 0) && (buffer[i] == '.'))
+ {
+ // RFC 2821 Section 4.5.2: We must pad leading dots on a line with an extra dot
+ // This is the only 'encoding' change we make to the data in this method
+ WriteState.Append((byte)'.');
+ WriteState.Append(buffer[i]);
+ }
+ else
+ {
+ // Just regular seven bit data
+ WriteState.Append(buffer[i]);
+ }
+ }
+ }
+
+ public Stream GetStream() => this;
+
+ public int DecodeBytes(byte[] buffer, int offset, int count) { throw new NotImplementedException(); }
+
+ public int EncodeBytes(byte[] buffer, int offset, int count) { throw new NotImplementedException(); }
+
+ public string GetEncodedString() { throw new NotImplementedException(); }
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mime/EncodedStreamFactory.cs b/src/System.Net.Mime/src/System/Net/Mime/EncodedStreamFactory.cs
new file mode 100644
index 0000000000..25f9853e51
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mime/EncodedStreamFactory.cs
@@ -0,0 +1,74 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.IO;
+using System.Text;
+
+namespace System.Net.Mime
+{
+ internal class EncodedStreamFactory
+ {
+ //RFC 2822: no encoded-word line should be longer than 76 characters not including the soft CRLF
+ //since the header length is unknown (if there even is one) we're going to be slightly more conservative
+ //and cut off at 70. This will also prevent any other folding behavior from being triggered anywhere
+ //in the code
+ internal const int DefaultMaxLineLength = 70;
+
+ //default buffer size for encoder
+ private const int InitialBufferSize = 1024;
+
+ //get a raw encoder, not for use with header encoding
+ internal IEncodableStream GetEncoder(TransferEncoding encoding, Stream stream)
+ {
+ //raw encoder
+ if (encoding == TransferEncoding.Base64)
+ {
+ return new Base64Stream(stream, new Base64WriteStateInfo());
+ }
+
+ //return a QuotedPrintable stream because this is not being used for header encoding
+ if (encoding == TransferEncoding.QuotedPrintable)
+ {
+ return new QuotedPrintableStream(stream, true);
+ }
+
+ if (encoding == TransferEncoding.SevenBit || encoding == TransferEncoding.EightBit)
+ {
+ return new EightBitStream(stream);
+ }
+
+ throw new NotSupportedException();
+ }
+
+ //use for encoding headers
+ internal IEncodableStream GetEncoderForHeader(Encoding encoding, bool useBase64Encoding, int headerTextLength)
+ {
+ byte[] header = CreateHeader(encoding, useBase64Encoding);
+ byte[] footer = CreateFooter();
+
+ WriteStateInfoBase writeState;
+ if (useBase64Encoding)
+ {
+ writeState = new Base64WriteStateInfo(InitialBufferSize, header, footer, DefaultMaxLineLength, headerTextLength);
+ return new Base64Stream((Base64WriteStateInfo)writeState);
+ }
+
+ writeState = new WriteStateInfoBase(InitialBufferSize, header, footer, DefaultMaxLineLength, headerTextLength);
+ return new QEncodedStream(writeState);
+ }
+
+ //Create the header for what type of byte encoding is going to be used
+ //based on the encoding type and if base64 encoding should be forced
+ //sample header: =?utf-8?B?
+ protected byte[] CreateHeader(Encoding encoding, bool useBase64Encoding)
+ {
+ //create encoded work header
+ string header = string.Format("=?{0}?{1}?", encoding.HeaderName, useBase64Encoding ? "B" : "Q");
+ return Encoding.ASCII.GetBytes(header);
+ }
+
+ //creates the footer that marks the end of a quoted string of some sort
+ protected byte[] CreateFooter() => new byte[] { (byte)'?', (byte)'=' };
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mime/HeaderCollection.cs b/src/System.Net.Mime/src/System/Net/Mime/HeaderCollection.cs
new file mode 100644
index 0000000000..d5b620aff5
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mime/HeaderCollection.cs
@@ -0,0 +1,213 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Specialized;
+using System.Globalization;
+using System.Net.Mail;
+
+namespace System.Net.Mime
+{
+ /// <summary>
+ /// Summary description for HeaderCollection.
+ /// </summary>
+ internal class HeaderCollection : NameValueCollection
+ {
+ private MimeBasePart _part = null;
+
+ // default constructor
+ // intentionally override the default comparer in the derived base class
+ internal HeaderCollection() : base(StringComparer.OrdinalIgnoreCase)
+ {
+ }
+
+ public override void Remove(string name)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ if (name == string.Empty)
+ {
+ throw new ArgumentException(SR.Format(SR.net_emptystringcall, nameof(name)), nameof(name));
+ }
+
+ MailHeaderID id = MailHeaderInfo.GetID(name);
+
+ if (id == MailHeaderID.ContentType && _part != null)
+ {
+ _part.ContentType = null;
+ }
+ else if (id == MailHeaderID.ContentDisposition && _part is MimePart)
+ {
+ ((MimePart)_part).ContentDisposition = null;
+ }
+
+ base.Remove(name);
+ }
+
+
+ public override string Get(string name)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ if (name == string.Empty)
+ {
+ throw new ArgumentException(SR.Format(SR.net_emptystringcall, nameof(name)), nameof(name));
+ }
+
+ MailHeaderID id = MailHeaderInfo.GetID(name);
+
+ if (id == MailHeaderID.ContentType && _part != null)
+ {
+ _part.ContentType.PersistIfNeeded(this, false);
+ }
+ else if (id == MailHeaderID.ContentDisposition && _part is MimePart)
+ {
+ ((MimePart)_part).ContentDisposition.PersistIfNeeded(this, false);
+ }
+ return base.Get(name);
+ }
+
+ public override string[] GetValues(string name)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ if (name == string.Empty)
+ {
+ throw new ArgumentException(SR.Format(SR.net_emptystringcall, nameof(name)), nameof(name));
+ }
+
+ MailHeaderID id = MailHeaderInfo.GetID(name);
+
+ if (id == MailHeaderID.ContentType && _part != null)
+ {
+ _part.ContentType.PersistIfNeeded(this, false);
+ }
+ else if (id == MailHeaderID.ContentDisposition && _part is MimePart)
+ {
+ ((MimePart)_part).ContentDisposition.PersistIfNeeded(this, false);
+ }
+ return base.GetValues(name);
+ }
+
+
+ internal void InternalRemove(string name) => base.Remove(name);
+
+ //set an existing header's value
+ internal void InternalSet(string name, string value) => base.Set(name, value);
+
+ //add a new header and set its value
+ internal void InternalAdd(string name, string value)
+ {
+ if (MailHeaderInfo.IsSingleton(name))
+ {
+ base.Set(name, value);
+ }
+ else
+ {
+ base.Add(name, value);
+ }
+ }
+
+ public override void Set(string name, string value)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ if (name == string.Empty)
+ {
+ throw new ArgumentException(SR.Format(SR.net_emptystringcall, nameof(name)), nameof(name));
+ }
+
+ if (value == string.Empty)
+ {
+ throw new ArgumentException(SR.Format(SR.net_emptystringcall, nameof(value)), nameof(name));
+ }
+
+ if (!MimeBasePart.IsAscii(name, false))
+ {
+ throw new FormatException(SR.Format(SR.InvalidHeaderName));
+ }
+
+ // normalize the case of well known headers
+ name = MailHeaderInfo.NormalizeCase(name);
+
+ MailHeaderID id = MailHeaderInfo.GetID(name);
+
+ // TODO https://github.com/dotnet/corefx/issues/11747: Uncomment once we can use NormalizationForm
+ //value = value.Normalize(NormalizationForm.FormC);
+
+ if (id == MailHeaderID.ContentType && _part != null)
+ {
+ _part.ContentType.Set(value.ToLower(CultureInfo.InvariantCulture), this);
+ }
+ else if (id == MailHeaderID.ContentDisposition && _part is MimePart)
+ {
+ ((MimePart)_part).ContentDisposition.Set(value.ToLower(CultureInfo.InvariantCulture), this);
+ }
+ else
+ {
+ base.Set(name, value);
+ }
+ }
+
+
+ public override void Add(string name, string value)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+ if (name == string.Empty)
+ {
+ throw new ArgumentException(SR.Format(SR.net_emptystringcall, nameof(name)), nameof(name));
+ }
+ if (value == string.Empty)
+ {
+ throw new ArgumentException(SR.Format(SR.net_emptystringcall, nameof(value)), nameof(name));
+ }
+
+ MailBnfHelper.ValidateHeaderName(name);
+
+ // normalize the case of well known headers
+ name = MailHeaderInfo.NormalizeCase(name);
+
+ MailHeaderID id = MailHeaderInfo.GetID(name);
+
+ // TODO https://github.com/dotnet/corefx/issues/11747: Uncomment once we can use NormalizationForm
+ //value = value.Normalize(Text.NormalizationForm.FormC);// TODO: Uncomment once we can use NormalizationForm
+
+ if (id == MailHeaderID.ContentType && _part != null)
+ {
+ _part.ContentType.Set(value.ToLower(CultureInfo.InvariantCulture), this);
+ }
+ else if (id == MailHeaderID.ContentDisposition && _part is MimePart)
+ {
+ ((MimePart)_part).ContentDisposition.Set(value.ToLower(CultureInfo.InvariantCulture), this);
+ }
+ else
+ {
+ InternalAdd(name, value);
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mime/IEncodableStream.cs b/src/System.Net.Mime/src/System/Net/Mime/IEncodableStream.cs
new file mode 100644
index 0000000000..6cdae96473
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mime/IEncodableStream.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.IO;
+
+namespace System.Net.Mime
+{
+ internal interface IEncodableStream
+ {
+ int DecodeBytes(byte[] buffer, int offset, int count);
+ int EncodeBytes(byte[] buffer, int offset, int count);
+ string GetEncodedString();
+ Stream GetStream();
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mime/MediaTypeNames.cs b/src/System.Net.Mime/src/System/Net/Mime/MediaTypeNames.cs
new file mode 100644
index 0000000000..09b6455122
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mime/MediaTypeNames.cs
@@ -0,0 +1,33 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace System.Net.Mime
+{
+ public static class MediaTypeNames
+ {
+ public static class Text
+ {
+ public const string Plain = "text/plain";
+ public const string Html = "text/html";
+ public const string Xml = "text/xml";
+ public const string RichText = "text/richtext";
+ }
+
+ public static class Application
+ {
+ public const string Soap = "application/soap+xml";
+ public const string Octet = "application/octet-stream";
+ public const string Rtf = "application/rtf";
+ public const string Pdf = "application/pdf";
+ public const string Zip = "application/zip";
+ }
+
+ public static class Image
+ {
+ public const string Gif = "image/gif";
+ public const string Tiff = "image/tiff";
+ public const string Jpeg = "image/jpeg";
+ }
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mime/MimeBasePart.cs b/src/System.Net.Mime/src/System/Net/Mime/MimeBasePart.cs
new file mode 100644
index 0000000000..d8de9eaf1b
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mime/MimeBasePart.cs
@@ -0,0 +1,289 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Specialized;
+using System.Text;
+using System.Net.Mail;
+
+namespace System.Net.Mime
+{
+ internal class MimeBasePart
+ {
+ internal const string DefaultCharSet = "utf-8";//"iso-8859-1";
+
+ protected ContentType _contentType;
+ protected ContentDisposition _contentDisposition;
+ private HeaderCollection _headers;
+
+ internal MimeBasePart() { }
+
+ internal static bool ShouldUseBase64Encoding(Encoding encoding) =>
+ encoding == Encoding.Unicode || encoding == Encoding.UTF8 || encoding == Encoding.UTF32 || encoding == Encoding.BigEndianUnicode;
+
+ //use when the length of the header is not known or if there is no header
+ internal static string EncodeHeaderValue(string value, Encoding encoding, bool base64Encoding) =>
+ EncodeHeaderValue(value, encoding, base64Encoding, 0);
+
+ //used when the length of the header name itself is known (i.e. Subject : )
+ internal static string EncodeHeaderValue(string value, Encoding encoding, bool base64Encoding, int headerLength)
+ {
+ var newString = new StringBuilder();
+
+ //no need to encode if it's pure ascii
+ if (IsAscii(value, false))
+ {
+ return value;
+ }
+
+ if (encoding == null)
+ {
+ encoding = Encoding.GetEncoding(MimeBasePart.DefaultCharSet);
+ }
+
+ EncodedStreamFactory factory = new EncodedStreamFactory();
+ IEncodableStream stream = factory.GetEncoderForHeader(encoding, base64Encoding, headerLength);
+
+ byte[] buffer = encoding.GetBytes(value);
+ stream.EncodeBytes(buffer, 0, buffer.Length);
+ return stream.GetEncodedString();
+ }
+
+ private static readonly char[] s_headerValueSplitChars = new char[] { '\r', '\n', ' ' };
+
+ internal static string DecodeHeaderValue(string value)
+ {
+ if (value == null || value.Length == 0)
+ {
+ return string.Empty;
+ }
+
+ string newValue = string.Empty;
+
+ //split strings, they may be folded. If they are, decode one at a time and append the results
+ string[] substringsToDecode = value.Split(s_headerValueSplitChars, StringSplitOptions.RemoveEmptyEntries);
+
+ foreach (string foldedSubString in substringsToDecode)
+ {
+ //an encoded string has as specific format in that it must start and end with an
+ //'=' char and contains five parts, separated by '?' chars.
+ //the first and last part are therefore '=', the second part is the byte encoding (B or Q)
+ //the third is the unicode encoding type, and the fourth is encoded message itself. '?' is not valid inside of
+ //an encoded string other than as a separator for these five parts.
+ //If this check fails, the string is either not encoded or cannot be decoded by this method
+ string[] subStrings = foldedSubString.Split('?');
+ if ((subStrings.Length != 5 || subStrings[0] != "=" || subStrings[4] != "="))
+ {
+ return value;
+ }
+
+ string charSet = subStrings[1];
+ bool base64Encoding = (subStrings[2] == "B");
+ byte[] buffer = Encoding.ASCII.GetBytes(subStrings[3]);
+ int newLength;
+
+ EncodedStreamFactory encoderFactory = new EncodedStreamFactory();
+ IEncodableStream s = encoderFactory.GetEncoderForHeader(Encoding.GetEncoding(charSet), base64Encoding, 0);
+
+ newLength = s.DecodeBytes(buffer, 0, buffer.Length);
+
+ Encoding encoding = Encoding.GetEncoding(charSet);
+ newValue += encoding.GetString(buffer, 0, newLength);
+ }
+ return newValue;
+ }
+
+ // Detect the encoding: "=?encoding?BorQ?content?="
+ // "=?utf-8?B?RmlsZU5hbWVf55CG0Y3Qq9C60I5jw4TRicKq0YIM0Y1hSsSeTNCy0Klh?="; // 3.5
+ // With the addition of folding in 4.0, there may be multiple lines with encoding, only detect the first:
+ // "=?utf-8?B?RmlsZU5hbWVf55CG0Y3Qq9C60I5jw4TRicKq0YIM0Y1hSsSeTNCy0Klh?=\r\n =?utf-8?B??=";
+ internal static Encoding DecodeEncoding(string value)
+ {
+ if (value == null || value.Length == 0)
+ {
+ return null;
+ }
+
+ string[] subStrings = value.Split('?', '\r', '\n');
+ if ((subStrings.Length < 5 || subStrings[0] != "=" || subStrings[4] != "="))
+ {
+ return null;
+ }
+
+ string charSet = subStrings[1];
+ return Encoding.GetEncoding(charSet);
+ }
+
+ internal static bool IsAscii(string value, bool permitCROrLF)
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ foreach (char c in value)
+ {
+ if (c > 0x7f)
+ {
+ return false;
+ }
+ if (!permitCROrLF && (c == '\r' || c == '\n'))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ internal static bool IsAnsi(string value, bool permitCROrLF)
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ foreach (char c in value)
+ {
+ if (c > 0xff)
+ {
+ return false;
+ }
+ if (!permitCROrLF && (c == '\r' || c == '\n'))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ internal string ContentID
+ {
+ get { return Headers[MailHeaderInfo.GetString(MailHeaderID.ContentID)]; }
+ set
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ Headers.Remove(MailHeaderInfo.GetString(MailHeaderID.ContentID));
+ }
+ else
+ {
+ Headers[MailHeaderInfo.GetString(MailHeaderID.ContentID)] = value;
+ }
+ }
+ }
+
+ internal string ContentLocation
+ {
+ get { return Headers[MailHeaderInfo.GetString(MailHeaderID.ContentLocation)]; }
+ set
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ Headers.Remove(MailHeaderInfo.GetString(MailHeaderID.ContentLocation));
+ }
+ else
+ {
+ Headers[MailHeaderInfo.GetString(MailHeaderID.ContentLocation)] = value;
+ }
+ }
+ }
+
+ internal NameValueCollection Headers
+ {
+ get
+ {
+ //persist existing info before returning
+ if (_headers == null)
+ {
+ _headers = new HeaderCollection();
+ }
+
+ if (_contentType == null)
+ {
+ _contentType = new ContentType();
+ }
+ _contentType.PersistIfNeeded(_headers, false);
+
+ if (_contentDisposition != null)
+ {
+ _contentDisposition.PersistIfNeeded(_headers, false);
+ }
+
+ return _headers;
+ }
+ }
+
+ internal ContentType ContentType
+ {
+ get { return _contentType ?? (_contentType = new ContentType()); }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ _contentType = value;
+ _contentType.PersistIfNeeded((HeaderCollection)Headers, true);
+ }
+ }
+
+ internal void PrepareHeaders(bool allowUnicode)
+ {
+ _contentType.PersistIfNeeded((HeaderCollection)Headers, false);
+ _headers.InternalSet(MailHeaderInfo.GetString(MailHeaderID.ContentType), _contentType.Encode(allowUnicode));
+
+ if (_contentDisposition != null)
+ {
+ _contentDisposition.PersistIfNeeded((HeaderCollection)Headers, false);
+ _headers.InternalSet(MailHeaderInfo.GetString(MailHeaderID.ContentDisposition), _contentDisposition.Encode(allowUnicode));
+ }
+ }
+
+ internal virtual void Send(BaseWriter writer, bool allowUnicode)
+ {
+ throw new NotImplementedException();
+ }
+
+ internal virtual IAsyncResult BeginSend(BaseWriter writer, AsyncCallback callback,
+ bool allowUnicode, object state)
+ {
+ throw new NotImplementedException();
+ }
+
+ internal void EndSend(IAsyncResult asyncResult)
+ {
+ if (asyncResult == null)
+ {
+ throw new ArgumentNullException(nameof(asyncResult));
+ }
+
+ LazyAsyncResult castedAsyncResult = asyncResult as MimePartAsyncResult;
+
+ if (castedAsyncResult == null || castedAsyncResult.AsyncObject != this)
+ {
+ throw new ArgumentException(SR.net_io_invalidasyncresult, nameof(asyncResult));
+ }
+
+ if (castedAsyncResult.EndCalled)
+ {
+ throw new InvalidOperationException(SR.Format(SR.net_io_invalidendcall, nameof(EndSend)));
+ }
+
+ castedAsyncResult.InternalWaitForCompletion();
+ castedAsyncResult.EndCalled = true;
+ if (castedAsyncResult.Result is Exception)
+ {
+ throw (Exception)castedAsyncResult.Result;
+ }
+ }
+
+ internal class MimePartAsyncResult : LazyAsyncResult
+ {
+ internal MimePartAsyncResult(MimeBasePart part, object state, AsyncCallback callback) : base(part, state, callback)
+ {
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mime/MimePart.cs b/src/System.Net.Mime/src/System/Net/Mime/MimePart.cs
new file mode 100644
index 0000000000..0df7697314
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mime/MimePart.cs
@@ -0,0 +1,384 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.IO;
+using System.Text;
+using System.Collections;
+using System.Globalization;
+using System.Net.Mail;
+
+namespace System.Net.Mime
+{
+ /// <summary>
+ /// Summary description for MimePart.
+ /// </summary>
+ internal class MimePart : MimeBasePart, IDisposable
+ {
+ private Stream _stream = null;
+ private bool _streamSet = false;
+ private bool _streamUsedOnce = false;
+ private AsyncCallback _readCallback;
+ private AsyncCallback _writeCallback;
+ private const int maxBufferSize = 0x4400; //seems optimal for send based on perf analysis
+
+ internal MimePart() { }
+
+ public void Dispose()
+ {
+ if (_stream != null)
+ {
+ _stream.Close();
+ }
+ }
+
+ internal Stream Stream => _stream;
+
+ internal ContentDisposition ContentDisposition
+ {
+ get { return _contentDisposition; }
+ set
+ {
+ _contentDisposition = value;
+ if (value == null)
+ {
+ ((HeaderCollection)Headers).InternalRemove(MailHeaderInfo.GetString(MailHeaderID.ContentDisposition));
+ }
+ else
+ {
+ _contentDisposition.PersistIfNeeded((HeaderCollection)Headers, true);
+ }
+ }
+ }
+
+ internal TransferEncoding TransferEncoding
+ {
+ get
+ {
+ string value = Headers[MailHeaderInfo.GetString(MailHeaderID.ContentTransferEncoding)];
+ if (value.Equals("base64", StringComparison.OrdinalIgnoreCase))
+ {
+ return TransferEncoding.Base64;
+ }
+ else if (value.Equals("quoted-printable", StringComparison.OrdinalIgnoreCase))
+ {
+ return TransferEncoding.QuotedPrintable;
+ }
+ else if (value.Equals("7bit", StringComparison.OrdinalIgnoreCase))
+ {
+ return TransferEncoding.SevenBit;
+ }
+ else if (value.Equals("8bit", StringComparison.OrdinalIgnoreCase))
+ {
+ return TransferEncoding.EightBit;
+ }
+ else
+ {
+ return TransferEncoding.Unknown;
+ }
+ }
+ set
+ {
+ //QFE 4554
+ if (value == TransferEncoding.Base64)
+ {
+ Headers[MailHeaderInfo.GetString(MailHeaderID.ContentTransferEncoding)] = "base64";
+ }
+ else if (value == TransferEncoding.QuotedPrintable)
+ {
+ Headers[MailHeaderInfo.GetString(MailHeaderID.ContentTransferEncoding)] = "quoted-printable";
+ }
+ else if (value == TransferEncoding.SevenBit)
+ {
+ Headers[MailHeaderInfo.GetString(MailHeaderID.ContentTransferEncoding)] = "7bit";
+ }
+ else if (value == TransferEncoding.EightBit)
+ {
+ Headers[MailHeaderInfo.GetString(MailHeaderID.ContentTransferEncoding)] = "8bit";
+ }
+ else
+ {
+ throw new NotSupportedException(SR.Format(SR.MimeTransferEncodingNotSupported, value));
+ }
+ }
+ }
+
+ internal void SetContent(Stream stream)
+ {
+ if (stream == null)
+ {
+ throw new ArgumentNullException(nameof(stream));
+ }
+
+ if (_streamSet)
+ {
+ _stream.Close();
+ _stream = null;
+ _streamSet = false;
+ }
+
+ _stream = stream;
+ _streamSet = true;
+ _streamUsedOnce = false;
+ TransferEncoding = TransferEncoding.Base64;
+ }
+
+ internal void SetContent(Stream stream, string name, string mimeType)
+ {
+ if (stream == null)
+ {
+ throw new ArgumentNullException(nameof(stream));
+ }
+
+ if (mimeType != null && mimeType != string.Empty)
+ {
+ _contentType = new ContentType(mimeType);
+ }
+ if (name != null && name != string.Empty)
+ {
+ ContentType.Name = name;
+ }
+ SetContent(stream);
+ }
+
+ internal void SetContent(Stream stream, ContentType contentType)
+ {
+ if (stream == null)
+ {
+ throw new ArgumentNullException(nameof(stream));
+ }
+ this._contentType = contentType;
+ SetContent(stream);
+ }
+
+ internal void Complete(IAsyncResult result, Exception e)
+ {
+ //if we already completed and we got called again,
+ //it mean's that there was an exception in the callback and we
+ //should just rethrow it.
+
+ MimePartContext context = (MimePartContext)result.AsyncState;
+ if (context.completed)
+ {
+ throw e;
+ }
+
+ try
+ {
+ if (context.outputStream != null)
+ {
+ context.outputStream.Close();
+ }
+ }
+ catch (Exception ex)
+ {
+ if (e == null)
+ {
+ e = ex;
+ }
+ }
+ context.completed = true;
+ context.result.InvokeCallback(e);
+ }
+
+
+ internal void ReadCallback(IAsyncResult result)
+ {
+ if (result.CompletedSynchronously)
+ {
+ return;
+ }
+
+ ((MimePartContext)result.AsyncState).completedSynchronously = false;
+
+ try
+ {
+ ReadCallbackHandler(result);
+ }
+ catch (Exception e)
+ {
+ Complete(result, e);
+ }
+ }
+
+ internal void ReadCallbackHandler(IAsyncResult result)
+ {
+ MimePartContext context = (MimePartContext)result.AsyncState;
+ context.bytesLeft = Stream.EndRead(result);
+ if (context.bytesLeft > 0)
+ {
+ IAsyncResult writeResult = context.outputStream.BeginWrite(context.buffer, 0, context.bytesLeft, _writeCallback, context);
+ if (writeResult.CompletedSynchronously)
+ {
+ WriteCallbackHandler(writeResult);
+ }
+ }
+ else
+ {
+ Complete(result, null);
+ }
+ }
+
+ internal void WriteCallback(IAsyncResult result)
+ {
+ if (result.CompletedSynchronously)
+ {
+ return;
+ }
+
+ ((MimePartContext)result.AsyncState).completedSynchronously = false;
+
+ try
+ {
+ WriteCallbackHandler(result);
+ }
+ catch (Exception e)
+ {
+ Complete(result, e);
+ }
+ }
+
+ internal void WriteCallbackHandler(IAsyncResult result)
+ {
+ MimePartContext context = (MimePartContext)result.AsyncState;
+ context.outputStream.EndWrite(result);
+ IAsyncResult readResult = Stream.BeginRead(context.buffer, 0, context.buffer.Length, _readCallback, context);
+ if (readResult.CompletedSynchronously)
+ {
+ ReadCallbackHandler(readResult);
+ }
+ }
+
+ internal Stream GetEncodedStream(Stream stream)
+ {
+ Stream outputStream = stream;
+
+ if (TransferEncoding == TransferEncoding.Base64)
+ {
+ outputStream = new Base64Stream(outputStream, new Base64WriteStateInfo());
+ }
+ else if (TransferEncoding == TransferEncoding.QuotedPrintable)
+ {
+ outputStream = new QuotedPrintableStream(outputStream, true);
+ }
+ else if (TransferEncoding == TransferEncoding.SevenBit || TransferEncoding == TransferEncoding.EightBit)
+ {
+ outputStream = new EightBitStream(outputStream);
+ }
+
+ return outputStream;
+ }
+
+ internal void ContentStreamCallbackHandler(IAsyncResult result)
+ {
+ MimePartContext context = (MimePartContext)result.AsyncState;
+ Stream outputStream = context.writer.EndGetContentStream(result);
+ context.outputStream = GetEncodedStream(outputStream);
+
+ _readCallback = new AsyncCallback(ReadCallback);
+ _writeCallback = new AsyncCallback(WriteCallback);
+ IAsyncResult readResult = Stream.BeginRead(context.buffer, 0, context.buffer.Length, _readCallback, context);
+ if (readResult.CompletedSynchronously)
+ {
+ ReadCallbackHandler(readResult);
+ }
+ }
+
+ internal void ContentStreamCallback(IAsyncResult result)
+ {
+ if (result.CompletedSynchronously)
+ {
+ return;
+ }
+
+ ((MimePartContext)result.AsyncState).completedSynchronously = false;
+
+ try
+ {
+ ContentStreamCallbackHandler(result);
+ }
+ catch (Exception e)
+ {
+ Complete(result, e);
+ }
+ }
+
+ internal class MimePartContext
+ {
+ internal MimePartContext(BaseWriter writer, LazyAsyncResult result)
+ {
+ this.writer = writer;
+ this.result = result;
+ buffer = new byte[maxBufferSize];
+ }
+
+ internal Stream outputStream;
+ internal LazyAsyncResult result;
+ internal int bytesLeft;
+ internal BaseWriter writer;
+ internal byte[] buffer;
+ internal bool completed;
+ internal bool completedSynchronously = true;
+ }
+
+ internal override IAsyncResult BeginSend(BaseWriter writer, AsyncCallback callback, bool allowUnicode, object state)
+ {
+ PrepareHeaders(allowUnicode);
+ writer.WriteHeaders(Headers, allowUnicode);
+ MimePartAsyncResult result = new MimePartAsyncResult(this, state, callback);
+ MimePartContext context = new MimePartContext(writer, result);
+
+ ResetStream();
+ _streamUsedOnce = true;
+ IAsyncResult contentResult = writer.BeginGetContentStream(new AsyncCallback(ContentStreamCallback), context);
+ if (contentResult.CompletedSynchronously)
+ {
+ ContentStreamCallbackHandler(contentResult);
+ }
+ return result;
+ }
+
+ internal override void Send(BaseWriter writer, bool allowUnicode)
+ {
+ if (Stream != null)
+ {
+ byte[] buffer = new byte[maxBufferSize];
+
+ PrepareHeaders(allowUnicode);
+ writer.WriteHeaders(Headers, allowUnicode);
+
+ Stream outputStream = writer.GetContentStream();
+ outputStream = GetEncodedStream(outputStream);
+
+ int read;
+
+ ResetStream();
+ _streamUsedOnce = true;
+
+ while ((read = Stream.Read(buffer, 0, maxBufferSize)) > 0)
+ {
+ outputStream.Write(buffer, 0, read);
+ }
+ outputStream.Close();
+ }
+ }
+
+ //Ensures that if we've used the stream once, we will either reset it to the origin, or throw.
+ internal void ResetStream()
+ {
+ if (_streamUsedOnce)
+ {
+ if (Stream.CanSeek)
+ {
+ Stream.Seek(0, SeekOrigin.Begin);
+ _streamUsedOnce = false;
+ }
+ else
+ {
+ throw new InvalidOperationException(SR.MimePartCantResetStream);
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mime/MultiAsyncResult.cs b/src/System.Net.Mime/src/System/Net/Mime/MultiAsyncResult.cs
new file mode 100644
index 0000000000..74553b58a5
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mime/MultiAsyncResult.cs
@@ -0,0 +1,50 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Threading;
+
+namespace System.Net.Mime
+{
+ internal sealed class MultiAsyncResult : LazyAsyncResult
+ {
+ private readonly object _context;
+ private int _outstanding;
+
+ internal MultiAsyncResult(object context, AsyncCallback callback, object state) : base(context, state, callback)
+ {
+ _context = context;
+ }
+
+ internal object Context => _context;
+
+ internal void Enter() => Increment();
+
+ internal void Leave() => Decrement();
+
+ internal void Leave(object result)
+ {
+ Result = result;
+ Decrement();
+ }
+
+ private void Decrement()
+ {
+ if (Interlocked.Decrement(ref _outstanding) == -1)
+ {
+ InvokeCallback(Result);
+ }
+ }
+
+ private void Increment() => Interlocked.Increment(ref _outstanding);
+
+ internal void CompleteSequence() => Decrement();
+
+ internal static object End(IAsyncResult result)
+ {
+ MultiAsyncResult thisPtr = (MultiAsyncResult)result;
+ thisPtr.InternalWaitForCompletion();
+ return thisPtr.Result;
+ }
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mime/QEncodedStream.cs b/src/System.Net.Mime/src/System/Net/Mime/QEncodedStream.cs
new file mode 100644
index 0000000000..7241b2c636
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mime/QEncodedStream.cs
@@ -0,0 +1,400 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.IO;
+using System.Text;
+using System.Net.Http;
+using System.Diagnostics;
+
+namespace System.Net.Mime
+{
+ /// <summary>
+ /// This stream performs in-place decoding of quoted-printable
+ /// encoded streams. Encoding requires copying into a separate
+ /// buffer as the data being encoded will most likely grow.
+ /// Encoding and decoding is done transparently to the caller.
+ /// </summary>
+ internal class QEncodedStream : DelegatingStream, IEncodableStream
+ {
+ //folding takes up 3 characters "\r\n "
+ private const int SizeOfFoldingCRLF = 3;
+
+ private static readonly byte[] s_hexDecodeMap = new byte[]
+ {
+ // 0 1 2 3 4 5 6 7 8 9 A B C D E F
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 0
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 1
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 2
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,255,255,255,255,255,255, // 3
+ 255, 10, 11, 12, 13, 14, 15,255,255,255,255,255,255,255,255,255, // 4
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 5
+ 255, 10, 11, 12, 13, 14, 15,255,255,255,255,255,255,255,255,255, // 6
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 7
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 8
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 9
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // A
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // B
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // C
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // D
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // E
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // F
+ };
+
+ //bytes that correspond to the hex char representations in ASCII (0-9, A-F)
+ private static readonly byte[] s_hexEncodeMap = new byte[] { 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70 };
+
+ private ReadStateInfo _readState;
+ private WriteStateInfoBase _writeState;
+
+ internal QEncodedStream(WriteStateInfoBase wsi) :
+ base(new MemoryStream()) // TODO: what should this be?!?!!
+ {
+ _writeState = wsi;
+ }
+
+ private ReadStateInfo ReadState => _readState ?? (_readState = new ReadStateInfo());
+
+ internal WriteStateInfoBase WriteState => _writeState;
+
+ public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException(nameof(buffer));
+ }
+ if (offset < 0 || offset > buffer.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset));
+ }
+ if (offset + count > buffer.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
+ }
+
+ WriteAsyncResult result = new WriteAsyncResult(this, buffer, offset, count, callback, state);
+ result.Write();
+ return result;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ FlushInternal();
+ base.Dispose(disposing);
+ }
+
+ public unsafe int DecodeBytes(byte[] buffer, int offset, int count)
+ {
+ fixed (byte* pBuffer = buffer)
+ {
+ byte* start = pBuffer + offset;
+ byte* source = start;
+ byte* dest = start;
+ byte* end = start + count;
+
+ // if the last read ended in a partially decoded
+ // sequence, pick up where we left off.
+ if (ReadState.IsEscaped)
+ {
+ // this will be -1 if the previous read ended
+ // with an escape character.
+ if (ReadState.Byte == -1)
+ {
+ // if we only read one byte from the underlying
+ // stream, we'll need to save the byte and
+ // ask for more.
+ if (count == 1)
+ {
+ ReadState.Byte = *source;
+ return 0;
+ }
+
+ // '=\r\n' means a soft (aka. invisible) CRLF sequence...
+ if (source[0] != '\r' || source[1] != '\n')
+ {
+ byte b1 = s_hexDecodeMap[source[0]];
+ byte b2 = s_hexDecodeMap[source[1]];
+ if (b1 == 255)
+ throw new FormatException(SR.Format(SR.InvalidHexDigit, b1));
+ if (b2 == 255)
+ throw new FormatException(SR.Format(SR.InvalidHexDigit, b2));
+
+ *dest++ = (byte)((b1 << 4) + b2);
+ }
+
+ source += 2;
+ }
+ else
+ {
+ // '=\r\n' means a soft (aka. invisible) CRLF sequence...
+ if (ReadState.Byte != '\r' || *source != '\n')
+ {
+ byte b1 = s_hexDecodeMap[ReadState.Byte];
+ byte b2 = s_hexDecodeMap[*source];
+ if (b1 == 255)
+ throw new FormatException(SR.Format(SR.InvalidHexDigit, b1));
+ if (b2 == 255)
+ throw new FormatException(SR.Format(SR.InvalidHexDigit, b2));
+ *dest++ = (byte)((b1 << 4) + b2);
+ }
+ source++;
+ }
+ // reset state for next read.
+ ReadState.IsEscaped = false;
+ ReadState.Byte = -1;
+ }
+
+ // Here's where most of the decoding takes place.
+ // We'll loop around until we've inspected all the
+ // bytes read.
+ while (source < end)
+ {
+ // if the source is not an escape character, then
+ // just copy as-is.
+ if (*source != '=')
+ {
+ if (*source == '_')
+ {
+ *dest++ = (byte)' ';
+ source++;
+ }
+ else
+ {
+ *dest++ = *source++;
+ }
+ }
+ else
+ {
+ // determine where we are relative to the end
+ // of the data. If we don't have enough data to
+ // decode the escape sequence, save off what we
+ // have and continue the decoding in the next
+ // read. Otherwise, decode the data and copy
+ // into dest.
+ switch (end - source)
+ {
+ case 2:
+ ReadState.Byte = source[1];
+ goto case 1;
+ case 1:
+ ReadState.IsEscaped = true;
+ goto EndWhile;
+ default:
+ if (source[1] != '\r' || source[2] != '\n')
+ {
+ byte b1 = s_hexDecodeMap[source[1]];
+ byte b2 = s_hexDecodeMap[source[2]];
+ if (b1 == 255)
+ throw new FormatException(SR.Format(SR.InvalidHexDigit, b1));
+ if (b2 == 255)
+ throw new FormatException(SR.Format(SR.InvalidHexDigit, b2));
+
+ *dest++ = (byte)((b1 << 4) + b2);
+ }
+ source += 3;
+ break;
+ }
+ }
+ }
+ EndWhile:
+ count = (int)(dest - start);
+ }
+
+ return count;
+ }
+
+ public int EncodeBytes(byte[] buffer, int offset, int count)
+ {
+ // Add Encoding header, if any. e.g. =?encoding?b?
+ _writeState.AppendHeader();
+
+ // Scan one character at a time looking for chars that need to be encoded.
+ int cur = offset;
+ for (; cur < count + offset; cur++)
+ {
+ if ( // Fold if we're before a whitespace and encoding another character would be too long
+ ((WriteState.CurrentLineLength + SizeOfFoldingCRLF + WriteState.FooterLength >= WriteState.MaxLineLength)
+ && (buffer[cur] == ' ' || buffer[cur] == '\t' || buffer[cur] == '\r' || buffer[cur] == '\n'))
+ // Or just adding the footer would be too long.
+ || (WriteState.CurrentLineLength + _writeState.FooterLength >= WriteState.MaxLineLength)
+ )
+ {
+ WriteState.AppendCRLF(true);
+ }
+
+ // We don't need to worry about RFC 2821 4.5.2 (encoding first dot on a line),
+ // it is done by the underlying 7BitStream
+
+ //always encode CRLF
+ if (buffer[cur] == '\r' && cur + 1 < count + offset && buffer[cur + 1] == '\n')
+ {
+ cur++;
+
+ //the encoding for CRLF is =0D=0A
+ WriteState.Append((byte)'=', (byte)'0', (byte)'D', (byte)'=', (byte)'0', (byte)'A');
+ }
+ else if (buffer[cur] == ' ')
+ {
+ //spaces should be escaped as either '_' or '=20' and
+ //we have chosen '_' for parity with other email client
+ //behavior
+ WriteState.Append((byte)'_');
+ }
+ // RFC 2047 Section 5 part 3 also allows for !*+-/ but these arn't required in headers.
+ // Conservatively encode anything but letters or digits.
+ else if (IsAsciiLetterOrDigit((char)buffer[cur]))
+ {
+ // Just a regular printable ascii char.
+ WriteState.Append(buffer[cur]);
+ }
+ else
+ {
+ //append an = to indicate an encoded character
+ WriteState.Append((byte)'=');
+ //shift 4 to get the first four bytes only and look up the hex digit
+ WriteState.Append(s_hexEncodeMap[buffer[cur] >> 4]);
+ //clear the first four bytes to get the last four and look up the hex digit
+ WriteState.Append(s_hexEncodeMap[buffer[cur] & 0xF]);
+ }
+ }
+ WriteState.AppendFooter();
+ return cur - offset;
+ }
+
+ private static bool IsAsciiLetterOrDigit(char character) =>
+ IsAsciiLetter(character) || (character >= '0' && character <= '9');
+
+ private static bool IsAsciiLetter(char character) =>
+ (character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z');
+
+ public Stream GetStream() => this;
+
+ public string GetEncodedString() => Encoding.ASCII.GetString(WriteState.Buffer, 0, WriteState.Length);
+
+ public override void EndWrite(IAsyncResult asyncResult) => WriteAsyncResult.End(asyncResult);
+
+ public override void Flush()
+ {
+ FlushInternal();
+ base.Flush();
+ }
+
+ private void FlushInternal()
+ {
+ if (_writeState != null && _writeState.Length > 0)
+ {
+ base.Write(WriteState.Buffer, 0, WriteState.Length);
+ WriteState.Reset();
+ }
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException(nameof(buffer));
+ }
+ if (offset < 0 || offset > buffer.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset));
+ }
+ if (offset + count > buffer.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
+ }
+
+ int written = 0;
+ for (;;)
+ {
+ written += EncodeBytes(buffer, offset + written, count - written);
+ if (written < count)
+ {
+ FlushInternal();
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+
+ private sealed class ReadStateInfo
+ {
+ internal bool IsEscaped { get; set; }
+ internal short Byte { get; set; } = -1;
+ }
+
+ private class WriteAsyncResult : LazyAsyncResult
+ {
+ private readonly static AsyncCallback s_onWrite = OnWrite;
+
+ private readonly QEncodedStream _parent;
+ private readonly byte[] _buffer;
+ private readonly int _offset;
+ private readonly int _count;
+
+ private int _written;
+
+ internal WriteAsyncResult(QEncodedStream parent, byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ : base(null, state, callback)
+ {
+ _parent = parent;
+ _buffer = buffer;
+ _offset = offset;
+ _count = count;
+ }
+
+ private void CompleteWrite(IAsyncResult result)
+ {
+ _parent.BaseStream.EndWrite(result);
+ _parent.WriteState.Reset();
+ }
+
+ internal static void End(IAsyncResult result)
+ {
+ WriteAsyncResult thisPtr = (WriteAsyncResult)result;
+ thisPtr.InternalWaitForCompletion();
+ Debug.Assert(thisPtr._written == thisPtr._count);
+ }
+
+ private static void OnWrite(IAsyncResult result)
+ {
+ if (!result.CompletedSynchronously)
+ {
+ WriteAsyncResult thisPtr = (WriteAsyncResult)result.AsyncState;
+ try
+ {
+ thisPtr.CompleteWrite(result);
+ thisPtr.Write();
+ }
+ catch (Exception e)
+ {
+ thisPtr.InvokeCallback(e);
+ }
+ }
+ }
+
+ internal void Write()
+ {
+ for (;;)
+ {
+ _written += _parent.EncodeBytes(_buffer, _offset + _written, _count - _written);
+ if (_written < _count)
+ {
+ IAsyncResult result = _parent.BaseStream.BeginWrite(_parent.WriteState.Buffer, 0, _parent.WriteState.Length, s_onWrite, this);
+ if (!result.CompletedSynchronously)
+ {
+ break;
+ }
+ CompleteWrite(result);
+ }
+ else
+ {
+ InvokeCallback();
+ break;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mime/QuotedPrintableStream.cs b/src/System.Net.Mime/src/System/Net/Mime/QuotedPrintableStream.cs
new file mode 100644
index 0000000000..5ce5d47b71
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mime/QuotedPrintableStream.cs
@@ -0,0 +1,444 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.IO;
+using System.Text;
+using System.Net.Http;
+using System.Diagnostics;
+
+namespace System.Net.Mime
+{
+ /// <summary>
+ /// This stream performs in-place decoding of quoted-printable
+ /// encoded streams. Encoding requires copying into a separate
+ /// buffer as the data being encoded will most likely grow.
+ /// Encoding and decoding is done transparently to the caller.
+ ///
+ /// This stream should only be used for the e-mail content.
+ /// Use QEncodedStream for encoding headers.
+ /// </summary>
+ internal class QuotedPrintableStream : DelegatingStream, IEncodableStream
+ {
+ //should we encode CRLF or not?
+ private bool _encodeCRLF;
+
+ //number of bytes needed for a soft CRLF in folding
+ private const int SizeOfSoftCRLF = 3;
+
+ //each encoded byte occupies three bytes when encoded
+ private const int SizeOfEncodedChar = 3;
+
+ //it takes six bytes to encode a CRLF character (a CRLF that does not indicate folding)
+ private const int SizeOfEncodedCRLF = 6;
+
+ //if we aren't encoding CRLF then it occupies two chars
+ private const int SizeOfNonEncodedCRLF = 2;
+
+ private static readonly byte[] s_hexDecodeMap = new byte[]
+ {
+ // 0 1 2 3 4 5 6 7 8 9 A B C D E F
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 0
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 1
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 2
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,255,255,255,255,255,255, // 3
+ 255, 10, 11, 12, 13, 14, 15,255,255,255,255,255,255,255,255,255, // 4
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 5
+ 255, 10, 11, 12, 13, 14, 15,255,255,255,255,255,255,255,255,255, // 6
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 7
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 8
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 9
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // A
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // B
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // C
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // D
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // E
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // F
+ };
+
+ private static readonly byte[] s_hexEncodeMap = new byte[] { 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70 };
+
+ private int _lineLength;
+ private ReadStateInfo _readState;
+ private WriteStateInfoBase _writeState;
+
+ /// <summary>
+ /// ctor.
+ /// </summary>
+ /// <param name="stream">Underlying stream</param>
+ /// <param name="lineLength">Preferred maximum line-length for writes</param>
+ internal QuotedPrintableStream(Stream stream, int lineLength) : base(stream)
+ {
+ if (lineLength < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(lineLength));
+ }
+
+ _lineLength = lineLength;
+ }
+
+ internal QuotedPrintableStream(Stream stream, bool encodeCRLF) : this(stream, EncodedStreamFactory.DefaultMaxLineLength)
+ {
+ _encodeCRLF = encodeCRLF;
+ }
+
+ private ReadStateInfo ReadState => _readState ?? (_readState = new ReadStateInfo());
+
+ internal WriteStateInfoBase WriteState => _writeState ?? (_writeState = new WriteStateInfoBase(1024, null, null, _lineLength));
+
+ public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException(nameof(buffer));
+ }
+ if (offset < 0 || offset > buffer.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset));
+ }
+ if (offset + count > buffer.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
+ }
+
+ WriteAsyncResult result = new WriteAsyncResult(this, buffer, offset, count, callback, state);
+ result.Write();
+ return result;
+ }
+
+ public override void Close()
+ {
+ FlushInternal();
+ base.Close();
+ }
+
+ public unsafe int DecodeBytes(byte[] buffer, int offset, int count)
+ {
+ fixed (byte* pBuffer = buffer)
+ {
+ byte* start = pBuffer + offset;
+ byte* source = start;
+ byte* dest = start;
+ byte* end = start + count;
+
+ // if the last read ended in a partially decoded
+ // sequence, pick up where we left off.
+ if (ReadState.IsEscaped)
+ {
+ // this will be -1 if the previous read ended
+ // with an escape character.
+ if (ReadState.Byte == -1)
+ {
+ // if we only read one byte from the underlying
+ // stream, we'll need to save the byte and
+ // ask for more.
+ if (count == 1)
+ {
+ ReadState.Byte = *source;
+ return 0;
+ }
+
+ // '=\r\n' means a soft (aka. invisible) CRLF sequence...
+ if (source[0] != '\r' || source[1] != '\n')
+ {
+ byte b1 = s_hexDecodeMap[source[0]];
+ byte b2 = s_hexDecodeMap[source[1]];
+ if (b1 == 255)
+ throw new FormatException(SR.Format(SR.InvalidHexDigit, b1));
+ if (b2 == 255)
+ throw new FormatException(SR.Format(SR.InvalidHexDigit, b2));
+
+ *dest++ = (byte)((b1 << 4) + b2);
+ }
+
+ source += 2;
+ }
+ else
+ {
+ // '=\r\n' means a soft (aka. invisible) CRLF sequence...
+ if (ReadState.Byte != '\r' || *source != '\n')
+ {
+ byte b1 = s_hexDecodeMap[ReadState.Byte];
+ byte b2 = s_hexDecodeMap[*source];
+ if (b1 == 255)
+ throw new FormatException(SR.Format(SR.InvalidHexDigit, b1));
+ if (b2 == 255)
+ throw new FormatException(SR.Format(SR.InvalidHexDigit, b2));
+ *dest++ = (byte)((b1 << 4) + b2);
+ }
+ source++;
+ }
+ // reset state for next read.
+ ReadState.IsEscaped = false;
+ ReadState.Byte = -1;
+ }
+
+ // Here's where most of the decoding takes place.
+ // We'll loop around until we've inspected all the
+ // bytes read.
+ while (source < end)
+ {
+ // if the source is not an escape character, then
+ // just copy as-is.
+ if (*source != '=')
+ {
+ *dest++ = *source++;
+ }
+ else
+ {
+ // determine where we are relative to the end
+ // of the data. If we don't have enough data to
+ // decode the escape sequence, save off what we
+ // have and continue the decoding in the next
+ // read. Otherwise, decode the data and copy
+ // into dest.
+ switch (end - source)
+ {
+ case 2:
+ ReadState.Byte = source[1];
+ goto case 1;
+ case 1:
+ ReadState.IsEscaped = true;
+ goto EndWhile;
+ default:
+ if (source[1] != '\r' || source[2] != '\n')
+ {
+ byte b1 = s_hexDecodeMap[source[1]];
+ byte b2 = s_hexDecodeMap[source[2]];
+ if (b1 == 255)
+ throw new FormatException(SR.Format(SR.InvalidHexDigit, b1));
+ if (b2 == 255)
+ throw new FormatException(SR.Format(SR.InvalidHexDigit, b2));
+
+ *dest++ = (byte)((b1 << 4) + b2);
+ }
+ source += 3;
+ break;
+ }
+ }
+ }
+ EndWhile:
+ count = (int)(dest - start);
+ }
+
+ return count;
+ }
+
+ public int EncodeBytes(byte[] buffer, int offset, int count)
+ {
+ int cur = offset;
+ for (; cur < count + offset; cur++)
+ {
+ //only fold if we're before a whitespace or if we're at the line limit
+ //add two to the encoded Byte Length to be conservative so that we guarantee that the line length is acceptable
+ if ((_lineLength != -1 && WriteState.CurrentLineLength + SizeOfEncodedChar + 2 >= _lineLength && (buffer[cur] == ' ' ||
+ buffer[cur] == '\t' || buffer[cur] == '\r' || buffer[cur] == '\n')) ||
+ _writeState.CurrentLineLength + SizeOfEncodedChar + 2 >= EncodedStreamFactory.DefaultMaxLineLength)
+ {
+ if (WriteState.Buffer.Length - WriteState.Length < SizeOfSoftCRLF)
+ {
+ return cur - offset; //ok because folding happens externally
+ }
+
+ WriteState.Append((byte)'=');
+ WriteState.AppendCRLF(false);
+ }
+
+ // We don't need to worry about RFC 2821 4.5.2 (encoding first dot on a line),
+ // it is done by the underlying 7BitStream
+
+ //detect a CRLF in the input and encode it.
+ if (buffer[cur] == '\r' && cur + 1 < count + offset && buffer[cur + 1] == '\n')
+ {
+ if (WriteState.Buffer.Length - WriteState.Length < (_encodeCRLF ? SizeOfEncodedCRLF : SizeOfNonEncodedCRLF))
+ {
+ return cur - offset;
+ }
+ cur++;
+
+ if (_encodeCRLF)
+ {
+ // The encoding for CRLF is =0D=0A
+ WriteState.Append((byte)'=', (byte)'0', (byte)'D', (byte)'=', (byte)'0', (byte)'A');
+ }
+ else
+ {
+ WriteState.AppendCRLF(false);
+ }
+ }
+ //ascii chars less than 32 (control chars) and greater than 126 (non-ascii) are not allowed so we have to encode
+ else if ((buffer[cur] < 32 && buffer[cur] != '\t') ||
+ buffer[cur] == '=' ||
+ buffer[cur] > 126)
+ {
+ if (WriteState.Buffer.Length - WriteState.Length < SizeOfSoftCRLF)
+ {
+ return cur - offset;
+ }
+
+ //append an = to indicate an encoded character
+ WriteState.Append((byte)'=');
+ //shift 4 to get the first four bytes only and look up the hex digit
+ WriteState.Append(s_hexEncodeMap[buffer[cur] >> 4]);
+ //clear the first four bytes to get the last four and look up the hex digit
+ WriteState.Append(s_hexEncodeMap[buffer[cur] & 0xF]);
+ }
+ else
+ {
+ if (WriteState.Buffer.Length - WriteState.Length < 1)
+ {
+ return cur - offset;
+ }
+
+ //detect special case: is whitespace at end of line? we must encode it if it is
+ if ((buffer[cur] == (byte)'\t' || buffer[cur] == (byte)' ') &&
+ (cur + 1 >= count + offset))
+ {
+ if (WriteState.Buffer.Length - WriteState.Length < SizeOfEncodedChar)
+ {
+ return cur - offset;
+ }
+
+ //append an = to indicate an encoded character
+ WriteState.Append((byte)'=');
+ //shift 4 to get the first four bytes only and look up the hex digit
+ WriteState.Append(s_hexEncodeMap[buffer[cur] >> 4]);
+ //clear the first four bytes to get the last four and look up the hex digit
+ WriteState.Append(s_hexEncodeMap[buffer[cur] & 0xF]);
+ }
+ else
+ {
+ WriteState.Append(buffer[cur]);
+ }
+ }
+ }
+ return cur - offset;
+ }
+
+ public Stream GetStream() => this;
+
+ public string GetEncodedString() => Encoding.ASCII.GetString(WriteState.Buffer, 0, WriteState.Length);
+
+ public override void EndWrite(IAsyncResult asyncResult) => WriteAsyncResult.End(asyncResult);
+
+ public override void Flush()
+ {
+ FlushInternal();
+ base.Flush();
+ }
+
+ private void FlushInternal()
+ {
+ if (_writeState != null && _writeState.Length > 0)
+ {
+ base.Write(WriteState.Buffer, 0, WriteState.Length);
+ WriteState.BufferFlushed();
+ }
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException(nameof(buffer));
+ }
+ if (offset < 0 || offset > buffer.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset));
+ }
+ if (offset + count > buffer.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
+ }
+
+ int written = 0;
+ for (;;)
+ {
+ written += EncodeBytes(buffer, offset + written, count - written);
+ if (written < count)
+ {
+ FlushInternal();
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+
+ private sealed class ReadStateInfo
+ {
+ internal bool IsEscaped { get; set; }
+ internal short Byte { get; set; } = -1;
+ }
+
+ private sealed class WriteAsyncResult : LazyAsyncResult
+ {
+ private readonly QuotedPrintableStream _parent;
+ private readonly byte[] _buffer;
+ private readonly int _offset;
+ private readonly int _count;
+ private readonly static AsyncCallback s_onWrite = new AsyncCallback(OnWrite);
+ private int _written;
+
+ internal WriteAsyncResult(QuotedPrintableStream parent, byte[] buffer, int offset, int count, AsyncCallback callback, object state) : base(null, state, callback)
+ {
+ _parent = parent;
+ _buffer = buffer;
+ _offset = offset;
+ _count = count;
+ }
+
+ private void CompleteWrite(IAsyncResult result)
+ {
+ _parent.BaseStream.EndWrite(result);
+ _parent.WriteState.BufferFlushed();
+ }
+
+ internal static void End(IAsyncResult result)
+ {
+ WriteAsyncResult thisPtr = (WriteAsyncResult)result;
+ thisPtr.InternalWaitForCompletion();
+ Debug.Assert(thisPtr._written == thisPtr._count);
+ }
+
+ private static void OnWrite(IAsyncResult result)
+ {
+ if (!result.CompletedSynchronously)
+ {
+ WriteAsyncResult thisPtr = (WriteAsyncResult)result.AsyncState;
+ try
+ {
+ thisPtr.CompleteWrite(result);
+ thisPtr.Write();
+ }
+ catch (Exception e)
+ {
+ thisPtr.InvokeCallback(e);
+ }
+ }
+ }
+
+ internal void Write()
+ {
+ for (;;)
+ {
+ _written += _parent.EncodeBytes(_buffer, _offset + _written, _count - _written);
+ if (_written < _count)
+ {
+ IAsyncResult result = _parent.BaseStream.BeginWrite(_parent.WriteState.Buffer, 0, _parent.WriteState.Length, s_onWrite, this);
+ if (!result.CompletedSynchronously)
+ break;
+ CompleteWrite(result);
+ }
+ else
+ {
+ InvokeCallback();
+ break;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mime/SmtpDateTime.cs b/src/System.Net.Mime/src/System/Net/Mime/SmtpDateTime.cs
new file mode 100644
index 0000000000..09fd6c81d5
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mime/SmtpDateTime.cs
@@ -0,0 +1,422 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Globalization;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace System.Net.Mime
+{
+ #region RFC2822 date time string format description
+ // Format of Date Time string as described by RFC 2822 section 4.3 which obsoletes
+ // some field formats that were allowed under RFC 822
+
+ // date-time = [ day-of-week "," ] date FWS time [CFWS]
+ // day-of-week = ([FWS] day-name) / obs-day-of-week
+ // day-name = "Mon" / "Tue" / "Wed" / "Thu" / "Fri" / "Sat" / "Sun"
+ // date = day month year
+ // year = 4*DIGIT / obs-year
+ // month = (FWS month-name FWS) / obs-month
+ // month-name = "Jan" / "Feb" / "Mar" / "Apr" / "May" / "Jun" / "Jul" / "Aug" /
+ // "Sep" / "Oct" / "Nov" / "Dec"
+ // day = ([FWS] 1*2DIGIT) / obs-day
+ // time = time-of-day FWS zone
+ // time-of-day = hour ":" minute [ ":" second ]
+ // hour = 2DIGIT / obs-hour
+ // minute = 2DIGIT / obs-minute
+ // second = 2DIGIT / obs-second
+ // zone = (( "+" / "-" ) 4DIGIT) / obs-zone
+ #endregion
+
+ // stores a Date and a Time Zone. These are parsed and formatted according to the
+ // rules in RFC 2822 section 3.3.
+ // This class is immutable
+ internal class SmtpDateTime
+ {
+ #region constants
+
+ // use this when a time zone is unknown or is not supplied
+ internal const string UnknownTimeZoneDefaultOffset = "-0000";
+ internal const string UtcDefaultTimeZoneOffset = "+0000";
+ internal const int OffsetLength = 5;
+
+ // range for absolute value of minutes. it is not necessary to include a max value for hours since
+ // the two-digit value that is parsed can't exceed the max value of hours, which is 99
+ internal const int MaxMinuteValue = 59;
+
+ // possible valid values for a date string
+ // these do NOT include the timezone
+ internal const string DateFormatWithDayOfWeek = "ddd, dd MMM yyyy HH:mm:ss";
+ internal const string DateFormatWithoutDayOfWeek = "dd MMM yyyy HH:mm:ss";
+ internal const string DateFormatWithDayOfWeekAndNoSeconds = "ddd, dd MMM yyyy HH:mm";
+ internal const string DateFormatWithoutDayOfWeekAndNoSeconds = "dd MMM yyyy HH:mm";
+
+ #endregion
+
+ #region static fields
+
+ // array of all possible date time values
+ // if a string matches any one of these it will be parsed correctly
+ internal readonly static string[] s_validDateTimeFormats = new string[]
+ {
+ DateFormatWithDayOfWeek,
+ DateFormatWithoutDayOfWeek,
+ DateFormatWithDayOfWeekAndNoSeconds,
+ DateFormatWithoutDayOfWeekAndNoSeconds
+ };
+
+ internal readonly static char[] s_allowedWhiteSpaceChars = new char[] { ' ', '\t' };
+
+ internal static readonly IDictionary<string, TimeSpan> s_timeZoneOffsetLookup = InitializeShortHandLookups();
+
+ // a TimeSpan must be between these two values in order for it to be within the range allowed
+ // by RFC 2822
+ internal const long TimeSpanMaxTicks = TimeSpan.TicksPerHour * 99 + TimeSpan.TicksPerMinute * 59;
+
+ // allowed max values for each digit. min value is always 0
+ internal const int OffsetMaxValue = 9959;
+
+ #endregion
+
+ #region static initializers
+
+ internal static IDictionary<string, TimeSpan> InitializeShortHandLookups()
+ {
+ var tempTimeZoneOffsetLookup = new Dictionary<string, TimeSpan>();
+
+ // all well-known short hand time zone values and their semantic equivalents
+ tempTimeZoneOffsetLookup.Add("UT", TimeSpan.Zero); // +0000
+ tempTimeZoneOffsetLookup.Add("GMT", TimeSpan.Zero); // +0000
+ tempTimeZoneOffsetLookup.Add("EDT", new TimeSpan(-4, 0, 0)); // -0400
+ tempTimeZoneOffsetLookup.Add("EST", new TimeSpan(-5, 0, 0)); // -0500
+ tempTimeZoneOffsetLookup.Add("CDT", new TimeSpan(-5, 0, 0)); // -0500
+ tempTimeZoneOffsetLookup.Add("CST", new TimeSpan(-6, 0, 0)); // -0600
+ tempTimeZoneOffsetLookup.Add("MDT", new TimeSpan(-6, 0, 0)); // -0600
+ tempTimeZoneOffsetLookup.Add("MST", new TimeSpan(-7, 0, 0)); // -0700
+ tempTimeZoneOffsetLookup.Add("PDT", new TimeSpan(-7, 0, 0)); // -0700
+ tempTimeZoneOffsetLookup.Add("PST", new TimeSpan(-8, 0, 0)); // -0800
+ return tempTimeZoneOffsetLookup;
+ }
+
+ #endregion
+
+ #region private fields
+
+ private readonly DateTime _date;
+ private readonly TimeSpan _timeZone;
+
+ // true if the time zone is unspecified i.e. -0000
+ // the time zone will usually be specified
+ private readonly bool _unknownTimeZone = false;
+
+ #endregion
+
+ #region constructors
+
+ internal SmtpDateTime(DateTime value)
+ {
+ _date = value;
+
+ switch (value.Kind)
+ {
+ case DateTimeKind.Local:
+ // GetUtcOffset takes local time zone information into account e.g. daylight savings time
+ TimeSpan localTimeZone = TimeZoneInfo.Local.GetUtcOffset(value);
+ _timeZone = ValidateAndGetSanitizedTimeSpan(localTimeZone);
+ break;
+
+ case DateTimeKind.Unspecified:
+ _unknownTimeZone = true;
+ break;
+
+ case DateTimeKind.Utc:
+ _timeZone = TimeSpan.Zero;
+ break;
+ }
+ }
+
+ internal SmtpDateTime(string value)
+ {
+ string timeZoneOffset;
+ _date = ParseValue(value, out timeZoneOffset);
+
+ if (!TryParseTimeZoneString(timeZoneOffset, out _timeZone))
+ {
+ // time zone is unknown
+ _unknownTimeZone = true;
+ }
+ }
+
+ #endregion
+
+ #region internal properties
+
+ internal DateTime Date
+ {
+ get
+ {
+ if (_unknownTimeZone)
+ {
+ return DateTime.SpecifyKind(_date, DateTimeKind.Unspecified);
+ }
+ else
+ {
+ // DateTimeOffset will convert the value of this.date to the time as
+ // specified in this.timeZone
+ DateTimeOffset offset = new DateTimeOffset(_date, _timeZone);
+ return offset.LocalDateTime;
+ }
+ }
+ }
+
+#if DEBUG
+ // this method is only called by test code
+ internal string TimeZone => _unknownTimeZone ? UnknownTimeZoneDefaultOffset : TimeSpanToOffset(_timeZone);
+#endif
+
+ #endregion
+
+ #region internals
+
+ // outputs the RFC 2822 formatted date string including time zone
+ public override string ToString() =>
+ string.Format("{0} {1}", FormatDate(_date), _unknownTimeZone ? UnknownTimeZoneDefaultOffset : TimeSpanToOffset(_timeZone));
+
+ // returns true if the offset is of the form [+|-]dddd and
+ // within the range 0000 to 9959
+ internal void ValidateAndGetTimeZoneOffsetValues(string offset, out bool positive, out int hours, out int minutes)
+ {
+ Debug.Assert(!string.IsNullOrEmpty(offset), "violation of precondition: offset must not be null or empty");
+ Debug.Assert(offset != UnknownTimeZoneDefaultOffset, "Violation of precondition: do not pass an unknown offset");
+ Debug.Assert(offset.StartsWith("-") || offset.StartsWith("+"), "offset initial character was not a + or -");
+
+ if (offset.Length != OffsetLength)
+ {
+ throw new FormatException(SR.MailDateInvalidFormat);
+ }
+
+ positive = offset.StartsWith("+");
+
+ // TryParse will parse in base 10 by default. do not allow any styles of input beyond the default
+ // which is numeric values only
+ if (!int.TryParse(offset.Substring(1, 2), NumberStyles.None, CultureInfo.InvariantCulture, out hours))
+ {
+ throw new FormatException(SR.MailDateInvalidFormat);
+ }
+
+ if (!int.TryParse(offset.Substring(3, 2), NumberStyles.None, CultureInfo.InvariantCulture, out minutes))
+ {
+ throw new FormatException(SR.MailDateInvalidFormat);
+ }
+
+ // we only explicitly validate the minutes. they must be below 59
+ // the hours are implicitly validated as a number formed from a string of length
+ // 2 can only be <= 99
+ if (minutes > MaxMinuteValue)
+ {
+ throw new FormatException(SR.MailDateInvalidFormat);
+ }
+ }
+
+ // returns true if the time zone short hand is all alphabetical characters
+ internal void ValidateTimeZoneShortHandValue(string value)
+ {
+ // time zones can't be empty
+ Debug.Assert(!string.IsNullOrEmpty(value), "violation of precondition: offset must not be null or empty");
+
+ // time zones must all be alphabetical characters
+ for (int i = 0; i < value.Length; i++)
+ {
+ if (!Char.IsLetter(value, i))
+ {
+ throw new FormatException(SR.MailHeaderFieldInvalidCharacter);
+ }
+ }
+ }
+
+ // formats a date only. Does not include time zone
+ internal string FormatDate(DateTime value) => value.ToString("ddd, dd MMM yyyy HH:mm:ss", CultureInfo.InvariantCulture);
+
+ // parses the date and time zone
+ // postconditions:
+ // return value is valid DateTime representation of the Date portion of data
+ // timeZone is the portion of data which should contain the time zone data
+ // timeZone is NOT evaluated by ParseValue
+ internal DateTime ParseValue(string data, out string timeZone)
+ {
+ // check that there is something to parse
+ if (string.IsNullOrEmpty(data))
+ {
+ throw new FormatException(SR.MailDateInvalidFormat);
+ }
+
+ // find the first occurrence of ':'
+ // this tells us where the separator between hour and minute are
+ int indexOfHourSeparator = data.IndexOf(':');
+
+ // no ':' means invalid value
+ if (indexOfHourSeparator == -1)
+ {
+ throw new FormatException(SR.MailHeaderFieldInvalidCharacter);
+ }
+
+ // now we know where hours and minutes are separated. The first whitespace after
+ // that MUST be the separator between the time portion and the timezone portion
+ // timezone may have additional spaces, characters, or comments after it but
+ // this is ok since we'll parse that whole section later
+ int indexOfTimeZoneSeparator = data.IndexOfAny(s_allowedWhiteSpaceChars, indexOfHourSeparator);
+
+ if (indexOfTimeZoneSeparator == -1)
+ {
+ throw new FormatException(SR.MailHeaderFieldInvalidCharacter);
+ }
+
+ // extract the time portion and remove all leading and trailing whitespace
+ string date = data.Substring(0, indexOfTimeZoneSeparator).Trim();
+
+ // attempt to parse the DateTime component.
+ DateTime dateValue;
+ if (!DateTime.TryParseExact(date, s_validDateTimeFormats, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out dateValue))
+ {
+ throw new FormatException(SR.MailDateInvalidFormat);
+ }
+
+ // kind property will be Unspecified since no timezone info was in the date string
+ Debug.Assert(dateValue.Kind == DateTimeKind.Unspecified);
+
+ // extract the second half of the string. This will start with at least one whitespace character.
+ // Trim the string to remove these characters.
+ string timeZoneString = data.Substring(indexOfTimeZoneSeparator).Trim();
+
+ // find, if any, the first whitespace character after the timezone.
+ // These will be CFWS and must be ignored. Remove them.
+ int endOfTimeZoneOffset = timeZoneString.IndexOfAny(s_allowedWhiteSpaceChars);
+
+ if (endOfTimeZoneOffset != -1)
+ {
+ timeZoneString = timeZoneString.Substring(0, endOfTimeZoneOffset);
+ }
+
+ if (string.IsNullOrEmpty(timeZoneString))
+ {
+ throw new FormatException(SR.MailDateInvalidFormat);
+ }
+
+ timeZone = timeZoneString;
+
+ return dateValue;
+ }
+
+ // if this returns true, timeZone is the correct TimeSpan representation of the input
+ // if it returns false then the time zone is unknown and so timeZone must be ignored
+ internal bool TryParseTimeZoneString(string timeZoneString, out TimeSpan timeZone)
+ {
+ // initialize default
+ timeZone = TimeSpan.Zero;
+
+ // see if the zone is the special unspecified case, a numeric offset, or a shorthand string
+ if (timeZoneString == UnknownTimeZoneDefaultOffset)
+ {
+ // The inputed time zone is the special value "unknown", -0000
+ return false;
+ }
+ else if ((timeZoneString[0] == '+' || timeZoneString[0] == '-'))
+ {
+ bool positive;
+ int hours;
+ int minutes;
+
+ ValidateAndGetTimeZoneOffsetValues(timeZoneString, out positive, out hours, out minutes);
+
+ // Apply the negative sign, if applicable, to whichever of hours or minutes is NOT 0.
+ if (!positive)
+ {
+ if (hours != 0)
+ {
+ hours *= -1;
+ }
+ else if (minutes != 0)
+ {
+ minutes *= -1;
+ }
+ }
+
+ timeZone = new TimeSpan(hours, minutes, 0);
+
+ return true;
+ }
+ else
+ {
+ // not an offset so ensure that it contains no invalid characters
+ ValidateTimeZoneShortHandValue(timeZoneString);
+
+ // check if the shorthand value has a semantically equivalent offset
+ if (s_timeZoneOffsetLookup.ContainsKey(timeZoneString))
+ {
+ timeZone = s_timeZoneOffsetLookup[timeZoneString];
+ return true;
+ }
+ }
+
+ // default time zone is the unspecified zone: -0000
+ return false;
+ }
+
+ internal TimeSpan ValidateAndGetSanitizedTimeSpan(TimeSpan span)
+ {
+ // sanitize the time span by removing the seconds and milliseconds. Days are not handled here
+ TimeSpan sanitizedTimeSpan = new TimeSpan(span.Days, span.Hours, span.Minutes, 0, 0);
+
+ // validate range of time span
+ if (Math.Abs(sanitizedTimeSpan.Ticks) > TimeSpanMaxTicks)
+ {
+ throw new FormatException(SR.MailDateInvalidFormat);
+ }
+
+ return sanitizedTimeSpan;
+ }
+
+ // precondition: span must be sanitized and within a valid range
+ internal string TimeSpanToOffset(TimeSpan span)
+ {
+ Debug.Assert(span.Seconds == 0, "Span had seconds value");
+ Debug.Assert(span.Milliseconds == 0, "Span had milliseconds value");
+
+ if (span.Ticks == 0)
+ {
+ return UtcDefaultTimeZoneOffset;
+ }
+ else
+ {
+ // get the total number of hours since TimeSpan.Hours won't go beyond 24
+ // ensure that it's a whole number since the fractional part represents minutes
+ uint hours = (uint)Math.Abs(Math.Floor(span.TotalHours));
+ uint minutes = (uint)Math.Abs(span.Minutes);
+
+ Debug.Assert((hours != 0) || (minutes != 0), "Input validation ensures hours or minutes isn't zero");
+
+ string output = span.Ticks > 0 ? "+" : "-";
+
+ // hours and minutes must be two digits
+ if (hours < 10)
+ {
+ output += "0";
+ }
+
+ output += hours.ToString();
+
+ if (minutes < 10)
+ {
+ output += "0";
+ }
+
+ output += minutes.ToString();
+
+ return output;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mime/TrackingStringDictionary.cs b/src/System.Net.Mime/src/System/Net/Mime/TrackingStringDictionary.cs
new file mode 100644
index 0000000000..7a32ad067c
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mime/TrackingStringDictionary.cs
@@ -0,0 +1,73 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Specialized;
+
+namespace System.Net
+{
+ internal sealed class TrackingStringDictionary : StringDictionary
+ {
+ private readonly bool _isReadOnly;
+ private bool _isChanged;
+
+ internal TrackingStringDictionary() : this(false)
+ {
+ }
+
+ internal TrackingStringDictionary(bool isReadOnly)
+ {
+ _isReadOnly = isReadOnly;
+ }
+
+ internal bool IsChanged { get { return _isChanged; } set { _isChanged = value; } }
+
+ public override void Add(string key, string value)
+ {
+ if (_isReadOnly)
+ {
+ throw new InvalidOperationException(SR.MailCollectionIsReadOnly);
+ }
+
+ base.Add(key, value);
+ _isChanged = true;
+ }
+
+ public override void Clear()
+ {
+ if (_isReadOnly)
+ {
+ throw new InvalidOperationException(SR.MailCollectionIsReadOnly);
+ }
+
+ base.Clear();
+ _isChanged = true;
+ }
+
+ public override void Remove(string key)
+ {
+ if (_isReadOnly)
+ {
+ throw new InvalidOperationException(SR.MailCollectionIsReadOnly);
+ }
+
+ base.Remove(key);
+ _isChanged = true;
+ }
+
+ public override string this[string key]
+ {
+ get { return base[key]; }
+ set
+ {
+ if (_isReadOnly)
+ {
+ throw new InvalidOperationException(SR.MailCollectionIsReadOnly);
+ }
+
+ base[key] = value;
+ _isChanged = true;
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mime/TrackingValidationObjectDictionary.cs b/src/System.Net.Mime/src/System/Net/Mime/TrackingValidationObjectDictionary.cs
new file mode 100644
index 0000000000..05bb223e63
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mime/TrackingValidationObjectDictionary.cs
@@ -0,0 +1,198 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Specialized;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace System.Net
+{
+ // TrackingValidationObjectDictionary uses an internal collection of objects to store
+ // only those objects which are not strings. It still places a copy of the string
+ // value of these objects into the base StringDictionary so that the public methods
+ // of StringDictionary still function correctly.
+ // NOTE: all keys are converted to lowercase prior to adding to ensure consistency of
+ // values between keys in the StringDictionary and internalObjects because StringDictionary
+ // automatically does this internally
+ internal sealed class TrackingValidationObjectDictionary : StringDictionary
+ {
+ #region Private Fields
+
+ // even though validators may exist, we should not initialize this initially since by default it is empty
+ // and it may never be populated with values if the user does not set them
+ private readonly IDictionary<string, ValidateAndParseValue> _validators;
+ private IDictionary<string, object> _internalObjects = null;
+
+ #endregion
+
+ #region Constructors
+
+ // it is valid for validators to be null. this means that no validation should be performed
+ internal TrackingValidationObjectDictionary(IDictionary<string, ValidateAndParseValue> validators)
+ {
+ IsChanged = false;
+ _validators = validators;
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ // precondition: key must not be null
+ // addValue determines if we are doing a set (false) or an add (true)
+ private void PersistValue(string key, string value, bool addValue)
+ {
+ Debug.Assert(key != null, "key was null");
+
+ // StringDictionary will automatically store the key as lower case so
+ // we must convert it so that the validators and internalObjects will
+ // be consistent
+ key = key.ToLowerInvariant();
+
+ // StringDictionary allows keys with null values however null values for parameters in
+ // ContentDisposition have no meaning so they must be ignored on add. StringDictionary
+ // would not throw on null so this can't either since it would be a breaking change.
+ // in addition, a key with an empty value is not valid so we do not persist those either
+ if (!string.IsNullOrEmpty(value))
+ {
+ if (_validators != null && _validators.ContainsKey(key))
+ {
+ // run the validator for this key; it will throw if the value is invalid
+ object valueToAdd = _validators[key](value);
+
+ // now that the value is valid, ensure that internalObjects exists since we have to
+ // add to it
+ if (_internalObjects == null)
+ {
+ _internalObjects = new Dictionary<string, object>();
+ }
+
+ if (addValue)
+ {
+ // set will do an Add if the key does not exist but if the user
+ // specifically called Add then we must let it throw
+ _internalObjects.Add(key, valueToAdd);
+ base.Add(key, valueToAdd.ToString());
+ }
+ else
+ {
+ _internalObjects[key] = valueToAdd;
+ base[key] = valueToAdd.ToString();
+ }
+ }
+ else
+ {
+ if (addValue)
+ {
+ base.Add(key, value);
+ }
+ else
+ {
+ base[key] = value;
+ }
+ }
+ IsChanged = true;
+ }
+ }
+
+ #endregion
+
+ #region Internal Fields
+
+ // set to true if any values have been changed by any mutator method
+ internal bool IsChanged { get; set; }
+
+ // delegate to perform validation and conversion if necessary
+ // these MUST throw on invalid values. Additionally, each validator
+ // may be passed a string OR another type of object and so should react
+ // appropriately
+ internal delegate object ValidateAndParseValue(object valueToValidate);
+
+ #endregion
+
+ #region Internal Methods
+
+ // public interface only allows strings so this provides a means
+ // to get the objects when they are not strings
+ internal object InternalGet(string key)
+ {
+ // internalObjects will throw if the key is not found so we must check it
+ if (_internalObjects != null && _internalObjects.ContainsKey(key))
+ {
+ return _internalObjects[key];
+ }
+ else
+ {
+ // this will return null if the key does not exist so no check needed
+ return base[key];
+ }
+ }
+
+ // this method bypasses validation
+ // preconditions: value MUST have been validated and must not be null
+ internal void InternalSet(string key, object value)
+ {
+ // InternalSet is only used with objects that belong in internalObjects so we must always
+ // initialize it here
+ if (_internalObjects == null)
+ {
+ _internalObjects = new Dictionary<string, object>();
+ }
+
+ // always replace the existing value when we set internally
+ _internalObjects[key] = value;
+ base[key] = value.ToString();
+ IsChanged = true;
+ }
+
+ #endregion
+
+ #region Public Fields
+
+ public override string this[string key]
+ {
+ get
+ {
+ // no need to check internalObjects since the string equivalent in base will
+ // already have been set correctly when the value was originally passed in
+ return base[key];
+ }
+ set
+ {
+ PersistValue(key, value, false);
+ }
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ public override void Add(string key, string value)
+ {
+ PersistValue(key, value, true);
+ }
+
+ public override void Clear()
+ {
+ if (_internalObjects != null)
+ {
+ _internalObjects.Clear();
+ }
+ base.Clear();
+ IsChanged = true;
+ }
+
+ public override void Remove(string key)
+ {
+ if (_internalObjects != null && _internalObjects.ContainsKey(key))
+ {
+ _internalObjects.Remove(key);
+ }
+ base.Remove(key);
+ IsChanged = true;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mime/TransferEncoding.cs b/src/System.Net.Mime/src/System/Net/Mime/TransferEncoding.cs
new file mode 100644
index 0000000000..afc9ad12c0
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mime/TransferEncoding.cs
@@ -0,0 +1,15 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace System.Net.Mime
+{
+ public enum TransferEncoding
+ {
+ Unknown = -1,
+ QuotedPrintable = 0,
+ Base64 = 1,
+ SevenBit = 2,
+ EightBit = 3,
+ }
+}
diff --git a/src/System.Net.Mime/src/System/Net/Mime/WriteStateInfoBase.cs b/src/System.Net.Mime/src/System/Net/Mime/WriteStateInfoBase.cs
new file mode 100644
index 0000000000..069ce43800
--- /dev/null
+++ b/src/System.Net.Mime/src/System/Net/Mime/WriteStateInfoBase.cs
@@ -0,0 +1,135 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace System.Net.Mime
+{
+ internal class WriteStateInfoBase
+ {
+ protected readonly byte[] _header;
+ protected readonly byte[] _footer;
+ protected readonly int _maxLineLength;
+
+ protected byte[] _buffer;
+ protected int _currentLineLength;
+ protected int _currentBufferUsed;
+
+ //1024 was originally set in the encoding streams
+ protected const int DefaultBufferSize = 1024;
+
+ internal WriteStateInfoBase()
+ {
+ _header = Array.Empty<byte>();
+ _footer = Array.Empty<byte>();
+ _maxLineLength = EncodedStreamFactory.DefaultMaxLineLength;
+
+ _buffer = new byte[DefaultBufferSize];
+ _currentLineLength = 0;
+ _currentBufferUsed = 0;
+ }
+
+ internal WriteStateInfoBase(int bufferSize, byte[] header, byte[] footer, int maxLineLength)
+ : this(bufferSize, header, footer, maxLineLength, 0)
+ {
+ }
+
+ internal WriteStateInfoBase(int bufferSize, byte[] header, byte[] footer, int maxLineLength, int mimeHeaderLength)
+ {
+ _buffer = new byte[bufferSize];
+ _header = header;
+ _footer = footer;
+ _maxLineLength = maxLineLength;
+ // Account for header name, if any. e.g. "Subject: "
+ _currentLineLength = mimeHeaderLength;
+ _currentBufferUsed = 0;
+ }
+
+ internal int FooterLength => _footer.Length;
+ internal byte[] Footer => _footer;
+ internal byte[] Header => _header;
+ internal byte[] Buffer => _buffer;
+ internal int Length => _currentBufferUsed;
+ internal int CurrentLineLength => _currentLineLength;
+
+ // Make sure there is enough space in the buffer to write at least this many more bytes.
+ // This should be called before ANY direct write to Buffer.
+ private void EnsureSpaceInBuffer(int moreBytes)
+ {
+ int newsize = Buffer.Length;
+ while (_currentBufferUsed + moreBytes >= newsize)
+ {
+ newsize *= 2;
+ }
+
+ if (newsize > Buffer.Length)
+ {
+ //try to resize- if the machine doesn't have the memory to resize just let it throw
+ byte[] tempBuffer = new byte[newsize];
+
+ _buffer.CopyTo(tempBuffer, 0);
+ _buffer = tempBuffer;
+ }
+ }
+
+ internal void Append(byte aByte)
+ {
+ EnsureSpaceInBuffer(1);
+ Buffer[_currentBufferUsed++] = aByte;
+ _currentLineLength++;
+ }
+
+ internal void Append(params byte[] bytes)
+ {
+ EnsureSpaceInBuffer(bytes.Length);
+ bytes.CopyTo(_buffer, Length);
+ _currentLineLength += bytes.Length;
+ _currentBufferUsed += bytes.Length;
+ }
+
+ internal void AppendCRLF(bool includeSpace)
+ {
+ AppendFooter();
+
+ //add soft line break
+ Append((byte)'\r', (byte)'\n');
+ _currentLineLength = 0; // New Line
+ if (includeSpace)
+ {
+ //add whitespace to new line (RFC 2045, soft CRLF must be followed by whitespace char)
+ //space selected for parity with other MS email clients
+ Append((byte)' ');
+ }
+
+ AppendHeader();
+ }
+
+ internal void AppendHeader()
+ {
+ if (Header != null && Header.Length != 0)
+ {
+ Append(Header);
+ }
+ }
+
+ internal void AppendFooter()
+ {
+ if (Footer != null && Footer.Length != 0)
+ {
+ Append(Footer);
+ }
+ }
+
+ internal int MaxLineLength => _maxLineLength;
+
+ internal void Reset()
+ {
+ _currentBufferUsed = 0;
+ _currentLineLength = 0;
+ }
+
+ internal void BufferFlushed()
+ {
+ _currentBufferUsed = 0;
+ }
+ }
+}
diff --git a/src/System.Net.Mime/src/project.json b/src/System.Net.Mime/src/project.json
new file mode 100644
index 0000000000..0532e6358b
--- /dev/null
+++ b/src/System.Net.Mime/src/project.json
@@ -0,0 +1,32 @@
+{
+ "frameworks": {
+ "netstandard1.7": {
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1",
+ "System.Collections": "4.3.0-beta-devapi-24512-01",
+ "System.Collections.Specialized": "4.3.0-beta-devapi-24512-01",
+ "System.Diagnostics.Debug": "4.3.0-beta-devapi-24512-01",
+ "System.Diagnostics.Tools": "4.3.0-beta-devapi-24512-01",
+ "System.Diagnostics.Tracing": "4.3.0-beta-devapi-24512-01",
+ "System.IO": "4.3.0-beta-devapi-24512-01",
+ "System.Globalization": "4.3.0-beta-devapi-24512-01",
+ "System.Globalization.Extensions": "4.3.0-beta-devapi-24512-01",
+ "System.Net.Primitives": "4.3.0-beta-devapi-24512-01",
+ "System.Resources.ResourceManager": "4.3.0-beta-devapi-24512-01",
+ "System.Runtime": "4.3.0-beta-devapi-24512-01",
+ "System.Runtime.Extensions": "4.3.0-beta-devapi-24512-01",
+ "System.Text.Encoding": "4.3.0-beta-devapi-24512-01",
+ "System.Threading": "4.3.0-beta-devapi-24512-01",
+ "System.Threading.Tasks": "4.3.0-beta-devapi-24512-01",
+ },
+ "imports": [
+ "dotnet5.8"
+ ]
+ },
+ "net463": {
+ "dependencies": {
+ "Microsoft.TargetingPack.NETFramework.v4.6": "1.0.1"
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Mime/tests/ContentDispositionTests.cs b/src/System.Net.Mime/tests/ContentDispositionTests.cs
new file mode 100644
index 0000000000..a0b57dff13
--- /dev/null
+++ b/src/System.Net.Mime/tests/ContentDispositionTests.cs
@@ -0,0 +1,152 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Xunit;
+
+namespace System.Net.Mime.Tests
+{
+ public class ContentDispositionTests
+ {
+ [Fact]
+ public static void DefaultCtor_ExpectedDefaultPropertyValues()
+ {
+ var cd = new ContentDisposition();
+ Assert.Equal(DateTime.MinValue, cd.CreationDate);
+ Assert.Equal("attachment", cd.DispositionType);
+ Assert.Null(cd.FileName);
+ Assert.False(cd.Inline);
+ Assert.Equal(DateTime.MinValue, cd.ModificationDate);
+ Assert.Empty(cd.Parameters);
+ Assert.Equal(DateTime.MinValue, cd.ReadDate);
+ Assert.Equal(-1, cd.Size);
+ Assert.Equal("attachment", cd.ToString());
+ }
+
+ [Theory]
+ [InlineData("inline", "inline")]
+ public static void Ctor_ContentDisposition_ParsedValueMatchesExpected(string contentDisposition, string expectedDispositionType)
+ {
+ var cd = new ContentDisposition(contentDisposition);
+ Assert.Equal(expectedDispositionType, cd.DispositionType);
+ }
+
+ [Theory]
+ [InlineData(typeof(ArgumentNullException), null)]
+ public static void Ctor_InvalidContentDisposition_Throws(Type exceptionType, string contentDisposition)
+ {
+ Assert.Throws(exceptionType, () => new ContentDisposition(contentDisposition));
+ }
+
+ [Theory]
+ [InlineData(typeof(ArgumentNullException), null)]
+ [InlineData(typeof(ArgumentException), "")]
+ public static void Property_InvalidContentDisposition_Throws(Type exceptionType, string contentDisposition)
+ {
+ Assert.Throws(exceptionType, () => new ContentDisposition().DispositionType = contentDisposition);
+ }
+
+ [Fact]
+ public static void Filename_Roundtrip()
+ {
+ var cd = new ContentDisposition();
+
+ Assert.Null(cd.FileName);
+ Assert.Empty(cd.Parameters);
+
+ cd.FileName = "hello";
+ Assert.Equal("hello", cd.FileName);
+ Assert.Equal(1, cd.Parameters.Count);
+ Assert.Equal("hello", cd.Parameters["filename"]);
+ Assert.Equal("attachment; filename=hello", cd.ToString());
+
+ cd.FileName = "world";
+ Assert.Equal("world", cd.FileName);
+ Assert.Equal(1, cd.Parameters.Count);
+ Assert.Equal("world", cd.Parameters["filename"]);
+ Assert.Equal("attachment; filename=world", cd.ToString());
+
+ cd.FileName = null;
+ Assert.Null(cd.FileName);
+ Assert.Empty(cd.Parameters);
+
+ cd.FileName = string.Empty;
+ Assert.Null(cd.FileName);
+ Assert.Empty(cd.Parameters);
+ }
+
+ [Fact]
+ public static void Inline_Roundtrip()
+ {
+ var cd = new ContentDisposition();
+ Assert.False(cd.Inline);
+
+ cd.Inline = true;
+ Assert.True(cd.Inline);
+
+ cd.Inline = false;
+ Assert.False(cd.Inline);
+
+ Assert.Empty(cd.Parameters);
+ }
+
+ [Fact]
+ public static void Dates_RoundtripWithoutImpactingOtherDates()
+ {
+ var cd = new ContentDisposition();
+
+ Assert.Equal(DateTime.MinValue, cd.CreationDate);
+ Assert.Equal(DateTime.MinValue, cd.ModificationDate);
+ Assert.Equal(DateTime.MinValue, cd.ReadDate);
+ Assert.Empty(cd.Parameters);
+
+ DateTime dt1 = DateTime.Now;
+ cd.CreationDate = dt1;
+ Assert.Equal(1, cd.Parameters.Count);
+
+ DateTime dt2 = DateTime.Now;
+ cd.ModificationDate = dt2;
+ Assert.Equal(2, cd.Parameters.Count);
+
+ DateTime dt3 = DateTime.Now;
+ cd.ReadDate = dt3;
+ Assert.Equal(3, cd.Parameters.Count);
+
+ Assert.Equal(dt1, cd.CreationDate);
+ Assert.Equal(dt2, cd.ModificationDate);
+ Assert.Equal(dt3, cd.ReadDate);
+
+ Assert.Equal(3, cd.Parameters.Count);
+ }
+
+ [Fact]
+ public static void DispositionType_Roundtrip()
+ {
+ var cd = new ContentDisposition();
+
+ Assert.Equal("attachment", cd.DispositionType);
+ Assert.Empty(cd.Parameters);
+
+ cd.DispositionType = "hello";
+ Assert.Equal("hello", cd.DispositionType);
+
+ cd.DispositionType = "world";
+ Assert.Equal("world", cd.DispositionType);
+
+ Assert.Equal(0, cd.Parameters.Count);
+ }
+
+ [Fact]
+ public static void Size_Roundtrip()
+ {
+ var cd = new ContentDisposition();
+
+ Assert.Equal(-1, cd.Size);
+ Assert.Empty(cd.Parameters);
+
+ cd.Size = 42;
+ Assert.Equal(42, cd.Size);
+ Assert.Equal(1, cd.Parameters.Count);
+ }
+ }
+}
diff --git a/src/System.Net.Mime/tests/ContentTypeTests.cs b/src/System.Net.Mime/tests/ContentTypeTests.cs
new file mode 100644
index 0000000000..d5111beca0
--- /dev/null
+++ b/src/System.Net.Mime/tests/ContentTypeTests.cs
@@ -0,0 +1,173 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Xunit;
+
+namespace System.Net.Mime.Tests
+{
+ public class ContentTypeTests
+ {
+ [Fact]
+ public static void DefaultCtor_ExpectedDefaultPropertyValues()
+ {
+ var ct = new ContentType();
+ Assert.Null(ct.Boundary);
+ Assert.Null(ct.CharSet);
+ Assert.Equal("application/octet-stream", ct.MediaType);
+ Assert.Empty(ct.Parameters);
+ Assert.Null(ct.Name);
+ Assert.Equal("application/octet-stream", ct.ToString());
+ }
+
+ [Theory]
+ [InlineData("text/plain", "text/plain", null, null, null)]
+ [InlineData("text/plain; charset=us-ascii", "text/plain", "us-ascii", null, null)]
+ [InlineData("text/plain; charset=us-ascii; boundary=hello", "text/plain", "us-ascii", "hello", null)]
+ [InlineData("text/plain; boundary=hello; charset=us-ascii; name=world", "text/plain", "us-ascii", "hello", "world")]
+ [InlineData("text/plain; charset=us-ascii; name=world", "text/plain", "us-ascii", null, "world")]
+ public static void Ctor_ContentString_ParsedValueMatchesExpected(
+ string contentType, string expectedMediaType, string expectedCharSet, string expectedBoundary, string expectedName)
+ {
+ var ct = new ContentType(contentType);
+ Assert.Equal(expectedMediaType, ct.MediaType);
+ Assert.Equal(expectedCharSet, ct.CharSet);
+ Assert.Equal(expectedBoundary, ct.Boundary);
+ Assert.Equal(expectedName, ct.Name);
+
+ Assert.Equal(
+ (expectedCharSet != null ? 1 : 0) + (expectedBoundary != null ? 1 : 0) + (expectedName != null ? 1 : 0),
+ ct.Parameters.Count);
+ Assert.Equal(expectedCharSet, ct.Parameters["charset"]);
+ Assert.Equal(expectedBoundary, ct.Parameters["boundary"]);
+ Assert.Equal(expectedName, ct.Parameters["name"]);
+ }
+
+ [Theory]
+ [InlineData(typeof(ArgumentNullException), null)]
+ [InlineData(typeof(ArgumentException), "")]
+ [InlineData(typeof(FormatException), " ")]
+ [InlineData(typeof(FormatException), "text/plain\x4F1A")]
+ [InlineData(typeof(FormatException), "text/plain ,")]
+ [InlineData(typeof(FormatException), "text/plain,")]
+ [InlineData(typeof(FormatException), "text/plain; charset=utf-8 ,")]
+ [InlineData(typeof(FormatException), "text/plain; charset=utf-8,")]
+ [InlineData(typeof(FormatException), "textplain")]
+ [InlineData(typeof(IndexOutOfRangeException), "text/")] // TODO: This seems like a bug, but it also exists in the .NET Framework
+ [InlineData(typeof(FormatException), ",, , ,,text/plain; charset=iso-8859-1; q=1.0,\r\n */xml; charset=utf-8; q=0.5,,,")]
+ [InlineData(typeof(FormatException), "text/plain; charset=iso-8859-1; q=1.0, */xml; charset=utf-8; q=0.5")]
+ [InlineData(typeof(FormatException), " , */xml; charset=utf-8; q=0.5 ")]
+ [InlineData(typeof(FormatException), "text/plain; charset=iso-8859-1; q=1.0 , ")]
+ public static void Ctor_InvalidContentType_Throws(Type exceptionType, string contentType)
+ {
+ Assert.Throws(exceptionType, () => new ContentType(contentType));
+ }
+
+ [Fact]
+ public static void Parameters_Roundtrip()
+ {
+ var ct = new ContentType();
+
+ Assert.Empty(ct.Parameters);
+ Assert.Same(ct.Parameters, ct.Parameters);
+
+ ct.Parameters.Add("hello", "world");
+ Assert.Equal("world", ct.Parameters["hello"]);
+ }
+
+ [Fact]
+ public static void Boundary_Roundtrip()
+ {
+ var ct = new ContentType();
+
+ Assert.Null(ct.Boundary);
+ Assert.Empty(ct.Parameters);
+
+ ct.Boundary = "hello";
+ Assert.Equal("hello", ct.Boundary);
+ Assert.Equal(1, ct.Parameters.Count);
+
+ ct.Boundary = "world";
+ Assert.Equal("world", ct.Boundary);
+ Assert.Equal(1, ct.Parameters.Count);
+
+ ct.Boundary = null;
+ Assert.Null(ct.Boundary);
+ Assert.Empty(ct.Parameters);
+
+ ct.Boundary = string.Empty;
+ Assert.Null(ct.Boundary);
+ Assert.Empty(ct.Parameters);
+ }
+
+ [Fact]
+ public static void CharSet_Roundtrip()
+ {
+ var ct = new ContentType();
+
+ Assert.Null(ct.CharSet);
+ Assert.Empty(ct.Parameters);
+
+ ct.CharSet = "hello";
+ Assert.Equal("hello", ct.CharSet);
+ Assert.Equal(1, ct.Parameters.Count);
+
+ ct.CharSet = "world";
+ Assert.Equal("world", ct.CharSet);
+ Assert.Equal(1, ct.Parameters.Count);
+
+ ct.CharSet = null;
+ Assert.Null(ct.CharSet);
+ Assert.Empty(ct.Parameters);
+
+ ct.CharSet = "";
+ Assert.Null(ct.CharSet);
+ Assert.Empty(ct.Parameters);
+ }
+
+ [Fact]
+ public static void Name_Roundtrip()
+ {
+ var ct = new ContentType();
+
+ Assert.Null(ct.Name);
+ Assert.Empty(ct.Parameters);
+
+ ct.Name = "hello";
+ Assert.Equal("hello", ct.Name);
+ Assert.Equal(1, ct.Parameters.Count);
+
+ ct.Name = "world";
+ Assert.Equal("world", ct.Name);
+ Assert.Equal(1, ct.Parameters.Count);
+
+ ct.Name = null;
+ Assert.Null(ct.Name);
+ Assert.Empty(ct.Parameters);
+
+ ct.Name = "";
+ Assert.Null(ct.Name);
+ Assert.Empty(ct.Parameters);
+ }
+
+ [Fact]
+ public static void MediaType_Set_InvalidArgs_Throws()
+ {
+ var ct = new ContentType();
+ Assert.Throws<ArgumentNullException>("value", () => ct.MediaType = null);
+ Assert.Throws<ArgumentException>("value", () => ct.MediaType = "");
+ }
+
+ [Fact]
+ public static void MediaType_Roundtrip()
+ {
+ var ct = new ContentType("text/plain; charset=us-ascii");
+ Assert.Equal("text/plain", ct.MediaType);
+ Assert.Equal("us-ascii", ct.CharSet);
+
+ ct.MediaType = "application/xml";
+ Assert.Equal("application/xml", ct.MediaType);
+ Assert.Equal("us-ascii", ct.CharSet);
+ }
+ }
+}
diff --git a/src/System.Net.Mime/tests/System.Net.Mime.Tests.builds b/src/System.Net.Mime/tests/System.Net.Mime.Tests.builds
new file mode 100644
index 0000000000..ee83df02af
--- /dev/null
+++ b/src/System.Net.Mime/tests/System.Net.Mime.Tests.builds
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.props))\dir.props" />
+ <ItemGroup>
+ <Project Include="System.Net.Mime.Tests.csproj">
+ <TestTFMs>netcoreapp1.1</TestTFMs>
+ </Project>
+ <Project Include="System.Net.Mime.Tests.csproj">
+ <TestTFMs>net463</TestTFMs>
+ <OSGroup>Windows_NT</OSGroup>
+ </Project>
+ </ItemGroup>
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.traversal.targets))\dir.traversal.targets" />
+</Project>
+
diff --git a/src/System.Net.Mime/tests/System.Net.Mime.Tests.csproj b/src/System.Net.Mime/tests/System.Net.Mime.Tests.csproj
new file mode 100644
index 0000000000..997421caaa
--- /dev/null
+++ b/src/System.Net.Mime/tests/System.Net.Mime.Tests.csproj
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Build">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.props))\dir.props" />
+ <PropertyGroup>
+ <OutputType>Library</OutputType>
+ <!--
+ Until we get first class support for NS1.7 and Netcoreapp1.1
+ we need to hard code $(TestTFM) and $(TestNugetTargetMoniker)
+ in the project file.
+ -->
+ <TestTFM>netcoreapp1.1</TestTFM>
+ <NugetTargetMoniker>.NETStandard,Version=v1.7</NugetTargetMoniker>
+ <ProjectGuid>{0D1E2954-A5C7-4B8C-932A-31EB4A96A726}</ProjectGuid>
+ </PropertyGroup>
+ <ItemGroup>
+ <TestNugetTargetMoniker Include="$(NugetTargetMoniker)">
+ <Folder>netcoreapp1.1</Folder>
+ </TestNugetTargetMoniker>
+ </ItemGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'netstandard1.7_Debug|AnyCPU' " />
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'netstandard1.7_Release|AnyCPU' " />
+ <ItemGroup>
+ <Compile Include="ContentDispositionTests.cs" />
+ <Compile Include="ContentTypeTests.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\pkg\System.Net.Mime.pkgproj">
+ <Name>System.Net.Mime</Name>
+ <Project>{53D09AF4-0C13-4197-B8AD-9746F0374E88}</Project>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="project.json" />
+ </ItemGroup>
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.targets))\dir.targets" />
+</Project> \ No newline at end of file
diff --git a/src/System.Net.Mime/tests/project.json b/src/System.Net.Mime/tests/project.json
new file mode 100644
index 0000000000..3cc1f36e87
--- /dev/null
+++ b/src/System.Net.Mime/tests/project.json
@@ -0,0 +1,48 @@
+{
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "4.3.0-beta-devapi-24512-01",
+ "System.Collections": "4.3.0-beta-devapi-24512-01",
+ "System.Collections.Specialized": "4.3.0-beta-devapi-24512-01",
+ "System.Diagnostics.Debug": "4.3.0-beta-devapi-24512-01",
+ "System.ObjectModel": "4.3.0-beta-devapi-24512-01",
+ "System.Reflection.TypeExtensions": "4.3.0-beta-devapi-24512-01",
+ "System.Runtime": "4.3.0-beta-devapi-24512-01",
+ "System.Runtime.Extensions": "4.3.0-beta-devapi-24512-01",
+ "System.Runtime.Serialization.Primitives": "4.3.0-beta-devapi-24512-01",
+ "System.Text.RegularExpressions": "4.3.0-beta-devapi-24512-01",
+ "System.Diagnostics.Process": "4.3.0-beta-devapi-24512-01",
+ "System.Collections.NonGeneric": "4.3.0-beta-devapi-24512-01",
+ "System.IO": "4.3.0-beta-devapi-24512-01",
+ "System.Reflection": "4.3.0-beta-devapi-24512-01",
+ "System.Reflection.Extensions": "4.3.0-beta-devapi-24512-01",
+ "System.Threading.Tasks": "4.3.0-beta-devapi-24512-01",
+ "System.Text.Encoding": "4.3.0-beta-devapi-24512-01",
+ "test-runtime": {
+ "target": "project",
+ "exclude": "compile"
+ },
+ "Microsoft.xunit.netcore.extensions": "1.0.0-prerelease-00807-03",
+ "Microsoft.DotNet.BuildTools.TestSuite": "1.0.0-prerelease-00807-03"
+ },
+ "frameworks": {
+ "netstandard1.7": {}
+ },
+ "supports": {
+ "coreFx.Test.netcoreapp1.1-ns17": {
+ "netstandard1.7": [
+ "win7-x86",
+ "win7-x64",
+ "win10-arm64",
+ "osx.10.10-x64",
+ "centos.7-x64",
+ "debian.8-x64",
+ "rhel.7-x64",
+ "ubuntu.14.04-x64",
+ "ubuntu.16.04-x64",
+ "fedora.23-x64",
+ "linux-x64",
+ "opensuse.13.2-x64"
+ ]
+ }
+ }
+}