diff --git a/MimeKit/Cryptography/DkimVerifierBase.cs b/MimeKit/Cryptography/DkimVerifierBase.cs index 8d72452ec2..f55ee69ee6 100644 --- a/MimeKit/Cryptography/DkimVerifierBase.cs +++ b/MimeKit/Cryptography/DkimVerifierBase.cs @@ -160,6 +160,7 @@ 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; diff --git a/MimeKit/Cryptography/MultipartSigned.cs b/MimeKit/Cryptography/MultipartSigned.cs index d8aa70e086..e0734b8210 100644 --- a/MimeKit/Cryptography/MultipartSigned.cs +++ b/MimeKit/Cryptography/MultipartSigned.cs @@ -767,6 +767,7 @@ 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; @@ -837,6 +838,7 @@ 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 743fae64ef..d57b8c405d 100644 --- a/MimeKit/FormatOptions.cs +++ b/MimeKit/FormatOptions.cs @@ -26,6 +26,7 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using MimeKit.IO.Filters; @@ -76,14 +77,32 @@ public class FormatOptions const int DefaultMaxLineLength = 78; - ParameterEncodingMethod parameterEncodingMethod; - bool alwaysQuoteParameterValues; - bool allowMixedHeaderCharsets; - NewLineFormat newLineFormat; - bool verifyingSignature; - bool ensureNewLine; - bool international; - int maxLineLength; + 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. @@ -105,6 +124,54 @@ 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. @@ -117,12 +184,13 @@ public class FormatOptions /// is out of range. It must be between 60 and 998. /// public int MaxLineLength { - get { return maxLineLength; } + get { return encodedOptions & MaxLineLengthMask; } set { if (value < MinimumLineLength || value > MaximumLineLength) throw new ArgumentOutOfRangeException (nameof (value)); - maxLineLength = value; + encodedOptions &= ~MaxLineLengthMask; + encodedOptions |= value & MaxLineLengthMask; } } @@ -138,12 +206,12 @@ public int MaxLineLength { /// is not a valid . /// public NewLineFormat NewLineFormat { - get { return newLineFormat; } + get { return GetNewLineFormat (encodedOptions); } set { - switch (newLineFormat) { + switch (value) { case NewLineFormat.Unix: case NewLineFormat.Dos: - newLineFormat = value; + SetNewLineFormat (ref encodedOptions, value); break; default: throw new ArgumentOutOfRangeException (nameof (value)); @@ -164,8 +232,8 @@ public NewLineFormat NewLineFormat { /// /// true in order to ensure that the message will end with a new-line sequence; otherwise, false. public bool EnsureNewLine { - get { return ensureNewLine; } - set { ensureNewLine = value; } + get { return GetValue (encodedOptions, EnsureNewLineMask); } + set { SetValue (ref encodedOptions, EnsureNewLineOffset, value); } } internal IMimeFilter CreateNewLineFilter (bool ensureNewLine = false) @@ -187,8 +255,8 @@ internal byte[] NewLineBytes { } internal bool VerifyingSignature { - get { return verifyingSignature; } - set { verifyingSignature = value; } + get { return GetValue (encodedOptions, VerifyingSignatureMask); } + set { SetValue (ref encodedOptions, VerifyingSignatureOffset, value); } } /// @@ -220,8 +288,8 @@ public HashSet HiddenHeaders { /// /// true if the new internationalized formatting should be used; otherwise, false. public bool International { - get { return international; } - set { international = value; } + get { return GetValue (encodedOptions, InternationalMask); } + set { SetValue (ref encodedOptions, InternationalOffset, value); } } /// @@ -241,8 +309,8 @@ 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 allowMixedHeaderCharsets; } - set { allowMixedHeaderCharsets = value; } + get { return GetValue (encodedOptions, AllowMixedHeaderCharsetsMask); } + set { SetValue (ref encodedOptions, AllowMixedHeaderCharsetsOffset, value); } } /// @@ -263,12 +331,12 @@ public bool AllowMixedHeaderCharsets { /// is not a valid value. /// public ParameterEncodingMethod ParameterEncodingMethod { - get { return parameterEncodingMethod; } + get { return GetParameterEncodingMethod (encodedOptions); } set { switch (value) { case ParameterEncodingMethod.Rfc2047: case ParameterEncodingMethod.Rfc2231: - parameterEncodingMethod = value; + SetParameterEncodingMethod (ref encodedOptions, value); break; default: throw new ArgumentOutOfRangeException (nameof (value)); @@ -288,16 +356,31 @@ public ParameterEncodingMethod ParameterEncodingMethod { /// /// true if Content-Type and Content-Disposition parameters should always be quoted; otherwise, false. public bool AlwaysQuoteParameterValues { - get { return alwaysQuoteParameterValues; } - set { alwaysQuoteParameterValues = value; } + get { return GetValue (encodedOptions, AlwaysQuoteParameterValuesMask); } + set { SetValue (ref encodedOptions, AlwaysQuoteParameterValuesOffset, value); } + } + + /// + /// 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); } } static FormatOptions () { VerifySignature = new FormatOptions { NewLineFormat = NewLineFormat.Dos, - VerifyingSignature = true, - EnsureNewLine = false + VerifyingSignature = true }; Default = new FormatOptions (); } @@ -312,17 +395,13 @@ static FormatOptions () public FormatOptions () { HiddenHeaders = new HashSet (); - parameterEncodingMethod = ParameterEncodingMethod.Rfc2231; - alwaysQuoteParameterValues = false; - maxLineLength = DefaultMaxLineLength; - allowMixedHeaderCharsets = false; - ensureNewLine = false; - international = false; + ParameterEncodingMethod = ParameterEncodingMethod.Rfc2231; + MaxLineLength = DefaultMaxLineLength; if (Environment.NewLine.Length == 1) - newLineFormat = NewLineFormat.Unix; + NewLineFormat = NewLineFormat.Unix; else - newLineFormat = NewLineFormat.Dos; + NewLineFormat = NewLineFormat.Dos; } /// @@ -335,31 +414,9 @@ public FormatOptions () public FormatOptions Clone () { return new FormatOptions { - maxLineLength = maxLineLength, - newLineFormat = newLineFormat, - ensureNewLine = ensureNewLine, HiddenHeaders = new HashSet (HiddenHeaders), - allowMixedHeaderCharsets = allowMixedHeaderCharsets, - parameterEncodingMethod = parameterEncodingMethod, - alwaysQuoteParameterValues = alwaysQuoteParameterValues, - verifyingSignature = verifyingSignature, - international = international + encodedOptions = encodedOptions }; } - - internal int EncodeHeaderFormatOptions () - { - // Notes: - // 1. The lowest 10 bites are used to encode MaxLineLength - // 2. The 11th bit is used to encode International - // 3. The 12th bit is used to encode AllowMixedHeaderCharsets - // 4. The 13th bit is used to encode AlwaysQuoteParameterValues - // 5. The 14th and 15th bits are used to encode ParameterEncodingMethod - return (((int) parameterEncodingMethod) & 0x03) << 13 | - (alwaysQuoteParameterValues ? 1 : 0) << 12 | - (allowMixedHeaderCharsets ? 1 : 0) << 11 | - (international ? 1 : 0) << 10 | - (maxLineLength & 0x03FF); - } } } diff --git a/MimeKit/Header.cs b/MimeKit/Header.cs index 939e4ea746..a87b42846b 100644 --- a/MimeKit/Header.cs +++ b/MimeKit/Header.cs @@ -45,9 +45,16 @@ public class Header static readonly char[] WhiteSpace = { ' ', '\t', '\r', '\n' }; internal readonly ParserOptions Options; + enum Reformattable : byte + { + Always, + ForceOnly, + Never, + } + readonly byte[] rawField; int cachedHeaderFormatOptions; - bool explicitRawValue; + Reformattable reformattable; string textValue; byte[] rawValue; @@ -322,6 +329,7 @@ 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; @@ -361,6 +369,7 @@ internal protected Header (ParserOptions options, byte[] field, byte[] value, bo count++; } + reformattable = Reformattable.ForceOnly; Options = options; rawField = field; rawValue = value; @@ -386,6 +395,10 @@ internal protected Header (ParserOptions options, HeaderId id, string field, byt { // 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; @@ -404,7 +417,7 @@ public Header Clone () { return new Header (Options, Id, Field, rawField, rawValue) { cachedHeaderFormatOptions = cachedHeaderFormatOptions, - explicitRawValue = explicitRawValue, + reformattable = reformattable, IsInvalid = IsInvalid, // if the textValue has already been calculated, set it on the cloned header as well. @@ -934,6 +947,35 @@ 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); @@ -1383,12 +1425,20 @@ protected virtual byte[] FormatRawValue (FormatOptions format, Encoding encoding [MethodImpl (MethodImplOptions.AggressiveInlining)] bool FormattingOptionsChanged (FormatOptions format) { - return format.EncodeHeaderFormatOptions () != cachedHeaderFormatOptions; + return format.EncodedHeaderOptions != cachedHeaderFormatOptions; } internal byte[] GetRawValue (FormatOptions format) { - if (!explicitRawValue && !format.VerifyingSignature && FormattingOptionsChanged (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) { switch (Id) { case HeaderId.DispositionNotificationTo: case HeaderId.ResentReplyTo: @@ -1405,31 +1455,27 @@ internal byte[] GetRawValue (FormatOptions format) case HeaderId.To: return ReformatAddressHeader (Options, format, CharsetUtils.UTF8, Field, rawValue); case HeaderId.Received: - // Note: Received headers should never be reformatted. - return rawValue; + return EncodeReceivedHeader (Options, format, CharsetUtils.UTF8, Field, Value); case HeaderId.ResentMessageId: case HeaderId.InReplyTo: case HeaderId.MessageId: case HeaderId.ContentId: - // Note: No text that can be internationalized. + // Note: These headers are not affected by the FormatOptions. return rawValue; case HeaderId.References: - // Note: No text that can be internationalized. - return rawValue; + return ReformatReferencesHeader (Options, format, CharsetUtils.UTF8, Field, 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 rawValue; + return EncodeDispositionNotificationOptions (Options, format, CharsetUtils.UTF8, Field, Value); case HeaderId.ArcAuthenticationResults: case HeaderId.AuthenticationResults: - // Note: No text that can be internationalized. - return rawValue; case HeaderId.ArcMessageSignature: case HeaderId.ArcSeal: case HeaderId.DkimSignature: - // TODO: Is there any value in reformatting this for internationalized text? + // Note: These headers should never be reformatted. return rawValue; case HeaderId.ListArchive: case HeaderId.ListHelp: @@ -1478,7 +1524,7 @@ public void SetValue (FormatOptions format, Encoding encoding, string value) textValue = Unfold (value.Trim ()); rawValue = FormatRawValue (format, encoding, textValue); - explicitRawValue = false; + reformattable = Reformattable.Always; // 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 @@ -1486,7 +1532,7 @@ public void SetValue (FormatOptions format, Encoding encoding, string value) // // TODO: Should EncodeHeaderFormatOptions() also encode the NewLineFormat? // And should we also cache the charset encoding used? - cachedHeaderFormatOptions = format.EncodeHeaderFormatOptions (); + cachedHeaderFormatOptions = format.EncodedHeaderOptions; OnChanged (); } @@ -1596,10 +1642,10 @@ 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 explicitRawValue is set to true, GetRawValue(FormatOptions) will always return - // the cached rawValue so it doesn't matter what what the value of headerFormatOptions is. + // 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;