| File: System\Net\Mime\MimeBasePart.cs | Web Access |
| Project: System.Net.Mail.csproj (System.Net.Mail) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Specialized; using System.Net.Mail; using System.Text; using System.Threading; using System.Threading.Tasks; namespace System.Net.Mime { internal abstract class MimeBasePart { internal const string DefaultCharSet = "utf-8"; 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) { //no need to encode if it's pure ascii if (IsAscii(value, false)) { return value; } encoding ??= Encoding.GetEncoding(DefaultCharSet); IEncodableStream stream = EncodedStreamFactory.GetEncoderForHeader(encoding, base64Encoding, headerLength); stream.EncodeString(value, encoding); return stream.GetEncodedString(); } internal static string DecodeHeaderValue(string? value) { if (string.IsNullOrEmpty(value)) { return string.Empty; } if (!value.Contains("=?", StringComparison.Ordinal)) { return value; } StringBuilder decodedValue = new StringBuilder(value.Length); ReadOnlySpan<char> valueSpan = value; bool decodedAny = false; bool previousTokenWasEncoded = false; int current = 0; while (current < valueSpan.Length) { int whitespaceStart = current; while (current < valueSpan.Length && IsLinearWhiteSpace(valueSpan[current])) { current++; } int tokenStart = current; while (current < valueSpan.Length && !IsLinearWhiteSpace(valueSpan[current])) { current++; } if (tokenStart == current) { decodedValue.Append(valueSpan[whitespaceStart..current]); break; } ReadOnlySpan<char> token = valueSpan[tokenStart..current]; if (TryDecodeHeaderValue(token, out string decodedToken)) { if (!previousTokenWasEncoded) { decodedValue.Append(valueSpan[whitespaceStart..tokenStart]); } decodedValue.Append(decodedToken); decodedAny = true; previousTokenWasEncoded = true; } else { decodedValue.Append(valueSpan[whitespaceStart..tokenStart]); decodedValue.Append(token); previousTokenWasEncoded = false; } } return decodedAny ? decodedValue.ToString() : value; } // 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 (string.IsNullOrEmpty(value)) { return null; } ReadOnlySpan<char> valueSpan = value; int current = 0; while (current < valueSpan.Length) { while (current < valueSpan.Length && IsLinearWhiteSpace(valueSpan[current])) { current++; } int tokenStart = current; while (current < valueSpan.Length && !IsLinearWhiteSpace(valueSpan[current])) { current++; } ReadOnlySpan<char> token = valueSpan[tokenStart..current]; if (TryParseEncodedWord(token, out Range charSet, out _, out _)) { Encoding? encoding = TryGetEncoding(token[charSet]); if (encoding is not null) { return encoding; } } } return null; } internal static bool IsFullyEncoded(string value) { ReadOnlySpan<char> valueSpan = value; bool encodedAny = false; int current = 0; while (current < valueSpan.Length) { int whitespaceStart = current; while (current < valueSpan.Length && IsLinearWhiteSpace(valueSpan[current])) { current++; } if (!IsValidHeaderWhiteSpace(valueSpan[whitespaceStart..current])) { return false; } int tokenStart = current; while (current < valueSpan.Length && !IsLinearWhiteSpace(valueSpan[current])) { current++; } if (tokenStart == current) { break; } ReadOnlySpan<char> token = valueSpan[tokenStart..current]; if (!TryParseEncodedWord(token, out Range charSet, out _, out _) || TryGetEncoding(token[charSet]) is null || token.ContainsAny('"', '\\')) { return false; } encodedAny = true; } return encodedAny; } private static bool TryDecodeHeaderValue(ReadOnlySpan<char> value, out string decodedValue) { decodedValue = string.Empty; if (!TryParseEncodedWord(value, out Range charSetRange, out bool base64Encoding, out Range encodedTextRange)) { return false; } Encoding? encoding = TryGetEncoding(value[charSetRange]); if (encoding is null) { return false; } ReadOnlySpan<char> encodedText = value[encodedTextRange]; byte[] buffer = new byte[encodedText.Length]; Encoding.ASCII.GetBytes(encodedText, buffer); IEncodableStream stream = EncodedStreamFactory.GetEncoderForHeader(encoding, base64Encoding, 0); int decodedLength = stream.DecodeBytes(buffer); decodedValue = encoding.GetString(buffer, 0, decodedLength); return true; } private static bool TryParseEncodedWord( ReadOnlySpan<char> value, out Range charSet, out bool base64Encoding, out Range encodedText) { charSet = default; base64Encoding = false; encodedText = default; if (value.Length < 7 || !value.StartsWith("=?") || !value.EndsWith("?=")) { return false; } foreach (char c in value) { if (c is < '!' or > '~') { return false; } } int charSetEnd = value[2..].IndexOf('?'); if (charSetEnd <= 0) { return false; } charSetEnd += 2; int encodingEnd = value[(charSetEnd + 1)..].IndexOf('?'); if (encodingEnd != 1) { return false; } encodingEnd += charSetEnd + 1; char encodingIdentifier = value[charSetEnd + 1]; if (encodingIdentifier is 'B' or 'b') { base64Encoding = true; } else if (encodingIdentifier is not ('Q' or 'q')) { return false; } ReadOnlySpan<char> encodedTextValue = value[(encodingEnd + 1)..^2]; if (encodedTextValue.Contains('?')) { return false; } charSet = 2..charSetEnd; encodedText = (encodingEnd + 1)..^2; return true; } private static Encoding? TryGetEncoding(ReadOnlySpan<char> charSet) { try { return Encoding.GetEncoding(charSet.ToString()); } catch (ArgumentException) { return null; } catch (NotSupportedException) { return null; } } private static bool IsValidHeaderWhiteSpace(ReadOnlySpan<char> value) { for (int i = 0; i < value.Length; i++) { if (value[i] is ' ' or '\t') { continue; } if (value[i] != '\r' || i + 2 >= value.Length || value[i + 1] != '\n' || value[i + 2] is not (' ' or '\t')) { return false; } i += 2; } return true; } private static bool IsLinearWhiteSpace(char value) => value is ' ' or '\t' or '\r' or '\n'; internal static bool IsAscii(string value, bool permitCROrLF) { ArgumentNullException.ThrowIfNull(value); return Ascii.IsValid(value) && (permitCROrLF || !value.AsSpan().ContainsAny('\r', '\n')); } 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 _headers ??= new HeaderCollection(); _contentType ??= new ContentType(); _contentType.PersistIfNeeded(_headers, false); _contentDisposition?.PersistIfNeeded(_headers, false); return _headers; } } internal ContentType ContentType { get { return _contentType ??= new ContentType(); } set { ArgumentNullException.ThrowIfNull(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 abstract Task SendAsync<TIOAdapter>(BaseWriter writer, bool allowUnicode, CancellationToken cancellationToken) where TIOAdapter : IReadWriteAdapter; } }