diff --git a/MimeKit/Cryptography/ArcVerifier.cs b/MimeKit/Cryptography/ArcVerifier.cs index ce4c3bb714..c7c2e8717e 100644 --- a/MimeKit/Cryptography/ArcVerifier.cs +++ b/MimeKit/Cryptography/ArcVerifier.cs @@ -396,6 +396,9 @@ async Task VerifyArcMessageSignatureAsync (FormatOptions options, MimeMess if (!IsEnabled (signatureAlgorithm)) return false; + options = options.Clone (); + options.NewLineFormat = NewLineFormat.Dos; + // first check the body hash (if that's invalid, then the entire signature is invalid) if (!VerifyBodyHash (options, message, signatureAlgorithm, bodyAlgorithm, maxLength, bh)) return false; @@ -430,6 +433,9 @@ async Task VerifyArcSealAsync (FormatOptions options, ArcHeaderSet[] sets, if ((key is RsaKeyParameters rsa) && rsa.Modulus.BitLength < MinimumRsaKeyLength) return false; + options = options.Clone (); + options.NewLineFormat = NewLineFormat.Dos; + using (var stream = new DkimSignatureStream (CreateVerifyContext (algorithm, key))) { using (var filtered = new FilteredStream (stream)) { filtered.Add (options.CreateNewLineFilter ()); @@ -667,7 +673,6 @@ async Task VerifyAsync (FormatOptions options, MimeMessage int newest = count - 1; result.Seals = new ArcHeaderValidationResult[count]; - options = GetVerifyOptions (options); // validate the most recent Arc-Message-Signature try { @@ -783,7 +788,7 @@ public Task VerifyAsync (FormatOptions options, MimeMessage /// public ArcValidationResult Verify (MimeMessage message, CancellationToken cancellationToken = default) { - return Verify (FormatOptions.VerifySignature, message, cancellationToken); + return Verify (FormatOptions.Default, message, cancellationToken); } /// @@ -806,7 +811,7 @@ public ArcValidationResult Verify (MimeMessage message, CancellationToken cancel /// public Task VerifyAsync (MimeMessage message, CancellationToken cancellationToken = default) { - return VerifyAsync (FormatOptions.VerifySignature, message, cancellationToken); + return VerifyAsync (FormatOptions.Default, message, cancellationToken); } } } diff --git a/MimeKit/Cryptography/DkimVerifier.cs b/MimeKit/Cryptography/DkimVerifier.cs index 7dd5ece0e3..c9b4019689 100644 --- a/MimeKit/Cryptography/DkimVerifier.cs +++ b/MimeKit/Cryptography/DkimVerifier.cs @@ -125,7 +125,8 @@ async Task VerifyAsync (FormatOptions options, MimeMessage message, Header if (!IsEnabled (signatureAlgorithm)) return false; - options = GetVerifyOptions (options); + options = options.Clone (); + options.NewLineFormat = NewLineFormat.Dos; // first check the body hash (if that's invalid, then the entire signature is invalid) if (!VerifyBodyHash (options, message, signatureAlgorithm, bodyAlgorithm, maxLength, bh)) @@ -241,7 +242,7 @@ public Task VerifyAsync (FormatOptions options, MimeMessage message, Heade /// public bool Verify (MimeMessage message, Header dkimSignature, CancellationToken cancellationToken = default) { - return Verify (FormatOptions.VerifySignature, message, dkimSignature, cancellationToken); + return Verify (FormatOptions.Default, message, dkimSignature, cancellationToken); } /// @@ -273,7 +274,7 @@ public bool Verify (MimeMessage message, Header dkimSignature, CancellationToken /// public Task VerifyAsync (MimeMessage message, Header dkimSignature, CancellationToken cancellationToken = default) { - return VerifyAsync (FormatOptions.VerifySignature, message, dkimSignature, cancellationToken); + return VerifyAsync (FormatOptions.Default, message, dkimSignature, cancellationToken); } } } diff --git a/MimeKit/Cryptography/DkimVerifierBase.cs b/MimeKit/Cryptography/DkimVerifierBase.cs index f55ee69ee6..7d84e6469d 100644 --- a/MimeKit/Cryptography/DkimVerifierBase.cs +++ b/MimeKit/Cryptography/DkimVerifierBase.cs @@ -150,22 +150,6 @@ public bool IsEnabled (DkimSignatureAlgorithm algorithm) return (enabledSignatureAlgorithms & (1 << (int) algorithm)) != 0; } - /// - /// Get the formatting options for use with verifying DKIM signatures. - /// - /// The user-requested formatting options. - /// The formatting options. - internal static FormatOptions GetVerifyOptions (FormatOptions format) - { - var options = format.Clone (); - options.NewLineFormat = NewLineFormat.Dos; - options.VerifyingSignature = true; - options.ReformatHeaders = false; - options.HiddenHeaders.Clear (); - options.EnsureNewLine = false; - return options; - } - static bool IsWhiteSpace (char c) { return c == ' ' || c == '\t'; diff --git a/MimeKit/Cryptography/MultipartSigned.cs b/MimeKit/Cryptography/MultipartSigned.cs index e0734b8210..d8aa70e086 100644 --- a/MimeKit/Cryptography/MultipartSigned.cs +++ b/MimeKit/Cryptography/MultipartSigned.cs @@ -767,7 +767,6 @@ public DigitalSignatureCollection Verify (CryptographyContext ctx, CancellationT var options = FormatOptions.Default.Clone (); options.NewLineFormat = NewLineFormat.Dos; options.VerifyingSignature = true; - options.ReformatHeaders = false; this[0].WriteTo (options, cleartext, cancellationToken); cleartext.Position = 0; @@ -838,7 +837,6 @@ public async Task VerifyAsync (CryptographyContext c var options = FormatOptions.Default.Clone (); options.NewLineFormat = NewLineFormat.Dos; options.VerifyingSignature = true; - options.ReformatHeaders = false; await this[0].WriteToAsync (options, cleartext, cancellationToken); cleartext.Position = 0; diff --git a/MimeKit/FormatOptions.cs b/MimeKit/FormatOptions.cs index d57b8c405d..89fefc9928 100644 --- a/MimeKit/FormatOptions.cs +++ b/MimeKit/FormatOptions.cs @@ -26,7 +26,6 @@ using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; using MimeKit.IO.Filters; @@ -77,42 +76,14 @@ public class FormatOptions const int DefaultMaxLineLength = 78; - const int VerifyingSignatureOffset = 23; - const int VerifyingSignatureMask = 0x01 << VerifyingSignatureOffset; - const int ReformatHeadersOffset = 22; - const int ReformatHeadersMask = 0x01 << ReformatHeadersOffset; - const int ReformatContentOffset = 21; - const int ReformatContentMask = 0x01 << ReformatContentOffset; - const int EnsureNewLineOffset = 20; - const int EnsureNewLineMask = 0x01 << EnsureNewLineOffset; - const int NewLineFormatOffset = 18; - const int NewLineFormatMask = 0x03 << NewLineFormatOffset; - - // The following offsets/options are used for header formatting and so need to stay together. - const int ParameterEncodingMethodOffset = 13; - const int ParameterEncodingMethodMask = 0x03 << ParameterEncodingMethodOffset; - const int AlwaysQuoteParameterValuesOffset = 12; - const int AlwaysQuoteParameterValuesMask = 0x01 << AlwaysQuoteParameterValuesOffset; - const int AllowMixedHeaderCharsetsOffset = 11; - const int AllowMixedHeaderCharsetsMask = 0x01 << AllowMixedHeaderCharsetsOffset; - const int InternationalOffset = 10; - const int InternationalMask = 0x01 << InternationalOffset; - const int MaxLineLengthOffset = 0; - const int MaxLineLengthMask = 0x03FF; - - const int EncodedHeaderOptionsMask = 0x07FF; - - int encodedOptions; - - /// - /// The default formatting options for verifying signatures. - /// - /// - /// If a custom is not passed to methods such as - /// , - /// the default options will be used. - /// - internal static readonly FormatOptions VerifySignature; + ParameterEncodingMethod parameterEncodingMethod; + bool alwaysQuoteParameterValues; + bool allowMixedHeaderCharsets; + NewLineFormat newLineFormat; + bool verifyingSignature; + bool ensureNewLine; + bool international; + int maxLineLength; /// /// The default formatting options. @@ -124,54 +95,6 @@ public class FormatOptions /// public static readonly FormatOptions Default; - [MethodImpl (MethodImplOptions.AggressiveInlining)] - static bool GetValue (int encodedOptions, int mask) - { - return (encodedOptions & mask) != 0; - } - - [MethodImpl (MethodImplOptions.AggressiveInlining)] - static void SetValue (ref int encodedOptions, int offset, bool value) - { - if (value) - encodedOptions |= 1 << offset; - else - encodedOptions &= ~(1 << offset); - } - - [MethodImpl (MethodImplOptions.AggressiveInlining)] - static NewLineFormat GetNewLineFormat (int encodedOptions) - { - return (NewLineFormat) ((encodedOptions & NewLineFormatMask) >> NewLineFormatOffset); - } - - [MethodImpl (MethodImplOptions.AggressiveInlining)] - static void SetNewLineFormat (ref int encodedOptions, NewLineFormat value) - { - encodedOptions &= ~NewLineFormatMask; - encodedOptions |= ((int) value) << NewLineFormatOffset; - } - - [MethodImpl (MethodImplOptions.AggressiveInlining)] - static ParameterEncodingMethod GetParameterEncodingMethod (int encodedOptions) - { - return (ParameterEncodingMethod) ((encodedOptions & ParameterEncodingMethodMask) >> ParameterEncodingMethodOffset); - } - - [MethodImpl (MethodImplOptions.AggressiveInlining)] - static void SetParameterEncodingMethod (ref int encodedOptions, ParameterEncodingMethod value) - { - encodedOptions &= ~ParameterEncodingMethodMask; - encodedOptions |= ((int) value) << ParameterEncodingMethodOffset; - } - - /// - /// Get an encoded representation of the formatting options relevant to header formatting. - /// - internal int EncodedHeaderOptions { - get { return encodedOptions & EncodedHeaderOptionsMask; } - } - /// /// Get or set the maximum line length used by the encoders. The encoders /// use this value to determine where to place line breaks. @@ -183,14 +106,19 @@ internal int EncodedHeaderOptions { /// /// is out of range. It must be between 60 and 998. /// + /// + /// cannot be changed. + /// public int MaxLineLength { - get { return encodedOptions & MaxLineLengthMask; } + get { return maxLineLength; } set { + if (this == Default) + throw new InvalidOperationException ("The default formatting options cannot be changed."); + if (value < MinimumLineLength || value > MaximumLineLength) throw new ArgumentOutOfRangeException (nameof (value)); - encodedOptions &= ~MaxLineLengthMask; - encodedOptions |= value & MaxLineLengthMask; + maxLineLength = value; } } @@ -205,13 +133,19 @@ public int MaxLineLength { /// /// is not a valid . /// + /// + /// cannot be changed. + /// public NewLineFormat NewLineFormat { - get { return GetNewLineFormat (encodedOptions); } + get { return newLineFormat; } set { - switch (value) { + if (this == Default) + throw new InvalidOperationException ("The default formatting options cannot be changed."); + + switch (newLineFormat) { case NewLineFormat.Unix: case NewLineFormat.Dos: - SetNewLineFormat (ref encodedOptions, value); + newLineFormat = value; break; default: throw new ArgumentOutOfRangeException (nameof (value)); @@ -231,9 +165,17 @@ public NewLineFormat NewLineFormat { /// that writing the message back to a stream will always end with a new-line sequence. /// /// true in order to ensure that the message will end with a new-line sequence; otherwise, false. + /// + /// cannot be changed. + /// public bool EnsureNewLine { - get { return GetValue (encodedOptions, EnsureNewLineMask); } - set { SetValue (ref encodedOptions, EnsureNewLineOffset, value); } + get { return ensureNewLine; } + set { + if (this == Default) + throw new InvalidOperationException ("The default formatting options cannot be changed."); + + ensureNewLine = value; + } } internal IMimeFilter CreateNewLineFilter (bool ensureNewLine = false) @@ -255,8 +197,8 @@ internal byte[] NewLineBytes { } internal bool VerifyingSignature { - get { return GetValue (encodedOptions, VerifyingSignatureMask); } - set { SetValue (ref encodedOptions, VerifyingSignatureOffset, value); } + get { return verifyingSignature; } + set { verifyingSignature = value; } } /// @@ -287,9 +229,17 @@ public HashSet HiddenHeaders { /// (rfc6855). /// /// true if the new internationalized formatting should be used; otherwise, false. + /// + /// cannot be changed. + /// public bool International { - get { return GetValue (encodedOptions, InternationalMask); } - set { SetValue (ref encodedOptions, InternationalOffset, value); } + get { return international; } + set { + if (this == Default) + throw new InvalidOperationException ("The default formatting options cannot be changed."); + + international = value; + } } /// @@ -309,8 +259,13 @@ public bool International { /// /// true if the formatter should be allowed to use us-ascii and/or iso-8859-1 when encoding headers; otherwise, false. public bool AllowMixedHeaderCharsets { - get { return GetValue (encodedOptions, AllowMixedHeaderCharsetsMask); } - set { SetValue (ref encodedOptions, AllowMixedHeaderCharsetsOffset, value); } + get { return allowMixedHeaderCharsets; } + set { + if (this == Default) + throw new InvalidOperationException ("The default formatting options cannot be changed."); + + allowMixedHeaderCharsets = value; + } } /// @@ -331,12 +286,15 @@ public bool AllowMixedHeaderCharsets { /// is not a valid value. /// public ParameterEncodingMethod ParameterEncodingMethod { - get { return GetParameterEncodingMethod (encodedOptions); } + get { return parameterEncodingMethod; } set { + if (this == Default) + throw new InvalidOperationException ("The default formatting options cannot be changed."); + switch (value) { case ParameterEncodingMethod.Rfc2047: case ParameterEncodingMethod.Rfc2231: - SetParameterEncodingMethod (ref encodedOptions, value); + parameterEncodingMethod = value; break; default: throw new ArgumentOutOfRangeException (nameof (value)); @@ -356,32 +314,17 @@ public ParameterEncodingMethod ParameterEncodingMethod { /// /// true if Content-Type and Content-Disposition parameters should always be quoted; otherwise, false. public bool AlwaysQuoteParameterValues { - get { return GetValue (encodedOptions, AlwaysQuoteParameterValuesMask); } - set { SetValue (ref encodedOptions, AlwaysQuoteParameterValuesOffset, value); } - } + get { return alwaysQuoteParameterValues; } + set { + if (this == Default) + throw new InvalidOperationException ("The default formatting options cannot be changed."); - /// - /// Get or set whether headers should be reformatted when writing back out to a stream. - /// - /// - /// Gets or sets whether headers should be reformatted when writing back out to a stream. - /// If is set to true, then all headers (except those set using - /// ) will be reformatted. - /// If is set to false, only headers that were not constructed by - /// parser will be (re)formatted. - /// - /// true if headers should always be reformatted; otherwise, false. - public bool ReformatHeaders { - get { return GetValue (encodedOptions, ReformatHeadersMask); } - set { SetValue (ref encodedOptions, ReformatHeadersOffset, value); } + alwaysQuoteParameterValues = value; + } } static FormatOptions () { - VerifySignature = new FormatOptions { - NewLineFormat = NewLineFormat.Dos, - VerifyingSignature = true - }; Default = new FormatOptions (); } @@ -395,13 +338,17 @@ static FormatOptions () public FormatOptions () { HiddenHeaders = new HashSet (); - ParameterEncodingMethod = ParameterEncodingMethod.Rfc2231; - MaxLineLength = DefaultMaxLineLength; + parameterEncodingMethod = ParameterEncodingMethod.Rfc2231; + alwaysQuoteParameterValues = false; + maxLineLength = DefaultMaxLineLength; + allowMixedHeaderCharsets = false; + ensureNewLine = false; + international = false; if (Environment.NewLine.Length == 1) - NewLineFormat = NewLineFormat.Unix; + newLineFormat = NewLineFormat.Unix; else - NewLineFormat = NewLineFormat.Dos; + newLineFormat = NewLineFormat.Dos; } /// @@ -414,8 +361,15 @@ public FormatOptions () public FormatOptions Clone () { return new FormatOptions { + maxLineLength = maxLineLength, + newLineFormat = newLineFormat, + ensureNewLine = ensureNewLine, HiddenHeaders = new HashSet (HiddenHeaders), - encodedOptions = encodedOptions + allowMixedHeaderCharsets = allowMixedHeaderCharsets, + parameterEncodingMethod = parameterEncodingMethod, + alwaysQuoteParameterValues = alwaysQuoteParameterValues, + verifyingSignature = verifyingSignature, + international = international }; } } diff --git a/MimeKit/Header.cs b/MimeKit/Header.cs index a87b42846b..0c83c31713 100644 --- a/MimeKit/Header.cs +++ b/MimeKit/Header.cs @@ -45,16 +45,14 @@ public class Header static readonly char[] WhiteSpace = { ' ', '\t', '\r', '\n' }; internal readonly ParserOptions Options; - enum Reformattable : byte - { - Always, - ForceOnly, - Never, - } + // cached FormatOptions that change the way the header is formatted + //bool allowMixedHeaderCharsets = FormatOptions.Default.AllowMixedHeaderCharsets; + //NewLineFormat newLineFormat = FormatOptions.Default.NewLineFormat; + //bool international = FormatOptions.Default.International; + //Encoding charset = CharsetUtils.UTF8; readonly byte[] rawField; - int cachedHeaderFormatOptions; - Reformattable reformattable; + bool explicitRawValue; string textValue; byte[] rawValue; @@ -296,7 +294,6 @@ public Header (string field, string value) : this (Encoding.UTF8, field, value) /// The raw value of the header. protected Header (ParserOptions options, HeaderId id, string name, byte[] field, byte[] value) { - // FIXME: This .ctor should become private and we should modify it to be exactly what the Clone() method needs. Options = options; rawField = field; rawValue = value; @@ -309,7 +306,8 @@ protected Header (ParserOptions options, HeaderId id, string name, byte[] field, /// /// /// Creates a new message or entity header with the specified raw values. - /// This constructor is used by . + /// This constructor is used by the + /// TryParse methods. /// /// The parser options used. /// The raw header field. @@ -321,7 +319,6 @@ protected Header (ParserOptions options, HeaderId id, string name, byte[] field, #endif internal protected Header (ParserOptions options, byte[] field, int fieldNameLength, byte[] value, bool invalid) { - // FIXME: This .ctor should become internal-only. It is only used from MimeReader.CreateHeader(). Span chars = fieldNameLength <= 32 ? stackalloc char[32] : new char[fieldNameLength]; @@ -329,7 +326,6 @@ internal protected Header (ParserOptions options, byte[] field, int fieldNameLen for (int i = 0; i < fieldNameLength; i++) chars[i] = (char) field[i]; - reformattable = Reformattable.ForceOnly; Options = options; rawField = field; rawValue = value; @@ -356,8 +352,6 @@ internal protected Header (ParserOptions options, byte[] field, int fieldNameLen #endif internal protected Header (ParserOptions options, byte[] field, byte[] value, bool invalid) { - // Note: This .ctor should become internal-only. It is only used by DkimVerifierBase and Header.TryParse(). - // Possibly these locations should be updated to use the same .ctor as MimeReader? Span chars = field.Length <= 32 ? stackalloc char[32] : new char[field.Length]; @@ -369,7 +363,6 @@ internal protected Header (ParserOptions options, byte[] field, byte[] value, bo count++; } - reformattable = Reformattable.ForceOnly; Options = options; rawField = field; rawValue = value; @@ -393,12 +386,6 @@ internal protected Header (ParserOptions options, byte[] field, byte[] value, bo /// The raw value of the header. internal protected Header (ParserOptions options, HeaderId id, string field, byte[] value) { - // Note: This .ctor should probably become internal-only. It is only used by MimeMessage - // and MimeEntity when serializing new values for headers. - - // FIXME: Ideally MimeMessage and MimeEntity should pass in the FormatOptions they used. - // We know they used FormatOptions.Default, though, so this is "safe" for now. - cachedHeaderFormatOptions = FormatOptions.Default.EncodedHeaderOptions; Options = options; rawField = Encoding.ASCII.GetBytes (field); rawValue = value; @@ -416,8 +403,7 @@ internal protected Header (ParserOptions options, HeaderId id, string field, byt public Header Clone () { return new Header (Options, Id, Field, rawField, rawValue) { - cachedHeaderFormatOptions = cachedHeaderFormatOptions, - reformattable = reformattable, + explicitRawValue = explicitRawValue, IsInvalid = IsInvalid, // if the textValue has already been calculated, set it on the cloned header as well. @@ -947,35 +933,6 @@ static byte[] EncodeDispositionNotificationOptions (ParserOptions options, Forma return encoding.GetBytes (encoded.ToString ()); } - static byte[] ReformatReferencesHeader (ParserOptions options, FormatOptions format, Encoding encoding, string field, byte[] rawValue) - { - var encoded = new ValueStringBuilder (rawValue.Length); - int lineLength = field.Length + 1; - int count = 0; - - foreach (var reference in MimeUtils.EnumerateReferences (rawValue, 0, rawValue.Length)) { - if (count > 0 && lineLength + reference.Length + 3 > format.MaxLineLength) { - encoded.Append (format.NewLine); - encoded.Append ('\t'); - lineLength = 1; - count = 0; - } else { - encoded.Append (' '); - lineLength++; - } - - encoded.Append ('<'); - encoded.Append (reference); - encoded.Append ('>'); - lineLength += reference.Length + 2; - count++; - } - - encoded.Append (format.NewLine); - - return encoding.GetBytes (encoded.ToString ()); - } - static byte[] EncodeReferencesHeader (ParserOptions options, FormatOptions format, Encoding encoding, string field, string value) { var encoded = new ValueStringBuilder (value.Length); @@ -1422,23 +1379,9 @@ protected virtual byte[] FormatRawValue (FormatOptions format, Encoding encoding } } - [MethodImpl (MethodImplOptions.AggressiveInlining)] - bool FormattingOptionsChanged (FormatOptions format) - { - return format.EncodedHeaderOptions != cachedHeaderFormatOptions; - } - internal byte[] GetRawValue (FormatOptions format) { - bool reformat; - - switch (reformattable) { - case Reformattable.ForceOnly: reformat = format.ReformatHeaders && FormattingOptionsChanged (format); break; - case Reformattable.Always: reformat = FormattingOptionsChanged (format); break; - default: /* Never */ reformat = false; break; - } - - if (reformat) { + if (format.International && !explicitRawValue) { switch (Id) { case HeaderId.DispositionNotificationTo: case HeaderId.ResentReplyTo: @@ -1455,27 +1398,31 @@ internal byte[] GetRawValue (FormatOptions format) case HeaderId.To: return ReformatAddressHeader (Options, format, CharsetUtils.UTF8, Field, rawValue); case HeaderId.Received: - return EncodeReceivedHeader (Options, format, CharsetUtils.UTF8, Field, Value); + // Note: Received headers should never be reformatted. + return rawValue; case HeaderId.ResentMessageId: case HeaderId.InReplyTo: case HeaderId.MessageId: case HeaderId.ContentId: - // Note: These headers are not affected by the FormatOptions. + // Note: No text that can be internationalized. return rawValue; case HeaderId.References: - return ReformatReferencesHeader (Options, format, CharsetUtils.UTF8, Field, rawValue); + // Note: No text that can be internationalized. + return rawValue; case HeaderId.ContentDisposition: return ReformatContentDisposition (Options, format, CharsetUtils.UTF8, Field, rawValue); case HeaderId.ContentType: return ReformatContentType (Options, format, CharsetUtils.UTF8, Field, rawValue); case HeaderId.DispositionNotificationOptions: - return EncodeDispositionNotificationOptions (Options, format, CharsetUtils.UTF8, Field, Value); + return rawValue; case HeaderId.ArcAuthenticationResults: case HeaderId.AuthenticationResults: + // Note: No text that can be internationalized. + return rawValue; case HeaderId.ArcMessageSignature: case HeaderId.ArcSeal: case HeaderId.DkimSignature: - // Note: These headers should never be reformatted. + // TODO: Is there any value in reformatting this for internationalized text? return rawValue; case HeaderId.ListArchive: case HeaderId.ListHelp: @@ -1524,15 +1471,13 @@ public void SetValue (FormatOptions format, Encoding encoding, string value) textValue = Unfold (value.Trim ()); rawValue = FormatRawValue (format, encoding, textValue); - reformattable = Reformattable.Always; + explicitRawValue = false; - // Note: This caches the formatting options used to generate the rawValue so that - // GetRawValue(FormatOptions) can determine if it can return the cached value or - // if it needs to reformat the value to fit the new formatting options. - // - // TODO: Should EncodeHeaderFormatOptions() also encode the NewLineFormat? - // And should we also cache the charset encoding used? - cachedHeaderFormatOptions = format.EncodedHeaderOptions; + // cache the formatting options that change the way the header is formatted + //allowMixedHeaderCharsets = format.AllowMixedHeaderCharsets; + //newLineFormat = format.NewLineFormat; + //international = format.International; + //charset = encoding; OnChanged (); } @@ -1642,10 +1587,7 @@ public void SetRawValue (byte[] value) if (value.Length == 0 || value[value.Length - 1] != (byte) '\n') throw new ArgumentException ("The raw value MUST end with a new-line character.", nameof (value)); - // Note: When reformattable is set to Never, GetRawValue(FormatOptions) will always return the - // cached rawValue so it doesn't matter what what the value of cachedHeaderFormatOptions is. - reformattable = Reformattable.Never; - cachedHeaderFormatOptions = 0; + explicitRawValue = true; rawValue = value; textValue = null; @@ -1738,7 +1680,6 @@ static bool IsBlank (byte c) return c.IsBlank (); } - // Note: This TryParse() method only needs to be marked internal because MimeParser uses it. internal static unsafe bool TryParse (ParserOptions options, byte* input, int length, bool strict, out Header header) { byte* inend = input + length; diff --git a/UnitTests/FormatOptionsTests.cs b/UnitTests/FormatOptionsTests.cs index 6193d608bc..ee03d8d947 100644 --- a/UnitTests/FormatOptionsTests.cs +++ b/UnitTests/FormatOptionsTests.cs @@ -41,5 +41,17 @@ public void TestArgumentExceptions () Assert.DoesNotThrow (() => format.MaxLineLength = 72); } + + [Test] + public void TestInvalidOperationExceptions () + { + Assert.Throws (() => FormatOptions.Default.MaxLineLength = 998, "MaxLineLength"); + Assert.Throws (() => FormatOptions.Default.EnsureNewLine = true, "EnsureNewLine"); + Assert.Throws (() => FormatOptions.Default.International = true, "International"); + Assert.Throws (() => FormatOptions.Default.NewLineFormat = NewLineFormat.Dos, "NewLineFormat"); + Assert.Throws (() => FormatOptions.Default.AllowMixedHeaderCharsets = true, "AllowMixedHeaderCharsets"); + Assert.Throws (() => FormatOptions.Default.ParameterEncodingMethod = ParameterEncodingMethod.Rfc2047, "ParameterEncodingMethod"); + Assert.Throws (() => FormatOptions.Default.AlwaysQuoteParameterValues = true, "AlwaysQuoteParameterValues"); + } } }