From d85894513b9ba3d11917c9b46082a8a0adf448a6 Mon Sep 17 00:00:00 2001 From: Jeffrey Stedfast Date: Wed, 7 Feb 2024 08:50:32 -0500 Subject: [PATCH] Revert change making it possible to change FormatOptions.Default property values This reverts the following commits: * 4b77a9f6607d722f3f0527ff0315261d53d87329: Allow changing the FormatOptions.Default values * e3bab7b5efb153a32b67a873227f5ec13230c1f7: Improve Header.GetRawValue() logic * 22881de2d361e3464d4a9038fdb5cf3c9cca2c9e: Cache more of the FormatOptions in each Header * 9570ebd54ac25af202242795ad3132ed705b3954: Added FormatOptions.ReformatHeaders and fixed unit tests ...And reopens issue #993. I will be revisiting this set of changes for v5.0 when I'll have more flexibility with behavioral changes. The problem with this set was not that it "didn't work", but rather that it changed expected behavior. I also wasn't sure if I really liked the new FormatOptions.ReformatHeaders property naming. --- MimeKit/Cryptography/ArcVerifier.cs | 11 +- MimeKit/Cryptography/DkimVerifier.cs | 7 +- MimeKit/Cryptography/DkimVerifierBase.cs | 16 -- MimeKit/Cryptography/MultipartSigned.cs | 2 - MimeKit/FormatOptions.cs | 208 +++++++++-------------- MimeKit/Header.cs | 111 +++--------- UnitTests/FormatOptionsTests.cs | 12 ++ 7 files changed, 131 insertions(+), 236 deletions(-) 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"); + } } }