diff options
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" + ] + } + } +} |